From 098ac045545d4149b0062bbb365bf7467eb2bff3 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Tue, 19 May 2020 11:29:25 +0200 Subject: Add support for ical's BYDAY recursion rule --- src/date.rs | 32 ++++++++++++++++-- src/errors.rs | 1 + src/periodic.rs | 101 ++++++++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 107 insertions(+), 27 deletions(-) diff --git a/src/date.rs b/src/date.rs index ab1c2c8..98e052a 100644 --- a/src/date.rs +++ b/src/date.rs @@ -1,10 +1,10 @@ use std::cmp::{Ordering, Ord}; -use std::ops::Add; +use std::ops::{Add, Sub}; use errors::EventError; use chrono; -use chrono::{TimeZone, Duration, Datelike, Local}; +use chrono::{TimeZone, Duration, Datelike, Local, Weekday}; use chrono::offset::Utc; use chrono_tz::{Tz, UTC}; @@ -79,6 +79,13 @@ impl Date { } } + pub fn weekday(&self) -> Weekday { + match *self { + Date::Time(t) => t.weekday(), + Date::AllDay(d) => d.weekday(), + } + } + pub fn month(&self) -> u32 { match *self { Date::Time(t) => t.month(), @@ -144,6 +151,27 @@ impl Add for Date { } } +impl Sub for Date { + type Output = Duration; + + fn sub(self, other: Self) -> Duration { + match self { + Date::Time(t1) => { + match other { + Date::Time(t2) => t1 - t2, + Date::AllDay(d) => t1.date() - d, + } + } + Date::AllDay(d1) => { + match other { + Date::Time(t) => d1 - t.date(), + Date::AllDay(d2) => d1 - d2, + } + } + } + } +} + fn cmp_date_time(date: &chrono::Date, time: &chrono::DateTime) -> Ordering { let d2 = time.date(); if date.eq(&d2) { diff --git a/src/errors.rs b/src/errors.rs index 9f570d2..431e6e4 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -9,6 +9,7 @@ pub enum EventError { IntError(ParseIntError), StatusError, FreqError, + BydayError, } impl From for EventError { diff --git a/src/periodic.rs b/src/periodic.rs index d69b817..b557ef7 100644 --- a/src/periodic.rs +++ b/src/periodic.rs @@ -1,7 +1,8 @@ use std::fmt; use std::str::FromStr; +use std::collections::HashSet; -use chrono::Duration; +use chrono::{Duration, Weekday}; use date::Date; use event::{Event, End}; @@ -14,6 +15,8 @@ pub struct Periodic { pub interval: i64, pub count: Option, pub until: Option, + pub byday: Option>, + pub wkst: Weekday, } #[derive(Debug)] @@ -35,6 +38,8 @@ impl Periodic { interval: 1, count: None, until: None, + byday: None, + wkst: Weekday::Mon, } } @@ -44,6 +49,8 @@ impl Periodic { "INTERVAL" => self.interval = value.parse()?, "COUNT" => self.count = Some(value.parse()?), "UNTIL" => self.until = Some(Date::parse(&value, "")?), + "BYDAY" => self.byday = Some(parse_byday(value)?), + "WKST" => self.wkst = parse_weekday(value)?, _ => (), } Ok(()) @@ -82,14 +89,59 @@ impl<'a> Iterator for Iter<'a> { event.start = self.start; event.end = End::Date(self.end); + let duration = self.next_duration(self.start); + self.start = self.start + duration; + self.end = self.end + duration; self.count += 1; - self.start = p.freq.next_date(self.start, p.interval); - self.end = p.freq.next_date(self.end, p.interval); Some(event) } } +impl<'a> Iter<'a> { + fn next_duration(&self, date: Date) -> Duration { + let p = self.periodic; + match p.freq { + Freq::Secondly => Duration::seconds(p.interval), + Freq::Minutely => Duration::minutes(p.interval), + Freq::Hourly => Duration::hours(p.interval), + Freq::Daily => Duration::days(p.interval), + Freq::Weekly => { + match &p.byday { + None => Duration::weeks(p.interval), + Some(byday) => { + let mut weekday = date.weekday().succ(); + let mut days = 1; + if weekday == p.wkst { + days += 7 * (p.interval - 1); + } + while !byday.contains(&weekday) { + weekday = weekday.succ(); + days += 1; + if weekday == p.wkst { + days += 7 * (p.interval - 1); + } + } + Duration::days(days) + } + } + } + Freq::Monthly => { + let new_date = if date.month() == 12 { + date.with_month(1) + .unwrap() + .with_year(date.year() + 1) + .unwrap() + } else { + date.with_month(date.month() + 1).unwrap() + }; + new_date - date + } + Freq::Yearly => date.with_year(date.year() + 1).unwrap() - date, + } + } +} + impl fmt::Display for Periodic { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self.freq)?; @@ -101,28 +153,6 @@ impl fmt::Display for Periodic { } } -impl Freq { - pub fn next_date(&self, date: Date, count: i64) -> Date { - match self { - Freq::Secondly => date + Duration::seconds(count), - Freq::Minutely => date + Duration::minutes(count), - Freq::Hourly => date + Duration::hours(count), - Freq::Daily => date + Duration::days(count), - Freq::Weekly => date + Duration::weeks(count), - Freq::Monthly => { - let month = date.month(); - if month == 12 { - let date = date.with_month(1).unwrap(); - date.with_year(date.year() + 1).unwrap() - } else { - date.with_month(month + 1).unwrap() - } - } - Freq::Yearly => date.with_year(date.year() + 1).unwrap(), - } - } -} - impl FromStr for Freq { type Err = EventError; @@ -139,3 +169,24 @@ impl FromStr for Freq { } } } + +fn parse_byday(s: &str) -> Result, EventError> { + let mut byday = HashSet::new(); + for v in s.split(",") { + byday.insert(parse_weekday(v)?); + } + Ok(byday) +} + +fn parse_weekday(s: &str) -> Result { + match s { + "MO" => Ok(Weekday::Mon), + "TU" => Ok(Weekday::Tue), + "WE" => Ok(Weekday::Wed), + "TH" => Ok(Weekday::Thu), + "FR" => Ok(Weekday::Fri), + "SA" => Ok(Weekday::Sat), + "SU" => Ok(Weekday::Sun), + _ => Err(EventError::BydayError), + } +} -- cgit v1.2.3