mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 22:52:22 +00:00
some preparation for reccurence expansion
This commit is contained in:
15
Cargo.lock
generated
15
Cargo.lock
generated
@@ -2894,6 +2894,19 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rrule"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "720acfb4980b9d8a6a430f6d7a11933e701ebbeba5eee39cc9d8c5f932aaff74"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"chrono-tz",
|
||||||
|
"log",
|
||||||
|
"regex",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "0.9.8"
|
version = "0.9.8"
|
||||||
@@ -3191,6 +3204,7 @@ dependencies = [
|
|||||||
"ical",
|
"ical",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"regex",
|
"regex",
|
||||||
|
"rrule",
|
||||||
"rustical_xml",
|
"rustical_xml",
|
||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
@@ -3227,6 +3241,7 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"regex",
|
"regex",
|
||||||
|
"rrule",
|
||||||
"rstest",
|
"rstest",
|
||||||
"rstest_reuse",
|
"rstest_reuse",
|
||||||
"rustical_dav",
|
"rustical_dav",
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ lazy_static.workspace = true
|
|||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
strum.workspace = true
|
strum.workspace = true
|
||||||
strum_macros.workspace = true
|
strum_macros.workspace = true
|
||||||
|
rrule = "0.14"
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
pub mod rrule;
|
|
||||||
|
|
||||||
mod property_ext;
|
mod property_ext;
|
||||||
pub use property_ext::*;
|
pub use property_ext::*;
|
||||||
|
|
||||||
|
|||||||
@@ -1,211 +0,0 @@
|
|||||||
use super::{RecurrenceFrequency, RecurrenceLimit, RecurrenceRule};
|
|
||||||
use crate::CalDateTime;
|
|
||||||
use chrono::{Datelike, Duration, IsoWeek, NaiveDate, Weekday, WeekdaySet};
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.10
|
|
||||||
+----------+--------+--------+-------+-------+------+-------+------+
|
|
||||||
| |SECONDLY|MINUTELY|HOURLY |DAILY |WEEKLY|MONTHLY|YEARLY|
|
|
||||||
+----------+--------+--------+-------+-------+------+-------+------+
|
|
||||||
|BYMONTH |Limit |Limit |Limit |Limit |Limit |Limit |Expand|
|
|
||||||
+----------+--------+--------+-------+-------+------+-------+------+
|
|
||||||
|BYWEEKNO |N/A |N/A |N/A |N/A |N/A |N/A |Expand|
|
|
||||||
+----------+--------+--------+-------+-------+------+-------+------+
|
|
||||||
|BYYEARDAY |Limit |Limit |Limit |N/A |N/A |N/A |Expand|
|
|
||||||
+----------+--------+--------+-------+-------+------+-------+------+
|
|
||||||
|BYMONTHDAY|Limit |Limit |Limit |Limit |N/A |Expand |Expand|
|
|
||||||
+----------+--------+--------+-------+-------+------+-------+------+
|
|
||||||
|BYDAY |Limit |Limit |Limit |Limit |Expand|Note 1 |Note 2|
|
|
||||||
+----------+--------+--------+-------+-------+------+-------+------+
|
|
||||||
|BYHOUR |Limit |Limit |Limit |Expand |Expand|Expand |Expand|
|
|
||||||
+----------+--------+--------+-------+-------+------+-------+------+
|
|
||||||
|BYMINUTE |Limit |Limit |Expand |Expand |Expand|Expand |Expand|
|
|
||||||
+----------+--------+--------+-------+-------+------+-------+------+
|
|
||||||
|BYSECOND |Limit |Expand |Expand |Expand |Expand|Expand |Expand|
|
|
||||||
+----------+--------+--------+-------+-------+------+-------+------+
|
|
||||||
|BYSETPOS |Limit |Limit |Limit |Limit |Limit |Limit |Limit |
|
|
||||||
+----------+--------+--------+-------+-------+------+-------+------+
|
|
||||||
|
|
||||||
Note 1: Limit if BYMONTHDAY is present; otherwise, special expand
|
|
||||||
for MONTHLY.
|
|
||||||
|
|
||||||
Note 2: Limit if BYYEARDAY or BYMONTHDAY is present; otherwise,
|
|
||||||
special expand for WEEKLY if BYWEEKNO present; otherwise,
|
|
||||||
special expand for MONTHLY if BYMONTH present; otherwise,
|
|
||||||
special expand for YEARLY.
|
|
||||||
*/
|
|
||||||
|
|
||||||
pub(crate) fn is_leap_year(year: i32) -> bool {
|
|
||||||
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn last_day_of_month(year: i32, month: u8) -> u8 {
|
|
||||||
match month {
|
|
||||||
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
|
|
||||||
4 | 6 | 9 | 11 => 30,
|
|
||||||
2 => {
|
|
||||||
if is_leap_year(year) {
|
|
||||||
29
|
|
||||||
} else {
|
|
||||||
28
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => panic!("invalid month: {}", month),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn list_days_in_month(year: i32, month: u8) -> &'static [i8] {
|
|
||||||
match last_day_of_month(year, month) {
|
|
||||||
28 => &DAYS28,
|
|
||||||
29 => &DAYS29,
|
|
||||||
30 => &DAYS30,
|
|
||||||
31 => &DAYS31,
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn first_weekday(year: i32, wkst: Weekday) -> NaiveDate {
|
|
||||||
// The first week of the year (starting from WKST) is the week having at
|
|
||||||
// least four days in the year
|
|
||||||
// isoweek_start marks week 1 with WKST
|
|
||||||
let mut isoweek_start = NaiveDate::from_isoywd_opt(year, 1, wkst).unwrap();
|
|
||||||
if isoweek_start.year() == year && isoweek_start.day0() >= 4 {
|
|
||||||
// We can fit another week before
|
|
||||||
isoweek_start -= Duration::days(7);
|
|
||||||
}
|
|
||||||
isoweek_start
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn weeks_in_year(year: i32, wkst: Weekday) -> (NaiveDate, u8) {
|
|
||||||
let first_day = first_weekday(year, wkst);
|
|
||||||
let next_year_first_day = first_weekday(year + 1, wkst);
|
|
||||||
(
|
|
||||||
first_day,
|
|
||||||
(next_year_first_day - first_day).num_weeks() as u8,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writing this bodge was easier than doing it the hard way :D
|
|
||||||
const DAYS28: [i8; 28] = [
|
|
||||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
|
|
||||||
27, 28,
|
|
||||||
];
|
|
||||||
const DAYS29: [i8; 29] = [
|
|
||||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
|
|
||||||
27, 28, 29,
|
|
||||||
];
|
|
||||||
const DAYS30: [i8; 30] = [
|
|
||||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
|
|
||||||
27, 28, 29, 30,
|
|
||||||
];
|
|
||||||
const DAYS31: [i8; 31] = [
|
|
||||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
|
|
||||||
27, 28, 29, 30, 31,
|
|
||||||
];
|
|
||||||
|
|
||||||
impl RecurrenceRule {
|
|
||||||
pub fn between(
|
|
||||||
&self,
|
|
||||||
start: CalDateTime,
|
|
||||||
end: Option<CalDateTime>,
|
|
||||||
limit: Option<usize>,
|
|
||||||
) -> Vec<CalDateTime> {
|
|
||||||
let mut end = end.as_ref();
|
|
||||||
if let Some(RecurrenceLimit::Until(until)) = &self.limit {
|
|
||||||
end = Some(end.unwrap_or(until).min(until));
|
|
||||||
}
|
|
||||||
let mut count = if let Some(RecurrenceLimit::Count(count)) = &self.limit {
|
|
||||||
*count
|
|
||||||
} else {
|
|
||||||
2048
|
|
||||||
};
|
|
||||||
if let Some(limit) = limit {
|
|
||||||
count = count.min(limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
let datetimes = vec![];
|
|
||||||
|
|
||||||
let mut year = start.year();
|
|
||||||
let mut month0 = start.month0();
|
|
||||||
// let months0 = self.bymonth0.clone().unwrap_or(vec![start.month0() as i8]);
|
|
||||||
|
|
||||||
let offset_weekdays = self.offset_weekdays();
|
|
||||||
let absolute_weekdays = self.absolute_weekdays();
|
|
||||||
|
|
||||||
while datetimes.len() < count {
|
|
||||||
let mut result_dates = vec![start.date()];
|
|
||||||
// Iterate over frequency*interval
|
|
||||||
match self.frequency {
|
|
||||||
RecurrenceFrequency::Yearly => year += self.interval as i32,
|
|
||||||
RecurrenceFrequency::Monthly => {
|
|
||||||
month0 += self.interval;
|
|
||||||
year += (month0 as f32 / 12.).floor() as i32;
|
|
||||||
month0 %= 12;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::single_match)]
|
|
||||||
match self.frequency {
|
|
||||||
RecurrenceFrequency::Yearly => {}
|
|
||||||
// RecurrenceFrequency::Monthly => {
|
|
||||||
// // Filter bymonth
|
|
||||||
// if let Some(bymonth0) = &self.bymonth0 {
|
|
||||||
// if !bymonth0.contains(&(month0 as u8)) {
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if let Some(monthdays) = &self.bymonthday {
|
|
||||||
// for monthday in monthdays {
|
|
||||||
// let monthday = if *monthday > 0 {
|
|
||||||
// *monthday as u32
|
|
||||||
// } else {
|
|
||||||
// // +1 because -1 is the last day
|
|
||||||
// last_day_of_month(year, month0 as u8 + 1) as u32
|
|
||||||
// + 1
|
|
||||||
// // monthday is negative
|
|
||||||
// + *monthday as u32
|
|
||||||
// };
|
|
||||||
// let date = if let Some(date) =
|
|
||||||
// NaiveDate::from_ymd_opt(year, month0 as u32 + 1, monthday)
|
|
||||||
// {
|
|
||||||
// date
|
|
||||||
// } else {
|
|
||||||
// continue;
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// if let Some(weekdays) = absolute_weekdays {
|
|
||||||
// if weekdays.contains(date.weekday()) {
|
|
||||||
// dates.insert(date);
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// dates.insert(date);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(end) = end {}
|
|
||||||
// datetimes.push(datetime.to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
datetimes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
// use crate::{CalDateTime, rrule::RecurrenceRule};
|
|
||||||
|
|
||||||
// #[test]
|
|
||||||
// fn test_between() {
|
|
||||||
// // Example: Last workday of the month
|
|
||||||
// let rrule = RecurrenceRule::parse("FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1").unwrap();
|
|
||||||
// let start = CalDateTime::parse("20250516T133000Z", None).unwrap();
|
|
||||||
// assert_eq!(rrule.between(start, None, Some(4)), vec![]);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use chrono::{Datelike, Duration, NaiveDate};
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
RecurrenceRule,
|
|
||||||
iter::{last_day_of_month, list_days_in_month, weeks_in_year},
|
|
||||||
};
|
|
||||||
|
|
||||||
impl RecurrenceRule {
|
|
||||||
pub fn week_expansion(&self, year: i32) -> Option<HashSet<NaiveDate>> {
|
|
||||||
let absolute_weekdays = self.absolute_weekdays();
|
|
||||||
|
|
||||||
if let Some(byweekno) = &self.byweekno {
|
|
||||||
let mut dates = HashSet::new();
|
|
||||||
|
|
||||||
let weekstart = self.week_start.unwrap_or(chrono::Weekday::Mon);
|
|
||||||
let (first_weekstart, num_weeks) = weeks_in_year(year, weekstart);
|
|
||||||
|
|
||||||
let weeknums0: Vec<u8> = byweekno
|
|
||||||
.iter()
|
|
||||||
.map(|num| {
|
|
||||||
if *num < 0 {
|
|
||||||
(num_weeks as i8 + *num) as u8
|
|
||||||
} else {
|
|
||||||
(*num - 1) as u8
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for weeknum0 in weeknums0 {
|
|
||||||
let weekstart_date = first_weekstart + Duration::weeks(weeknum0 as i64);
|
|
||||||
// Iterate over the week and check if the weekdays are allowed
|
|
||||||
for i in 0..7 {
|
|
||||||
let date = weekstart_date + Duration::days(i);
|
|
||||||
if let Some(weekdays) = absolute_weekdays {
|
|
||||||
if weekdays.contains(date.weekday()) {
|
|
||||||
dates.insert(date);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dates.insert(date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(dates)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn month_expansion(&self, year: i32) -> Option<HashSet<NaiveDate>> {
|
|
||||||
let offset_weekdays = self.offset_weekdays();
|
|
||||||
let absolute_weekdays = self.absolute_weekdays();
|
|
||||||
|
|
||||||
if let Some(bymonth0) = &self.bymonth0 {
|
|
||||||
let mut dates = HashSet::new();
|
|
||||||
for month0 in bymonth0 {
|
|
||||||
// Add BYMONTHDAY or all days
|
|
||||||
let monthdays = self
|
|
||||||
.bymonthday
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or(list_days_in_month(year, month0 + 1));
|
|
||||||
|
|
||||||
for monthday in monthdays {
|
|
||||||
let monthday = if *monthday > 0 {
|
|
||||||
*monthday as u32
|
|
||||||
} else {
|
|
||||||
// +1 because -1 is the last day
|
|
||||||
last_day_of_month(year, month0 + 1) as u32
|
|
||||||
+ 1
|
|
||||||
// monthday is negative
|
|
||||||
+ *monthday as u32
|
|
||||||
};
|
|
||||||
let date = if let Some(date) =
|
|
||||||
NaiveDate::from_ymd_opt(year, *month0 as u32 + 1, monthday)
|
|
||||||
{
|
|
||||||
date
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(weekdays) = absolute_weekdays {
|
|
||||||
if weekdays.contains(date.weekday()) {
|
|
||||||
dates.insert(date);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dates.insert(date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add offset weekdays
|
|
||||||
if let Some(offset_weekdays) = &offset_weekdays {
|
|
||||||
for (num, day) in offset_weekdays.iter() {
|
|
||||||
let date = if *num > 0 {
|
|
||||||
NaiveDate::from_weekday_of_month_opt(
|
|
||||||
year,
|
|
||||||
*month0 as u32 + 1,
|
|
||||||
*day,
|
|
||||||
*num as u8,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// If index negative:
|
|
||||||
// Go to first day of next month and walk back the weeks
|
|
||||||
NaiveDate::from_weekday_of_month_opt(
|
|
||||||
year,
|
|
||||||
*month0 as u32 + 1 + 1,
|
|
||||||
*day,
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
.map(|date| date + Duration::weeks(*num))
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(date) = date {
|
|
||||||
dates.insert(date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(dates)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dates_yearly(
|
|
||||||
&self,
|
|
||||||
start: NaiveDate,
|
|
||||||
end: Option<NaiveDate>,
|
|
||||||
limit: usize,
|
|
||||||
) -> Vec<NaiveDate> {
|
|
||||||
let mut dates = vec![start];
|
|
||||||
let mut year = start.year();
|
|
||||||
|
|
||||||
while dates.len() < limit {
|
|
||||||
// Expand BYMONTH
|
|
||||||
let month_expansion = self.month_expansion(year);
|
|
||||||
|
|
||||||
// Expand BYWEEKNO
|
|
||||||
let week_expansion = self.week_expansion(year);
|
|
||||||
|
|
||||||
let mut occurence_set = match (month_expansion, week_expansion) {
|
|
||||||
(Some(month_expansion), Some(week_expansion)) => month_expansion
|
|
||||||
.intersection(&week_expansion)
|
|
||||||
.cloned()
|
|
||||||
.collect(),
|
|
||||||
(Some(month_expansion), None) => month_expansion,
|
|
||||||
(None, Some(week_expansion)) => week_expansion,
|
|
||||||
(None, None) => start
|
|
||||||
.with_year(year)
|
|
||||||
.map(|date| HashSet::from([date]))
|
|
||||||
.unwrap_or_default(),
|
|
||||||
}
|
|
||||||
.into_iter()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
occurence_set.sort();
|
|
||||||
if let Some(bysetpos) = &self.bysetpos {
|
|
||||||
occurence_set = bysetpos
|
|
||||||
.iter()
|
|
||||||
.filter_map(|i| {
|
|
||||||
if *i > 0 {
|
|
||||||
occurence_set.get((*i - 1) as usize)
|
|
||||||
} else {
|
|
||||||
occurence_set.get((occurence_set.len() as i64 + *i) as usize)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
}
|
|
||||||
dates.extend_from_slice(occurence_set.as_slice());
|
|
||||||
year += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
dates
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,318 +0,0 @@
|
|||||||
use super::{CalDateTime, CalDateTimeError};
|
|
||||||
use chrono::{Weekday, WeekdaySet};
|
|
||||||
use std::{num::ParseIntError, str::FromStr};
|
|
||||||
use strum_macros::EnumString;
|
|
||||||
|
|
||||||
mod iter;
|
|
||||||
mod iter_monthly;
|
|
||||||
mod iter_yearly;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum ParserError {
|
|
||||||
#[error("Missing RRULE FREQ")]
|
|
||||||
MissingFrequency,
|
|
||||||
#[error("Invalid RRULE part: {0}")]
|
|
||||||
InvalidPart(String),
|
|
||||||
#[error(transparent)]
|
|
||||||
StrumError(#[from] strum::ParseError),
|
|
||||||
#[error(transparent)]
|
|
||||||
ParseIntError(#[from] ParseIntError),
|
|
||||||
#[error(transparent)]
|
|
||||||
CalDateTimeError(#[from] CalDateTimeError),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, EnumString, Default, PartialEq)]
|
|
||||||
#[strum(serialize_all = "UPPERCASE")]
|
|
||||||
pub enum RecurrenceFrequency {
|
|
||||||
Secondly,
|
|
||||||
Minutely,
|
|
||||||
Hourly,
|
|
||||||
#[default]
|
|
||||||
Daily,
|
|
||||||
Weekly,
|
|
||||||
Monthly,
|
|
||||||
Yearly,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, EnumString, PartialEq)]
|
|
||||||
#[strum(serialize_all = "UPPERCASE")]
|
|
||||||
pub enum IcalWeekday {
|
|
||||||
Mo,
|
|
||||||
Tu,
|
|
||||||
We,
|
|
||||||
Th,
|
|
||||||
Fr,
|
|
||||||
Sa,
|
|
||||||
Su,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<IcalWeekday> for Weekday {
|
|
||||||
fn from(value: IcalWeekday) -> Self {
|
|
||||||
match value {
|
|
||||||
IcalWeekday::Mo => Self::Mon,
|
|
||||||
IcalWeekday::Tu => Self::Tue,
|
|
||||||
IcalWeekday::We => Self::Wed,
|
|
||||||
IcalWeekday::Th => Self::Thu,
|
|
||||||
IcalWeekday::Fr => Self::Fri,
|
|
||||||
IcalWeekday::Sa => Self::Sat,
|
|
||||||
IcalWeekday::Su => Self::Sun,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum RecurrenceLimit {
|
|
||||||
Count(usize),
|
|
||||||
Until(CalDateTime),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq)]
|
|
||||||
pub struct RecurrenceRule {
|
|
||||||
// Self-explanatory
|
|
||||||
pub frequency: RecurrenceFrequency,
|
|
||||||
pub limit: Option<RecurrenceLimit>,
|
|
||||||
// Repeat every n-th time
|
|
||||||
pub interval: u32,
|
|
||||||
|
|
||||||
pub bysecond: Option<Vec<usize>>,
|
|
||||||
pub byminute: Option<Vec<usize>>,
|
|
||||||
pub byhour: Option<Vec<usize>>,
|
|
||||||
pub byday: Option<Vec<(Option<i64>, Weekday)>>,
|
|
||||||
pub bymonthday: Option<Vec<i8>>,
|
|
||||||
pub byyearday: Option<Vec<i64>>,
|
|
||||||
pub byweekno: Option<Vec<i8>>,
|
|
||||||
pub bymonth0: Option<Vec<u8>>,
|
|
||||||
pub week_start: Option<Weekday>,
|
|
||||||
// Selects the n-th occurence within an a recurrence rule
|
|
||||||
pub bysetpos: Option<Vec<i64>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RecurrenceRule {
|
|
||||||
pub fn parse(rule: &str) -> Result<Self, ParserError> {
|
|
||||||
let mut frequency = None;
|
|
||||||
let mut limit = None;
|
|
||||||
let mut interval = 1;
|
|
||||||
let mut bysecond = None;
|
|
||||||
let mut byminute = None;
|
|
||||||
let mut byhour = None;
|
|
||||||
let mut byday = None;
|
|
||||||
let mut bymonthday = None;
|
|
||||||
let mut byyearday = None;
|
|
||||||
let mut byweekno = None;
|
|
||||||
let mut bymonth0 = None;
|
|
||||||
let mut week_start = None;
|
|
||||||
let mut bysetpos = None;
|
|
||||||
|
|
||||||
for part in rule.split(';') {
|
|
||||||
match part
|
|
||||||
.split_once('=')
|
|
||||||
.ok_or(ParserError::InvalidPart(part.to_owned()))?
|
|
||||||
{
|
|
||||||
("FREQ", val) => {
|
|
||||||
frequency = Some(RecurrenceFrequency::from_str(val)?);
|
|
||||||
}
|
|
||||||
("COUNT", val) => limit = Some(RecurrenceLimit::Count(val.parse()?)),
|
|
||||||
("UNTIL", val) => {
|
|
||||||
limit = Some(RecurrenceLimit::Until(CalDateTime::parse(val, None)?))
|
|
||||||
}
|
|
||||||
("INTERVAL", val) => interval = val.parse()?,
|
|
||||||
("BYSECOND", val) => {
|
|
||||||
bysecond = Some(
|
|
||||||
val.split(',')
|
|
||||||
.map(|val| val.parse())
|
|
||||||
.collect::<Result<Vec<_>, _>>()?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
("BYMINUTE", val) => {
|
|
||||||
byminute = Some(
|
|
||||||
val.split(',')
|
|
||||||
.map(|val| val.parse())
|
|
||||||
.collect::<Result<Vec<_>, _>>()?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
("BYHOUR", val) => {
|
|
||||||
byhour = Some(
|
|
||||||
val.split(',')
|
|
||||||
.map(|val| val.parse())
|
|
||||||
.collect::<Result<Vec<_>, _>>()?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
("BYDAY", val) => {
|
|
||||||
byday = Some(
|
|
||||||
val.split(',')
|
|
||||||
.map(|val| {
|
|
||||||
assert!(val.len() >= 2);
|
|
||||||
let weekday =
|
|
||||||
IcalWeekday::from_str(val.get((val.len() - 2)..).unwrap())?
|
|
||||||
.into();
|
|
||||||
let prefix = if val.len() > 2 {
|
|
||||||
Some(val.get(..(val.len() - 2)).unwrap().parse()?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
Ok((prefix, weekday))
|
|
||||||
})
|
|
||||||
.collect::<Result<Vec<_>, ParserError>>()?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
("BYMONTHDAY", val) => {
|
|
||||||
bymonthday = Some(
|
|
||||||
val.split(',')
|
|
||||||
.map(|val| val.parse())
|
|
||||||
.collect::<Result<Vec<_>, _>>()?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
("BYYEARDAY", val) => {
|
|
||||||
byyearday = Some(
|
|
||||||
val.split(',')
|
|
||||||
.map(|val| val.parse())
|
|
||||||
.collect::<Result<Vec<_>, _>>()?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
("BYWEEKNO", val) => {
|
|
||||||
byweekno = Some(
|
|
||||||
val.split(',')
|
|
||||||
.map(|val| val.parse())
|
|
||||||
.collect::<Result<Vec<_>, _>>()?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
("BYMONTH", val) => {
|
|
||||||
bymonth0 = Some(
|
|
||||||
val.split(',')
|
|
||||||
.map(|val| val.parse())
|
|
||||||
.collect::<Result<Vec<u8>, _>>()?
|
|
||||||
.into_iter()
|
|
||||||
.map(|month| month - 1)
|
|
||||||
.collect(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
("WKST", val) => week_start = Some(IcalWeekday::from_str(val)?.into()),
|
|
||||||
("BYSETPOS", val) => {
|
|
||||||
bysetpos = Some(
|
|
||||||
val.split(',')
|
|
||||||
.map(|val| val.parse())
|
|
||||||
.collect::<Result<Vec<_>, _>>()?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
(name, val) => panic!("Cannot handle {name}={val}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Self {
|
|
||||||
frequency: frequency.ok_or(ParserError::MissingFrequency)?,
|
|
||||||
limit,
|
|
||||||
interval,
|
|
||||||
bysecond,
|
|
||||||
byminute,
|
|
||||||
byhour,
|
|
||||||
byday,
|
|
||||||
bymonthday,
|
|
||||||
byyearday,
|
|
||||||
byweekno,
|
|
||||||
bymonth0,
|
|
||||||
week_start,
|
|
||||||
bysetpos,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RecurrenceRule {
|
|
||||||
fn offset_weekdays(&self) -> Option<Vec<(i64, Weekday)>> {
|
|
||||||
if let Some(byday) = self.byday.as_ref() {
|
|
||||||
Some(
|
|
||||||
byday
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(offset, day)| offset.map(|offset| (offset, day.clone())))
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn absolute_weekdays(&self) -> Option<WeekdaySet> {
|
|
||||||
if let Some(byday) = self.byday.as_ref() {
|
|
||||||
Some(WeekdaySet::from_iter(byday.iter().filter_map(
|
|
||||||
|(offset, day)| {
|
|
||||||
if &None == offset {
|
|
||||||
Some(day.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::{ParserError, RecurrenceRule};
|
|
||||||
use crate::{
|
|
||||||
CalDateTime,
|
|
||||||
rrule::{RecurrenceFrequency, RecurrenceLimit},
|
|
||||||
};
|
|
||||||
use chrono::Weekday;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_recurrence_rule() -> Result<(), ParserError> {
|
|
||||||
assert_eq!(
|
|
||||||
RecurrenceRule::parse("FREQ=DAILY;UNTIL=20250516T133000Z;INTERVAL=3")?,
|
|
||||||
RecurrenceRule {
|
|
||||||
frequency: RecurrenceFrequency::Daily,
|
|
||||||
limit: Some(RecurrenceLimit::Until(
|
|
||||||
CalDateTime::parse("20250516T133000Z", None).unwrap()
|
|
||||||
)),
|
|
||||||
interval: 3,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
RecurrenceRule::parse("FREQ=WEEKLY;COUNT=4;INTERVAL=2;BYDAY=TU,TH,SU")?,
|
|
||||||
RecurrenceRule {
|
|
||||||
frequency: RecurrenceFrequency::Weekly,
|
|
||||||
limit: Some(RecurrenceLimit::Count(4)),
|
|
||||||
interval: 2,
|
|
||||||
byday: Some(vec![
|
|
||||||
(None, Weekday::Tue),
|
|
||||||
(None, Weekday::Thu),
|
|
||||||
(None, Weekday::Sun),
|
|
||||||
]),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
// Example: Last workday of the month
|
|
||||||
assert_eq!(
|
|
||||||
RecurrenceRule::parse("FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1")?,
|
|
||||||
RecurrenceRule {
|
|
||||||
frequency: RecurrenceFrequency::Monthly,
|
|
||||||
byday: Some(vec![
|
|
||||||
(None, Weekday::Mon),
|
|
||||||
(None, Weekday::Tue),
|
|
||||||
(None, Weekday::Wed),
|
|
||||||
(None, Weekday::Thu),
|
|
||||||
(None, Weekday::Fri),
|
|
||||||
]),
|
|
||||||
bysetpos: Some(vec![-1]),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Every last Sunday of March
|
|
||||||
assert_eq!(
|
|
||||||
RecurrenceRule::parse("FREQ=YEARLY;UNTIL=20370329T010000Z;BYDAY=-1SU;BYMONTH=3")?,
|
|
||||||
RecurrenceRule {
|
|
||||||
frequency: RecurrenceFrequency::Yearly,
|
|
||||||
limit: Some(RecurrenceLimit::Until(
|
|
||||||
CalDateTime::parse("20370329T010000Z", None).unwrap()
|
|
||||||
)),
|
|
||||||
byday: Some(vec![(Some(-1), Weekday::Sun)]),
|
|
||||||
bymonth0: Some(vec![3]),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ use ical::{
|
|||||||
};
|
};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use rustical_xml::{ValueDeserialize, ValueSerialize};
|
use rustical_xml::{ValueDeserialize, ValueSerialize};
|
||||||
use std::{collections::HashMap, ops::Add};
|
use std::{borrow::Cow, collections::HashMap, ops::Add};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref RE_VCARD_DATE_MM_DD: regex::Regex =
|
static ref RE_VCARD_DATE_MM_DD: regex::Regex =
|
||||||
@@ -62,14 +62,46 @@ impl ValueSerialize for UtcDateTime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum CalDateTime {
|
pub enum CalDateTime {
|
||||||
// Form 1, example: 19980118T230000 -> Local
|
// Form 1, example: 19980118T230000 -> Local
|
||||||
// Form 2, example: 19980119T070000Z -> UTC
|
// Form 2, example: 19980119T070000Z -> UTC
|
||||||
// Form 3, example: TZID=America/New_York:19980119T020000 -> Olson
|
// Form 3, example: TZID=America/New_York:19980119T020000 -> Olson
|
||||||
// https://en.wikipedia.org/wiki/Tz_database
|
// https://en.wikipedia.org/wiki/Tz_database
|
||||||
DateTime(DateTime<CalTimezone>),
|
DateTime(DateTime<CalTimezone>),
|
||||||
Date(NaiveDate),
|
Date(NaiveDate, CalTimezone),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CalDateTime> for DateTime<rrule::Tz> {
|
||||||
|
fn from(value: CalDateTime) -> Self {
|
||||||
|
value
|
||||||
|
.as_datetime()
|
||||||
|
.into_owned()
|
||||||
|
.with_timezone(&value.timezone().to_owned().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DateTime<rrule::Tz>> for CalDateTime {
|
||||||
|
fn from(value: DateTime<rrule::Tz>) -> Self {
|
||||||
|
Self::DateTime(value.with_timezone(&value.timezone().into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for CalDateTime {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for CalDateTime {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
match (&self, &other) {
|
||||||
|
(Self::DateTime(a), Self::DateTime(b)) => a.cmp(b),
|
||||||
|
(Self::DateTime(a), Self::Date(..)) => a.cmp(&other.as_datetime()),
|
||||||
|
(Self::Date(..), Self::DateTime(b)) => self.as_datetime().as_ref().cmp(b),
|
||||||
|
(Self::Date(..), Self::Date(..)) => self.as_datetime().cmp(&other.as_datetime()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DateTime<Local>> for CalDateTime {
|
impl From<DateTime<Local>> for CalDateTime {
|
||||||
@@ -90,9 +122,9 @@ impl Add<Duration> for CalDateTime {
|
|||||||
fn add(self, duration: Duration) -> Self::Output {
|
fn add(self, duration: Duration) -> Self::Output {
|
||||||
match self {
|
match self {
|
||||||
Self::DateTime(datetime) => Self::DateTime(datetime + duration),
|
Self::DateTime(datetime) => Self::DateTime(datetime + duration),
|
||||||
Self::Date(date) => Self::DateTime(
|
Self::Date(date, tz) => Self::DateTime(
|
||||||
date.and_time(NaiveTime::default())
|
date.and_time(NaiveTime::default())
|
||||||
.and_local_timezone(CalTimezone::Local)
|
.and_local_timezone(tz)
|
||||||
.earliest()
|
.earliest()
|
||||||
.expect("Local timezone has constant offset")
|
.expect("Local timezone has constant offset")
|
||||||
+ duration,
|
+ duration,
|
||||||
@@ -105,12 +137,13 @@ impl CalDateTime {
|
|||||||
pub fn parse_prop(
|
pub fn parse_prop(
|
||||||
prop: &Property,
|
prop: &Property,
|
||||||
timezones: &HashMap<String, IcalTimeZone>,
|
timezones: &HashMap<String, IcalTimeZone>,
|
||||||
) -> Result<Option<Self>, CalDateTimeError> {
|
) -> Result<Self, CalDateTimeError> {
|
||||||
let prop_value = if let Some(value) = prop.value.as_ref() {
|
let prop_value = prop
|
||||||
value
|
.value
|
||||||
} else {
|
.as_ref()
|
||||||
return Ok(None);
|
.ok_or(CalDateTimeError::InvalidDatetimeFormat(
|
||||||
};
|
"empty property".to_owned(),
|
||||||
|
))?;
|
||||||
|
|
||||||
// Use the TZID parameter from the property
|
// Use the TZID parameter from the property
|
||||||
let timezone = if let Some(tzid) = prop.get_tzid() {
|
let timezone = if let Some(tzid) = prop.get_tzid() {
|
||||||
@@ -144,7 +177,7 @@ impl CalDateTime {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
Self::parse(prop_value, timezone).map(Some)
|
Self::parse(prop_value, timezone)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format(&self) -> String {
|
pub fn format(&self) -> String {
|
||||||
@@ -153,25 +186,26 @@ impl CalDateTime {
|
|||||||
CalTimezone::Utc => datetime.format(UTC_DATE_TIME).to_string(),
|
CalTimezone::Utc => datetime.format(UTC_DATE_TIME).to_string(),
|
||||||
_ => datetime.format(LOCAL_DATE_TIME).to_string(),
|
_ => datetime.format(LOCAL_DATE_TIME).to_string(),
|
||||||
},
|
},
|
||||||
Self::Date(date) => date.format(LOCAL_DATE).to_string(),
|
Self::Date(date, _) => date.format(LOCAL_DATE).to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn date(&self) -> NaiveDate {
|
pub fn date(&self) -> NaiveDate {
|
||||||
match self {
|
match self {
|
||||||
Self::DateTime(datetime) => datetime.date_naive(),
|
Self::DateTime(datetime) => datetime.date_naive(),
|
||||||
Self::Date(date) => date.to_owned(),
|
Self::Date(date, _) => date.to_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_datetime(self) -> DateTime<CalTimezone> {
|
pub fn as_datetime(&self) -> Cow<DateTime<CalTimezone>> {
|
||||||
match self {
|
match self {
|
||||||
Self::DateTime(datetime) => datetime,
|
Self::DateTime(datetime) => Cow::Borrowed(datetime),
|
||||||
Self::Date(date) => date
|
Self::Date(date, tz) => Cow::Owned(
|
||||||
.and_time(NaiveTime::default())
|
date.and_time(NaiveTime::default())
|
||||||
.and_local_timezone(CalTimezone::Local)
|
.and_local_timezone(tz.to_owned())
|
||||||
.earliest()
|
.earliest()
|
||||||
.expect("Midnight always exists"),
|
.expect("Midnight always exists"),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,15 +230,18 @@ impl CalDateTime {
|
|||||||
if let Ok(datetime) = NaiveDateTime::parse_from_str(value, UTC_DATE_TIME) {
|
if let Ok(datetime) = NaiveDateTime::parse_from_str(value, UTC_DATE_TIME) {
|
||||||
return Ok(datetime.and_utc().into());
|
return Ok(datetime.and_utc().into());
|
||||||
}
|
}
|
||||||
|
let timezone = timezone
|
||||||
|
.map(CalTimezone::Olson)
|
||||||
|
.unwrap_or(CalTimezone::Local);
|
||||||
if let Ok(date) = NaiveDate::parse_from_str(value, LOCAL_DATE) {
|
if let Ok(date) = NaiveDate::parse_from_str(value, LOCAL_DATE) {
|
||||||
return Ok(CalDateTime::Date(date));
|
return Ok(CalDateTime::Date(date, timezone));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
|
if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
|
||||||
return Ok(CalDateTime::Date(date));
|
return Ok(CalDateTime::Date(date, timezone));
|
||||||
}
|
}
|
||||||
if let Ok(date) = NaiveDate::parse_from_str(value, "%Y%m%d") {
|
if let Ok(date) = NaiveDate::parse_from_str(value, "%Y%m%d") {
|
||||||
return Ok(CalDateTime::Date(date));
|
return Ok(CalDateTime::Date(date, timezone));
|
||||||
}
|
}
|
||||||
if let Some(captures) = RE_VCARD_DATE_MM_DD.captures(value) {
|
if let Some(captures) = RE_VCARD_DATE_MM_DD.captures(value) {
|
||||||
// Because 1972 is a leap year
|
// Because 1972 is a leap year
|
||||||
@@ -216,6 +253,7 @@ impl CalDateTime {
|
|||||||
return Ok(CalDateTime::Date(
|
return Ok(CalDateTime::Date(
|
||||||
NaiveDate::from_ymd_opt(year, month, day)
|
NaiveDate::from_ymd_opt(year, month, day)
|
||||||
.ok_or(CalDateTimeError::ParseError(value.to_string()))?,
|
.ok_or(CalDateTimeError::ParseError(value.to_string()))?,
|
||||||
|
timezone,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,16 +261,13 @@ impl CalDateTime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn utc(&self) -> DateTime<Utc> {
|
pub fn utc(&self) -> DateTime<Utc> {
|
||||||
match &self {
|
self.as_datetime().to_utc()
|
||||||
CalDateTime::DateTime(datetime) => datetime.to_utc(),
|
|
||||||
CalDateTime::Date(date) => date.and_time(NaiveTime::default()).and_utc(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn timezone(&self) -> Option<CalTimezone> {
|
pub fn timezone(&self) -> CalTimezone {
|
||||||
match &self {
|
match &self {
|
||||||
CalDateTime::DateTime(datetime) => Some(datetime.timezone()),
|
CalDateTime::DateTime(datetime) => datetime.timezone(),
|
||||||
CalDateTime::Date(_) => None,
|
CalDateTime::Date(_, tz) => tz.to_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,86 +282,88 @@ impl Datelike for CalDateTime {
|
|||||||
fn year(&self) -> i32 {
|
fn year(&self) -> i32 {
|
||||||
match &self {
|
match &self {
|
||||||
CalDateTime::DateTime(datetime) => datetime.year(),
|
CalDateTime::DateTime(datetime) => datetime.year(),
|
||||||
CalDateTime::Date(date) => date.year(),
|
CalDateTime::Date(date, _) => date.year(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn month(&self) -> u32 {
|
fn month(&self) -> u32 {
|
||||||
match &self {
|
match &self {
|
||||||
CalDateTime::DateTime(datetime) => datetime.month(),
|
CalDateTime::DateTime(datetime) => datetime.month(),
|
||||||
CalDateTime::Date(date) => date.month(),
|
CalDateTime::Date(date, _) => date.month(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn month0(&self) -> u32 {
|
fn month0(&self) -> u32 {
|
||||||
match &self {
|
match &self {
|
||||||
CalDateTime::DateTime(datetime) => datetime.month0(),
|
CalDateTime::DateTime(datetime) => datetime.month0(),
|
||||||
CalDateTime::Date(date) => date.month0(),
|
CalDateTime::Date(date, _) => date.month0(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn day(&self) -> u32 {
|
fn day(&self) -> u32 {
|
||||||
match &self {
|
match &self {
|
||||||
CalDateTime::DateTime(datetime) => datetime.day(),
|
CalDateTime::DateTime(datetime) => datetime.day(),
|
||||||
CalDateTime::Date(date) => date.day(),
|
CalDateTime::Date(date, _) => date.day(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn day0(&self) -> u32 {
|
fn day0(&self) -> u32 {
|
||||||
match &self {
|
match &self {
|
||||||
CalDateTime::DateTime(datetime) => datetime.day0(),
|
CalDateTime::DateTime(datetime) => datetime.day0(),
|
||||||
CalDateTime::Date(date) => date.day0(),
|
CalDateTime::Date(date, _) => date.day0(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn ordinal(&self) -> u32 {
|
fn ordinal(&self) -> u32 {
|
||||||
match &self {
|
match &self {
|
||||||
CalDateTime::DateTime(datetime) => datetime.ordinal(),
|
CalDateTime::DateTime(datetime) => datetime.ordinal(),
|
||||||
CalDateTime::Date(date) => date.ordinal(),
|
CalDateTime::Date(date, _) => date.ordinal(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn ordinal0(&self) -> u32 {
|
fn ordinal0(&self) -> u32 {
|
||||||
match &self {
|
match &self {
|
||||||
CalDateTime::DateTime(datetime) => datetime.ordinal0(),
|
CalDateTime::DateTime(datetime) => datetime.ordinal0(),
|
||||||
CalDateTime::Date(date) => date.ordinal0(),
|
CalDateTime::Date(date, _) => date.ordinal0(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn weekday(&self) -> chrono::Weekday {
|
fn weekday(&self) -> chrono::Weekday {
|
||||||
match &self {
|
match &self {
|
||||||
CalDateTime::DateTime(datetime) => datetime.weekday(),
|
CalDateTime::DateTime(datetime) => datetime.weekday(),
|
||||||
CalDateTime::Date(date) => date.weekday(),
|
CalDateTime::Date(date, _) => date.weekday(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn iso_week(&self) -> chrono::IsoWeek {
|
fn iso_week(&self) -> chrono::IsoWeek {
|
||||||
match &self {
|
match &self {
|
||||||
CalDateTime::DateTime(datetime) => datetime.iso_week(),
|
CalDateTime::DateTime(datetime) => datetime.iso_week(),
|
||||||
CalDateTime::Date(date) => date.iso_week(),
|
CalDateTime::Date(date, _) => date.iso_week(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn with_year(&self, year: i32) -> Option<Self> {
|
fn with_year(&self, year: i32) -> Option<Self> {
|
||||||
match &self {
|
match &self {
|
||||||
CalDateTime::DateTime(datetime) => Some(Self::DateTime(datetime.with_year(year)?)),
|
CalDateTime::DateTime(datetime) => Some(Self::DateTime(datetime.with_year(year)?)),
|
||||||
CalDateTime::Date(date) => Some(Self::Date(date.with_year(year)?)),
|
CalDateTime::Date(date, tz) => Some(Self::Date(date.with_year(year)?, tz.to_owned())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn with_month(&self, month: u32) -> Option<Self> {
|
fn with_month(&self, month: u32) -> Option<Self> {
|
||||||
match &self {
|
match &self {
|
||||||
CalDateTime::DateTime(datetime) => Some(Self::DateTime(datetime.with_month(month)?)),
|
CalDateTime::DateTime(datetime) => Some(Self::DateTime(datetime.with_month(month)?)),
|
||||||
CalDateTime::Date(date) => Some(Self::Date(date.with_month(month)?)),
|
CalDateTime::Date(date, tz) => Some(Self::Date(date.with_month(month)?, tz.to_owned())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn with_month0(&self, month0: u32) -> Option<Self> {
|
fn with_month0(&self, month0: u32) -> Option<Self> {
|
||||||
match &self {
|
match &self {
|
||||||
CalDateTime::DateTime(datetime) => Some(Self::DateTime(datetime.with_month0(month0)?)),
|
CalDateTime::DateTime(datetime) => Some(Self::DateTime(datetime.with_month0(month0)?)),
|
||||||
CalDateTime::Date(date) => Some(Self::Date(date.with_month0(month0)?)),
|
CalDateTime::Date(date, tz) => {
|
||||||
|
Some(Self::Date(date.with_month0(month0)?, tz.to_owned()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn with_day(&self, day: u32) -> Option<Self> {
|
fn with_day(&self, day: u32) -> Option<Self> {
|
||||||
match &self {
|
match &self {
|
||||||
CalDateTime::DateTime(datetime) => Some(Self::DateTime(datetime.with_day(day)?)),
|
CalDateTime::DateTime(datetime) => Some(Self::DateTime(datetime.with_day(day)?)),
|
||||||
CalDateTime::Date(date) => Some(Self::Date(date.with_day(day)?)),
|
CalDateTime::Date(date, tz) => Some(Self::Date(date.with_day(day)?, tz.to_owned())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn with_day0(&self, day0: u32) -> Option<Self> {
|
fn with_day0(&self, day0: u32) -> Option<Self> {
|
||||||
match &self {
|
match &self {
|
||||||
CalDateTime::DateTime(datetime) => Some(Self::DateTime(datetime.with_day0(day0)?)),
|
CalDateTime::DateTime(datetime) => Some(Self::DateTime(datetime.with_day0(day0)?)),
|
||||||
CalDateTime::Date(date) => Some(Self::Date(date.with_day0(day0)?)),
|
CalDateTime::Date(date, tz) => Some(Self::Date(date.with_day0(day0)?, tz.to_owned())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn with_ordinal(&self, ordinal: u32) -> Option<Self> {
|
fn with_ordinal(&self, ordinal: u32) -> Option<Self> {
|
||||||
@@ -334,7 +371,9 @@ impl Datelike for CalDateTime {
|
|||||||
CalDateTime::DateTime(datetime) => {
|
CalDateTime::DateTime(datetime) => {
|
||||||
Some(Self::DateTime(datetime.with_ordinal(ordinal)?))
|
Some(Self::DateTime(datetime.with_ordinal(ordinal)?))
|
||||||
}
|
}
|
||||||
CalDateTime::Date(date) => Some(Self::Date(date.with_ordinal(ordinal)?)),
|
CalDateTime::Date(date, tz) => {
|
||||||
|
Some(Self::Date(date.with_ordinal(ordinal)?, tz.to_owned()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn with_ordinal0(&self, ordinal0: u32) -> Option<Self> {
|
fn with_ordinal0(&self, ordinal0: u32) -> Option<Self> {
|
||||||
@@ -342,7 +381,9 @@ impl Datelike for CalDateTime {
|
|||||||
CalDateTime::DateTime(datetime) => {
|
CalDateTime::DateTime(datetime) => {
|
||||||
Some(Self::DateTime(datetime.with_ordinal0(ordinal0)?))
|
Some(Self::DateTime(datetime.with_ordinal0(ordinal0)?))
|
||||||
}
|
}
|
||||||
CalDateTime::Date(date) => Some(Self::Date(date.with_ordinal0(ordinal0)?)),
|
CalDateTime::Date(date, tz) => {
|
||||||
|
Some(Self::Date(date.with_ordinal0(ordinal0)?, tz.to_owned()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,15 +425,24 @@ mod tests {
|
|||||||
fn test_vcard_date() {
|
fn test_vcard_date() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
CalDateTime::parse("19850412", None).unwrap(),
|
CalDateTime::parse("19850412", None).unwrap(),
|
||||||
CalDateTime::Date(NaiveDate::from_ymd_opt(1985, 4, 12).unwrap())
|
CalDateTime::Date(
|
||||||
|
NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(),
|
||||||
|
crate::CalTimezone::Local
|
||||||
|
)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
CalDateTime::parse("1985-04-12", None).unwrap(),
|
CalDateTime::parse("1985-04-12", None).unwrap(),
|
||||||
CalDateTime::Date(NaiveDate::from_ymd_opt(1985, 4, 12).unwrap())
|
CalDateTime::Date(
|
||||||
|
NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(),
|
||||||
|
crate::CalTimezone::Local
|
||||||
|
)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
CalDateTime::parse("--0412", None).unwrap(),
|
CalDateTime::parse("--0412", None).unwrap(),
|
||||||
CalDateTime::Date(NaiveDate::from_ymd_opt(1972, 4, 12).unwrap())
|
CalDateTime::Date(
|
||||||
|
NaiveDate::from_ymd_opt(1972, 4, 12).unwrap(),
|
||||||
|
crate::CalTimezone::Local
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,33 @@ use chrono::{Local, NaiveDate, NaiveDateTime, TimeZone, Utc};
|
|||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use derive_more::{Display, From};
|
use derive_more::{Display, From};
|
||||||
|
|
||||||
#[derive(Debug, Clone, From)]
|
#[derive(Debug, Clone, From, PartialEq, Eq)]
|
||||||
pub enum CalTimezone {
|
pub enum CalTimezone {
|
||||||
Local,
|
Local,
|
||||||
Utc,
|
Utc,
|
||||||
Olson(Tz),
|
Olson(Tz),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<CalTimezone> for rrule::Tz {
|
||||||
|
fn from(value: CalTimezone) -> Self {
|
||||||
|
match value {
|
||||||
|
CalTimezone::Local => Self::LOCAL,
|
||||||
|
CalTimezone::Utc => Self::UTC,
|
||||||
|
CalTimezone::Olson(tz) => Self::Tz(tz),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<rrule::Tz> for CalTimezone {
|
||||||
|
fn from(value: rrule::Tz) -> Self {
|
||||||
|
match value {
|
||||||
|
rrule::Tz::Local(_) => Self::Local,
|
||||||
|
rrule::Tz::Tz(chrono_tz::UTC) => Self::Utc,
|
||||||
|
rrule::Tz::Tz(tz) => Self::Olson(tz),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Display)]
|
#[derive(Debug, Clone, PartialEq, Display)]
|
||||||
pub enum CalTimezoneOffset {
|
pub enum CalTimezoneOffset {
|
||||||
Local(chrono::FixedOffset),
|
Local(chrono::FixedOffset),
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ rustical_dav.workspace = true
|
|||||||
strum.workspace = true
|
strum.workspace = true
|
||||||
strum_macros.workspace = true
|
strum_macros.workspace = true
|
||||||
rustical_ical.workspace = true
|
rustical_ical.workspace = true
|
||||||
|
rrule = "0.14"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rstest = { workspace = true }
|
rstest = { workspace = true }
|
||||||
|
|||||||
@@ -48,12 +48,12 @@ impl AddressObject {
|
|||||||
|
|
||||||
pub fn get_anniversary(&self) -> Option<CalDateTime> {
|
pub fn get_anniversary(&self) -> Option<CalDateTime> {
|
||||||
let prop = self.vcard.get_property("ANNIVERSARY")?;
|
let prop = self.vcard.get_property("ANNIVERSARY")?;
|
||||||
CalDateTime::parse_prop(prop, &HashMap::default()).unwrap_or(None)
|
CalDateTime::parse_prop(prop, &HashMap::default()).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_birthday(&self) -> Option<CalDateTime> {
|
pub fn get_birthday(&self) -> Option<CalDateTime> {
|
||||||
let prop = self.vcard.get_property("BDAY")?;
|
let prop = self.vcard.get_property("BDAY")?;
|
||||||
CalDateTime::parse_prop(prop, &HashMap::default()).unwrap_or(None)
|
CalDateTime::parse_prop(prop, &HashMap::default()).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_full_name(&self) -> Option<&String> {
|
pub fn get_full_name(&self) -> Option<&String> {
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
use crate::Error;
|
use crate::Error;
|
||||||
use chrono::Duration;
|
use chrono::{DateTime, Duration};
|
||||||
use ical::{
|
use ical::{
|
||||||
generator::IcalEvent,
|
generator::IcalEvent,
|
||||||
parser::{Component, ical::component::IcalTimeZone},
|
parser::{Component, ical::component::IcalTimeZone},
|
||||||
property::Property,
|
property::Property,
|
||||||
};
|
};
|
||||||
use rustical_ical::{
|
use rrule::{RRule, RRuleSet};
|
||||||
CalDateTime, ComponentMut, parse_duration,
|
use rustical_ical::{CalDateTime, ComponentMut, parse_duration};
|
||||||
rrule::{ParserError, RecurrenceRule},
|
use std::{collections::HashMap, str::FromStr};
|
||||||
};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct EventObject {
|
pub struct EventObject {
|
||||||
@@ -21,7 +19,7 @@ pub struct EventObject {
|
|||||||
impl EventObject {
|
impl EventObject {
|
||||||
pub fn get_first_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
pub fn get_first_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
||||||
if let Some(dtstart) = self.event.get_property("DTSTART") {
|
if let Some(dtstart) = self.event.get_property("DTSTART") {
|
||||||
Ok(CalDateTime::parse_prop(dtstart, &self.timezones)?)
|
Ok(Some(CalDateTime::parse_prop(dtstart, &self.timezones)?))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
@@ -34,7 +32,7 @@ impl EventObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(dtend) = self.event.get_property("DTEND") {
|
if let Some(dtend) = self.event.get_property("DTEND") {
|
||||||
return Ok(CalDateTime::parse_prop(dtend, &self.timezones)?);
|
return Ok(Some(CalDateTime::parse_prop(dtend, &self.timezones)?));
|
||||||
};
|
};
|
||||||
|
|
||||||
let duration = self.get_duration()?.unwrap_or(Duration::days(1));
|
let duration = self.get_duration()?.unwrap_or(Duration::days(1));
|
||||||
@@ -55,57 +53,81 @@ impl EventObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn recurrence_rule(&self) -> Result<Option<RecurrenceRule>, ParserError> {
|
pub fn recurrence_ruleset(&self) -> Result<Option<rrule::RRuleSet>, Error> {
|
||||||
let rrule = if let Some(&Property {
|
let dtstart: DateTime<rrule::Tz> = if let Some(dtstart) = self.get_first_occurence()? {
|
||||||
value: Some(rrule), ..
|
dtstart
|
||||||
}) = self.event.get_property("RRULE").as_ref()
|
.as_datetime()
|
||||||
{
|
.with_timezone(&dtstart.timezone().into())
|
||||||
rrule
|
|
||||||
} else {
|
} else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
RecurrenceRule::parse(rrule).map(Some)
|
|
||||||
|
let mut rrule_set = RRuleSet::new(dtstart);
|
||||||
|
|
||||||
|
for prop in &self.event.properties {
|
||||||
|
rrule_set = match prop.name.as_str() {
|
||||||
|
"RRULE" => {
|
||||||
|
let rrule = RRule::from_str(prop.value.as_ref().ok_or(Error::RRuleError(
|
||||||
|
rrule::ParseError::MissingDateGenerationRules.into(),
|
||||||
|
))?)?
|
||||||
|
.validate(dtstart)
|
||||||
|
.unwrap();
|
||||||
|
rrule_set.rrule(rrule)
|
||||||
|
}
|
||||||
|
"RDATE" => {
|
||||||
|
let rdate = CalDateTime::parse_prop(prop, &self.timezones)?.into();
|
||||||
|
rrule_set.rdate(rdate)
|
||||||
|
}
|
||||||
|
"EXDATE" => {
|
||||||
|
let exdate = CalDateTime::parse_prop(prop, &self.timezones)?.into();
|
||||||
|
rrule_set.exdate(exdate)
|
||||||
|
}
|
||||||
|
_ => rrule_set,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(rrule_set))
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub fn expand_recurrence(&self) -> Result<Vec<IcalEvent>, Error> {
|
pub fn expand_recurrence(&self) -> Result<Vec<IcalEvent>, Error> {
|
||||||
// if let Some(rrule) = self.recurrence_rule()? {
|
if let Some(rrule_set) = self.recurrence_ruleset()? {
|
||||||
// let mut events = vec![];
|
let mut events = vec![];
|
||||||
// let first_occurence = self.get_first_occurence()?.unwrap();
|
let dates = rrule_set.all(2048).dates;
|
||||||
// let dates = rrule.between(first_occurence, None, None);
|
|
||||||
//
|
for date in dates {
|
||||||
// for date in dates {
|
let date = CalDateTime::from(date);
|
||||||
// let dtstart_utc = date;
|
let mut ev = self.event.clone();
|
||||||
// let mut ev = self.event.clone();
|
ev.remove_property("RRULE");
|
||||||
// ev.remove_property("RRULE");
|
ev.set_property(Property {
|
||||||
// ev.set_property(Property {
|
name: "RECURRENCE-ID".to_string(),
|
||||||
// name: "RECURRENCE-ID".to_string(),
|
value: Some(date.format()),
|
||||||
// value: Some(dtstart_utc.format()),
|
params: None,
|
||||||
// params: None,
|
});
|
||||||
// });
|
ev.set_property(Property {
|
||||||
// ev.set_property(Property {
|
name: "DTSTART".to_string(),
|
||||||
// name: "DTSTART".to_string(),
|
value: Some(date.format()),
|
||||||
// value: Some(dtstart_utc.format()),
|
params: None,
|
||||||
// params: None,
|
});
|
||||||
// });
|
if let Some(duration) = self.get_duration()? {
|
||||||
// if let Some(duration) = self.get_duration()? {
|
ev.set_property(Property {
|
||||||
// ev.set_property(Property {
|
name: "DTEND".to_string(),
|
||||||
// name: "DTEND".to_string(),
|
value: Some((date + duration).format()),
|
||||||
// value: Some((dtstart_utc + duration).format()),
|
params: None,
|
||||||
// params: None,
|
});
|
||||||
// });
|
}
|
||||||
// }
|
events.push(ev);
|
||||||
// events.push(ev);
|
}
|
||||||
// }
|
Ok(events)
|
||||||
// Ok(events)
|
} else {
|
||||||
// } else {
|
Ok(vec![self.event.clone()])
|
||||||
// Ok(vec![self.event.clone()])
|
}
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::CalendarObject;
|
use crate::CalendarObject;
|
||||||
|
use ical::generator::Emitter;
|
||||||
|
|
||||||
const ICS: &str = r#"BEGIN:VCALENDAR
|
const ICS: &str = r#"BEGIN:VCALENDAR
|
||||||
CALSCALE:GREGORIAN
|
CALSCALE:GREGORIAN
|
||||||
@@ -127,6 +149,49 @@ RRULE:FREQ=WEEKLY;COUNT=4;INTERVAL=2;BYDAY=TU,TH,SU
|
|||||||
END:VEVENT
|
END:VEVENT
|
||||||
END:VCALENDAR"#;
|
END:VCALENDAR"#;
|
||||||
|
|
||||||
|
const EXPANDED: [&str; 4] = [
|
||||||
|
"BEGIN:VEVENT\r
|
||||||
|
UID:318ec6503573d9576818daf93dac07317058d95c\r
|
||||||
|
DTSTAMP:20250502T132758Z\r
|
||||||
|
DTEND;TZID=Europe/Berlin:20250506T092500\r
|
||||||
|
SEQUENCE:2\r
|
||||||
|
SUMMARY:weekly stuff\r
|
||||||
|
TRANSP:OPAQUE\r
|
||||||
|
RECURRENCE-ID:20250506T090000\r
|
||||||
|
DTSTART:20250506T090000\r
|
||||||
|
END:VEVENT\r\n",
|
||||||
|
"BEGIN:VEVENT\r
|
||||||
|
UID:318ec6503573d9576818daf93dac07317058d95c\r
|
||||||
|
DTSTAMP:20250502T132758Z\r
|
||||||
|
DTEND;TZID=Europe/Berlin:20250506T092500\r
|
||||||
|
SEQUENCE:2\r
|
||||||
|
SUMMARY:weekly stuff\r
|
||||||
|
TRANSP:OPAQUE\r
|
||||||
|
RECURRENCE-ID:20250508T090000\r
|
||||||
|
DTSTART:20250508T090000\r
|
||||||
|
END:VEVENT\r\n",
|
||||||
|
"BEGIN:VEVENT\r
|
||||||
|
UID:318ec6503573d9576818daf93dac07317058d95c\r
|
||||||
|
DTSTAMP:20250502T132758Z\r
|
||||||
|
DTEND;TZID=Europe/Berlin:20250506T092500\r
|
||||||
|
SEQUENCE:2\r
|
||||||
|
SUMMARY:weekly stuff\r
|
||||||
|
TRANSP:OPAQUE\r
|
||||||
|
RECURRENCE-ID:20250511T090000\r
|
||||||
|
DTSTART:20250511T090000\r
|
||||||
|
END:VEVENT\r\n",
|
||||||
|
"BEGIN:VEVENT\r
|
||||||
|
UID:318ec6503573d9576818daf93dac07317058d95c\r
|
||||||
|
DTSTAMP:20250502T132758Z\r
|
||||||
|
DTEND;TZID=Europe/Berlin:20250506T092500\r
|
||||||
|
SEQUENCE:2\r
|
||||||
|
SUMMARY:weekly stuff\r
|
||||||
|
TRANSP:OPAQUE\r
|
||||||
|
RECURRENCE-ID:20250520T090000\r
|
||||||
|
DTSTART:20250520T090000\r
|
||||||
|
END:VEVENT\r\n",
|
||||||
|
];
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_expand_recurrence() {
|
fn test_expand_recurrence() {
|
||||||
let event = CalendarObject::from_ics(
|
let event = CalendarObject::from_ics(
|
||||||
@@ -134,6 +199,14 @@ END:VCALENDAR"#;
|
|||||||
ICS.to_string(),
|
ICS.to_string(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(event.expand_recurrence().unwrap(), "asd".to_string());
|
let event = event.event().unwrap();
|
||||||
|
|
||||||
|
let events: Vec<String> = event
|
||||||
|
.expand_recurrence()
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.map(|event| Emitter::generate(&event))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(events.as_slice(), &EXPANDED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,14 +181,21 @@ impl CalendarObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn event(&self) -> Option<&EventObject> {
|
||||||
|
match &self.data {
|
||||||
|
CalendarObjectComponent::Event(event) => Some(event),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn expand_recurrence(&self) -> Result<String, Error> {
|
pub fn expand_recurrence(&self) -> Result<String, Error> {
|
||||||
// Only events can be expanded
|
// Only events can be expanded
|
||||||
match &self.data {
|
match &self.data {
|
||||||
// CalendarObjectComponent::Event(event) => {
|
CalendarObjectComponent::Event(event) => {
|
||||||
// let mut cal = self.cal.clone();
|
let mut cal = self.cal.clone();
|
||||||
// cal.events = event.expand_recurrence()?;
|
cal.events = event.expand_recurrence()?;
|
||||||
// Ok(cal.generate())
|
Ok(cal.generate())
|
||||||
// }
|
}
|
||||||
_ => Ok(self.get_ics().to_string()),
|
_ => Ok(self.get_ics().to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ pub enum Error {
|
|||||||
#[error("Invalid ics/vcf input: {0}")]
|
#[error("Invalid ics/vcf input: {0}")]
|
||||||
InvalidData(String),
|
InvalidData(String),
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
RRuleParserError(#[from] rustical_ical::rrule::ParserError),
|
|
||||||
|
|
||||||
#[error("Read-only")]
|
#[error("Read-only")]
|
||||||
ReadOnly,
|
ReadOnly,
|
||||||
|
|
||||||
@@ -32,6 +29,9 @@ pub enum Error {
|
|||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
CalDateTimeError(#[from] CalDateTimeError),
|
CalDateTimeError(#[from] CalDateTimeError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
RRuleError(#[from] rrule::RRuleError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseError for Error {
|
impl ResponseError for Error {
|
||||||
|
|||||||
Reference in New Issue
Block a user