use crate::core::datetime::datetime_to_ical_format;
use crate::core::utils::collect_with_error;
use crate::parser::{ContentLine, Grammar};
use crate::{ParseError, RRule, RRuleError, Tz};
use chrono::DateTime;
#[cfg(feature = "serde")]
use serde_with::{serde_as, DeserializeFromStr, SerializeDisplay};
use std::fmt::Display;
use std::str::FromStr;
#[cfg_attr(feature = "serde", serde_as)]
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(DeserializeFromStr, SerializeDisplay))]
pub struct RRuleSet {
pub(crate) rrule: Vec<RRule>,
pub(crate) rdate: Vec<DateTime<Tz>>,
pub(crate) exrule: Vec<RRule>,
pub(crate) exdate: Vec<DateTime<Tz>>,
pub(crate) dt_start: DateTime<Tz>,
pub(crate) before: Option<DateTime<Tz>>,
pub(crate) after: Option<DateTime<Tz>>,
pub(crate) limited: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RRuleResult {
pub dates: Vec<DateTime<Tz>>,
pub limited: bool,
}
impl RRuleSet {
#[must_use]
pub fn new(dt_start: DateTime<Tz>) -> Self {
Self {
dt_start,
rrule: vec![],
rdate: vec![],
exrule: vec![],
exdate: vec![],
before: None,
after: None,
limited: false,
}
}
#[must_use]
pub fn limit(mut self) -> Self {
self.limited = true;
self
}
#[must_use]
pub fn before(mut self, dt: DateTime<Tz>) -> Self {
self.before = Some(dt);
self
}
#[must_use]
pub fn after(mut self, dt: DateTime<Tz>) -> Self {
self.after = Some(dt);
self
}
#[must_use]
pub fn rrule(mut self, rrule: RRule) -> Self {
self.rrule.push(rrule);
self
}
#[must_use]
#[cfg(feature = "exrule")]
pub fn exrule(mut self, rrule: RRule) -> Self {
self.exrule.push(rrule);
self
}
#[must_use]
pub fn rdate(mut self, rdate: DateTime<Tz>) -> Self {
self.rdate.push(rdate);
self
}
#[must_use]
pub fn exdate(mut self, exdate: DateTime<Tz>) -> Self {
self.exdate.push(exdate);
self
}
#[must_use]
pub fn set_rrules(mut self, rrules: Vec<RRule>) -> Self {
self.rrule = rrules;
self
}
#[must_use]
#[cfg(feature = "exrule")]
pub fn set_exrules(mut self, exrules: Vec<RRule>) -> Self {
self.exrule = exrules;
self
}
#[must_use]
pub fn set_rdates(mut self, rdates: Vec<DateTime<Tz>>) -> Self {
self.rdate = rdates;
self
}
#[must_use]
pub fn set_exdates(mut self, exdates: Vec<DateTime<Tz>>) -> Self {
self.exdate = exdates;
self
}
#[must_use]
pub fn get_rrule(&self) -> &Vec<RRule> {
&self.rrule
}
#[must_use]
pub fn get_exrule(&self) -> &Vec<RRule> {
&self.exrule
}
#[must_use]
pub fn get_rdate(&self) -> &Vec<DateTime<Tz>> {
&self.rdate
}
#[must_use]
pub fn get_exdate(&self) -> &Vec<DateTime<Tz>> {
&self.exdate
}
#[must_use]
pub fn get_dt_start(&self) -> &DateTime<Tz> {
&self.dt_start
}
#[must_use]
pub fn all(mut self, limit: u16) -> RRuleResult {
self.limited = true;
collect_with_error(
self.into_iter(),
&self.after,
&self.before,
true,
Some(limit),
)
}
#[must_use]
pub fn all_unchecked(self) -> Vec<DateTime<Tz>> {
collect_with_error(self.into_iter(), &self.after, &self.before, true, None).dates
}
fn set_from_content_lines(self, content_lines: Vec<ContentLine>) -> Result<Self, RRuleError> {
let dt_start = self.dt_start;
content_lines.into_iter().try_fold(
self,
|rrule_set, content_line| match content_line {
ContentLine::RRule(rrule) => rrule
.validate(dt_start)
.map(|rrule| rrule_set.rrule(rrule)),
#[allow(unused_variables)]
ContentLine::ExRule(exrule) => {
#[cfg(feature = "exrule")]
{
exrule
.validate(dt_start)
.map(|exrule| rrule_set.exrule(exrule))
}
#[cfg(not(feature = "exrule"))]
{
log::warn!("Found EXRULE in input, but it will be ignored since the `exrule` feature is not enabled.");
Ok(rrule_set)
}
}
ContentLine::ExDate(exdates) => {
Ok(exdates.into_iter().fold(rrule_set, Self::exdate))
}
ContentLine::RDate(rdates) => {
Ok(rdates.into_iter().fold(rrule_set, Self::rdate))
}
},
)
}
pub fn set_from_string(mut self, s: &str) -> Result<Self, RRuleError> {
let Grammar {
start,
content_lines,
} = Grammar::from_str(s)?;
if let Some(dtstart) = start {
self.dt_start = dtstart.datetime;
}
self.set_from_content_lines(content_lines)
}
}
impl FromStr for RRuleSet {
type Err = RRuleError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Grammar {
start,
content_lines,
} = Grammar::from_str(s)?;
let start = start.ok_or(ParseError::MissingStartDate)?;
Self::new(start.datetime).set_from_content_lines(content_lines)
}
}
impl Display for RRuleSet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let start_datetime = format!("DTSTART{}", datetime_to_ical_format(&self.dt_start));
let mut rrules = self
.rrule
.iter()
.map(|rrule| format!("RRULE:{rrule}"))
.collect::<Vec<_>>()
.join("\n");
if !rrules.is_empty() {
rrules = format!("\n{rrules}");
}
let mut rdates = self
.rdate
.iter()
.map(|dt| {
let maybe_zulu = if dt.timezone().is_local() { "" } else { "Z" };
format!("{}{}", dt.format("%Y%m%dT%H%M%S"), maybe_zulu)
})
.collect::<Vec<_>>()
.join(",");
if !rdates.is_empty() {
rdates = format!("\nRDATE;VALUE=DATE-TIME:{rdates}");
}
let mut exrules = self
.exrule
.iter()
.map(|exrule| format!("EXRULE:{exrule}"))
.collect::<Vec<_>>()
.join("\n");
if !exrules.is_empty() {
exrules = format!("\n{exrules}");
}
let mut exdates = self
.exdate
.iter()
.map(|dt| {
let maybe_zulu = if dt.timezone().is_local() { "" } else { "Z" };
format!("{}{}", dt.format("%Y%m%dT%H%M%S"), maybe_zulu)
})
.collect::<Vec<_>>()
.join(",");
if !exdates.is_empty() {
exdates = format!("\nEXDATE;VALUE=DATE-TIME:{exdates}");
}
write!(f, "{start_datetime}{rrules}{rdates}{exrules}{exdates}")
}
}
#[cfg(feature = "exrule")]
#[cfg(test)]
mod tests {
use std::str::FromStr;
use chrono::{Month, TimeZone};
use crate::{Frequency, RRule, RRuleSet, Tz};
#[test]
fn rruleset_string_roundtrip() {
let rruleset_str = "DTSTART:20120201T093000Z\nRRULE:FREQ=DAILY;COUNT=3;BYHOUR=9;BYMINUTE=30;BYSECOND=0\nRDATE;VALUE=DATE-TIME:19970101T000000Z,19970120T000000Z\nEXRULE:FREQ=YEARLY;COUNT=8;BYMONTH=6,7;BYMONTHDAY=1;BYHOUR=9;BYMINUTE=30;BYSECOND=0\nEXDATE;VALUE=DATE-TIME:19970121T000000Z";
let rruleset = RRuleSet::from_str(rruleset_str).unwrap();
let dt_start = Tz::UTC.with_ymd_and_hms(2012, 2, 1, 9, 30, 0).unwrap();
assert_eq!(rruleset.dt_start, dt_start);
assert_eq!(
rruleset.rrule,
vec![RRule::new(Frequency::Daily)
.count(3)
.validate(dt_start)
.unwrap()]
);
assert_eq!(
rruleset.rdate,
vec![
Tz::UTC.with_ymd_and_hms(1997, 1, 1, 0, 0, 0).unwrap(),
Tz::UTC.with_ymd_and_hms(1997, 1, 20, 0, 0, 0).unwrap()
]
);
assert_eq!(
rruleset.exrule,
vec![RRule::new(Frequency::Yearly)
.count(8)
.by_month(&[Month::June, Month::July])
.validate(dt_start)
.unwrap()]
);
assert_eq!(
rruleset.exdate,
vec![Tz::UTC.with_ymd_and_hms(1997, 1, 21, 0, 0, 0).unwrap()]
);
assert_eq!(rruleset.to_string(), rruleset_str);
}
#[test]
fn respect_local_timezone_in_exdates_rdates() {
let rruleset_str = "DTSTART:20120201T093000Z\nRRULE:FREQ=DAILY;COUNT=3;BYHOUR=9;BYMINUTE=30;BYSECOND=0\nRDATE;VALUE=DATE-TIME:19970101T000000,19970120T000000\nEXRULE:FREQ=YEARLY;COUNT=8;BYMONTH=6,7;BYMONTHDAY=1;BYHOUR=9;BYMINUTE=30;BYSECOND=0\nEXDATE;VALUE=DATE-TIME:19970121T000000";
let rruleset = RRuleSet::from_str(rruleset_str).unwrap();
assert_eq!(rruleset.to_string(), rruleset_str);
}
#[test]
fn respect_utc_timezone_in_exdates_rdates() {
let rruleset_str = "DTSTART:20120201T093000Z\nRRULE:FREQ=DAILY;COUNT=3;BYHOUR=9;BYMINUTE=30;BYSECOND=0\nRDATE;VALUE=DATE-TIME:19970101T000000Z,19970120T000000Z\nEXRULE:FREQ=YEARLY;COUNT=8;BYMONTH=6,7;BYMONTHDAY=1;BYHOUR=9;BYMINUTE=30;BYSECOND=0\nEXDATE;VALUE=DATE-TIME:19970121T000000Z";
let rruleset = RRuleSet::from_str(rruleset_str).unwrap();
assert_eq!(rruleset.to_string(), rruleset_str);
}
}