use crate::error::{Result, TruthError};
use chrono::{DateTime, Duration, Utc};
use rrule::RRuleSet;
#[derive(Debug, Clone, PartialEq)]
pub struct ExpandedEvent {
pub start: DateTime<Utc>,
pub end: DateTime<Utc>,
}
pub fn expand_rrule(
rrule: &str,
dtstart: &str,
duration_minutes: u32,
timezone: &str,
until: Option<&str>,
count: Option<u32>,
) -> Result<Vec<ExpandedEvent>> {
expand_rrule_with_exdates(
rrule,
dtstart,
duration_minutes,
timezone,
until,
count,
&[],
)
}
pub fn expand_rrule_with_exdates(
rrule: &str,
dtstart: &str,
duration_minutes: u32,
timezone: &str,
until: Option<&str>,
count: Option<u32>,
exdates: &[&str],
) -> Result<Vec<ExpandedEvent>> {
if rrule.is_empty() {
return Err(TruthError::InvalidRule("empty RRULE string".to_string()));
}
if count == Some(0) {
return Ok(Vec::new());
}
let _tz: chrono_tz::Tz = timezone
.parse()
.map_err(|_| TruthError::InvalidTimezone(timezone.to_string()))?;
let dtstart_ical = dtstart.replace(['-', ':'], "");
let mut rrule_str = rrule.to_string();
if let Some(c) = count {
if !rrule_str.to_uppercase().contains("COUNT=") {
rrule_str = format!("{};COUNT={}", rrule_str, c);
}
}
if let Some(until_str) = until {
if !rrule_str.to_uppercase().contains("UNTIL=") {
let mut until_ical = until_str.replace(['-', ':'], "");
if timezone == "UTC" {
until_ical.push('Z');
}
rrule_str = format!("{};UNTIL={}", rrule_str, until_ical);
}
}
let mut rrule_text = format!(
"DTSTART;TZID={}:{}\nRRULE:{}",
timezone, dtstart_ical, rrule_str
);
if !exdates.is_empty() {
let exdate_icals: Vec<String> = exdates.iter().map(|d| d.replace(['-', ':'], "")).collect();
rrule_text.push_str(&format!(
"\nEXDATE;TZID={}:{}",
timezone,
exdate_icals.join(",")
));
}
let rrule_set: RRuleSet = rrule_text
.parse()
.map_err(|e| TruthError::InvalidRule(format!("{}", e)))?;
let exdate_buffer = exdates.len() as u16;
let max_count: u16 = count
.map(|c| (c as u16).saturating_add(exdate_buffer))
.unwrap_or(500);
let instances = rrule_set.all(max_count);
let duration = Duration::minutes(duration_minutes as i64);
let mut events: Vec<ExpandedEvent> = instances
.dates
.into_iter()
.map(|dt| {
let start_utc: DateTime<Utc> = dt.with_timezone(&Utc);
ExpandedEvent {
start: start_utc,
end: start_utc + duration,
}
})
.collect();
if let Some(c) = count {
events.truncate(c as usize);
}
Ok(events)
}