use std::collections::HashMap;
use chrono::{DateTime, Duration, Utc};
use rrule::RRuleSet;
use crate::error::{CalDirError, CalDirResult};
use crate::event::{Event, EventTime, Recurrence};
fn normalize_rrule_until(rrule: &str, start: &EventTime) -> String {
rrule
.split(';')
.map(|part| {
if !part.starts_with("UNTIL=") {
return part.to_string();
}
let value = &part[6..];
match start {
EventTime::DateTimeUtc(_) => {
if !value.ends_with('Z') {
if value.contains('T') {
format!("UNTIL={}Z", value)
} else {
format!("UNTIL={}T235959Z", value)
}
} else {
part.to_string()
}
}
EventTime::DateTimeZoned { .. } => {
if !value.ends_with('Z') {
if value.contains('T') {
format!("UNTIL={}Z", value)
} else {
format!("UNTIL={}T235959Z", value)
}
} else {
part.to_string()
}
}
_ => {
if value.ends_with('Z') {
format!("UNTIL={}", value.trim_end_matches('Z'))
} else {
part.to_string()
}
}
}
})
.collect::<Vec<_>>()
.join(";")
}
fn build_rrule_string(start: &EventTime, recurrence: &Recurrence) -> String {
let mut lines = Vec::new();
let dtstart = match start {
EventTime::Date(d) => {
format!("DTSTART:{}T000000", d.format("%Y%m%d"))
}
EventTime::DateTimeUtc(dt) => {
format!("DTSTART:{}", dt.format("%Y%m%dT%H%M%SZ"))
}
EventTime::DateTimeFloating(dt) => {
format!("DTSTART:{}", dt.format("%Y%m%dT%H%M%S"))
}
EventTime::DateTimeZoned { datetime, tzid } => {
format!("DTSTART;TZID={}:{}", tzid, datetime.format("%Y%m%dT%H%M%S"))
}
};
lines.push(dtstart);
let rrule = normalize_rrule_until(&recurrence.rrule, start);
lines.push(format!("RRULE:{}", rrule));
for exdate in &recurrence.exdates {
let exdate_str = match exdate {
EventTime::Date(d) => format!("EXDATE:{}T000000", d.format("%Y%m%d")),
EventTime::DateTimeUtc(dt) => {
format!("EXDATE:{}", dt.format("%Y%m%dT%H%M%SZ"))
}
EventTime::DateTimeFloating(dt) => {
format!("EXDATE:{}", dt.format("%Y%m%dT%H%M%S"))
}
EventTime::DateTimeZoned { datetime, tzid } => {
format!("EXDATE;TZID={}:{}", tzid, datetime.format("%Y%m%dT%H%M%S"))
}
};
lines.push(exdate_str);
}
lines.join("\n")
}
fn occurrence_to_event_time(dt: &DateTime<rrule::Tz>, master_start: &EventTime) -> EventTime {
match master_start {
EventTime::Date(_) => EventTime::Date(dt.date_naive()),
EventTime::DateTimeUtc(_) => EventTime::DateTimeUtc(dt.with_timezone(&Utc)),
EventTime::DateTimeFloating(_) => EventTime::DateTimeFloating(dt.naive_utc()),
EventTime::DateTimeZoned { tzid, .. } => EventTime::DateTimeZoned {
datetime: dt.naive_local(),
tzid: tzid.clone(),
},
}
}
pub fn expand_recurring_event(
master: &Event,
range_start: DateTime<Utc>,
range_end: DateTime<Utc>,
overrides: &HashMap<String, Event>,
) -> CalDirResult<Vec<Event>> {
let recurrence = match &master.recurrence {
Some(r) => r,
None => return Ok(Vec::new()),
};
let rrule_str = build_rrule_string(&master.start, recurrence);
let rrule_set: RRuleSet = rrule_str.parse().map_err(|e| {
CalDirError::IcsParse(format!(
"Failed to parse RRULE for event '{}': {}",
master.uid, e
))
})?;
let tz: rrule::Tz = Utc.into();
let after = range_start.with_timezone(&tz);
let before = range_end.with_timezone(&tz);
let result = rrule_set.after(after).before(before).all(365);
let duration = match (master.start.to_utc(), master.end.to_utc()) {
(Some(s), Some(e)) => e - s,
_ => Duration::zero(),
};
let mut events = Vec::new();
for occ_dt in &result.dates {
let occ_event_time = occurrence_to_event_time(occ_dt, &master.start);
let ics_key = occ_event_time.to_ics_string();
if let Some(override_event) = overrides.get(&ics_key) {
events.push(override_event.clone());
} else {
let instance_end = match (&master.start, &master.end) {
(EventTime::Date(d_start), EventTime::Date(d_end)) => {
let day_diff = (*d_end - *d_start).num_days();
EventTime::Date(occ_dt.date_naive() + Duration::days(day_diff))
}
(EventTime::DateTimeUtc(_), _) => {
EventTime::DateTimeUtc(occ_dt.with_timezone(&Utc) + duration)
}
(EventTime::DateTimeFloating(_), _) => {
EventTime::DateTimeFloating(occ_dt.naive_utc() + duration)
}
(EventTime::DateTimeZoned { tzid, .. }, _) => EventTime::DateTimeZoned {
datetime: occ_dt.naive_local() + duration,
tzid: tzid.clone(),
},
_ => EventTime::DateTimeUtc(occ_dt.with_timezone(&Utc) + duration),
};
events.push(Event {
uid: master.uid.clone(),
summary: master.summary.clone(),
description: master.description.clone(),
location: master.location.clone(),
start: occ_event_time.clone(),
end: instance_end,
status: master.status.clone(),
recurrence: None,
recurrence_id: Some(occ_event_time),
reminders: master.reminders.clone(),
transparency: master.transparency.clone(),
organizer: master.organizer.clone(),
attendees: master.attendees.clone(),
conference_url: master.conference_url.clone(),
updated: master.updated,
sequence: master.sequence,
custom_properties: master.custom_properties.clone(),
});
}
}
Ok(events)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
#[test]
fn test_zoned_dtstart_with_utc_until() {
let start = EventTime::DateTimeZoned {
datetime: NaiveDateTime::new(
NaiveDate::from_ymd_opt(1968, 5, 8).unwrap(),
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
),
tzid: "Europe/Stockholm".to_string(),
};
let recurrence = Recurrence {
rrule: "FREQ=YEARLY;UNTIL=20080507T120000Z".to_string(),
exdates: vec![],
};
let rrule_str = build_rrule_string(&start, &recurrence);
eprintln!("Generated rrule string:\n{}", rrule_str);
let result: Result<RRuleSet, _> = rrule_str.parse();
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
}
#[test]
fn test_allday_dtstart_with_date_until() {
let start = EventTime::Date(NaiveDate::from_ymd_opt(2008, 5, 8).unwrap());
let recurrence = Recurrence {
rrule: "FREQ=YEARLY;UNTIL=20220507".to_string(),
exdates: vec![],
};
let rrule_str = build_rrule_string(&start, &recurrence);
eprintln!("Generated rrule string:\n{}", rrule_str);
let result: Result<RRuleSet, _> = rrule_str.parse();
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
}
}