diff options
-rw-r--r-- | src/iter/masks.rs | 1 | ||||
-rw-r--r-- | src/options.rs | 49 | ||||
-rw-r--r-- | src/parse_options.rs | 22 | ||||
-rw-r--r-- | src/rrulestr.rs | 92 |
4 files changed, 120 insertions, 44 deletions
diff --git a/src/iter/masks.rs b/src/iter/masks.rs index 726d270..d789d00 100644 --- a/src/iter/masks.rs +++ b/src/iter/masks.rs @@ -4,7 +4,6 @@ // Every mask is 7 days longer to handle cross-year weekly periods. - lazy_static! { pub static ref MASKS: Masks = Masks::default(); } diff --git a/src/options.rs b/src/options.rs index 0863294..ac35315 100644 --- a/src/options.rs +++ b/src/options.rs @@ -1,8 +1,8 @@ use crate::datetime::{get_weekday_val, DTime}; use crate::parse_options::parse_options; use chrono::prelude::*; -use serde::{Serialize, Deserialize}; use chrono_tz::{Tz, UTC}; +use serde::{Deserialize, Serialize}; use std::error::Error; use std::fmt::{Display, Formatter}; @@ -17,6 +17,40 @@ pub enum Frequenzy { Secondly = 6, } +#[derive(Copy, Clone, Debug)] +pub struct NWeekday { + pub weekday: usize, + pub n: isize, +} + +impl NWeekday { + pub fn new(weekday: usize, n: isize) -> Self { + // if (n === 0) throw new Error("Can't create weekday with n == 0") + Self { weekday, n } + } + + pub fn from(weekday: &Weekday, n: isize) -> Self { + // if (n === 0) throw new Error("Can't create weekday with n == 0") + Self { + weekday: get_weekday_val(weekday), + n, + } + } + + pub fn nth(&self, n: isize) -> Self { + if self.n == n { + return self.clone(); + } + Self::new(self.weekday, n) + } +} + +impl PartialEq for NWeekday { + fn eq(&self, other: &Self) -> bool { + self.n == other.n && self.weekday == other.weekday + } +} + #[derive(Debug, Clone)] pub struct ParsedOptions { pub freq: Frequenzy, @@ -33,10 +67,10 @@ pub struct ParsedOptions { pub byyearday: Vec<isize>, pub byweekno: Vec<isize>, pub byweekday: Vec<usize>, + pub bynweekday: Vec<Vec<isize>>, pub byhour: Vec<usize>, pub byminute: Vec<usize>, pub bysecond: Vec<usize>, - pub bynweekday: Vec<Vec<isize>>, pub byeaster: Option<isize>, } @@ -54,7 +88,7 @@ pub struct Options { pub bymonthday: Option<Vec<isize>>, pub byyearday: Option<Vec<isize>>, pub byweekno: Option<Vec<isize>>, - pub byweekday: Option<Vec<usize>>, + pub byweekday: Option<Vec<NWeekday>>, pub byhour: Option<Vec<usize>>, pub byminute: Option<Vec<usize>>, pub bysecond: Option<Vec<usize>>, @@ -198,7 +232,11 @@ impl Options { /// When given, these variables will define the weekdays where the recurrence /// will be applied. pub fn byweekday(mut self, byweekday: Vec<Weekday>) -> Self { - let byweekday = byweekday.iter().map(|w| get_weekday_val(w)).collect(); + let byweekday = byweekday + .iter() + .map(|w| get_weekday_val(w)) + .map(|w| NWeekday::new(w, 1)) + .collect(); self.byweekday = Some(byweekday); self } @@ -250,7 +288,6 @@ impl Display for RRuleParseError { impl Error for RRuleParseError {} - pub fn weekday_from_str(val: &str) -> Result<Weekday, String> { match val { "MO" => Ok(Weekday::Mon), @@ -262,4 +299,4 @@ pub fn weekday_from_str(val: &str) -> Result<Weekday, String> { "SU" => Ok(Weekday::Sun), _ => Err(format!("Invalid weekday: {}", val)), } -}
\ No newline at end of file +} diff --git a/src/parse_options.rs b/src/parse_options.rs index 1f86311..028686d 100644 --- a/src/parse_options.rs +++ b/src/parse_options.rs @@ -1,4 +1,4 @@ -use crate::options::{Frequenzy, Options, ParsedOptions, RRuleParseError}; +use crate::options::{Frequenzy, NWeekday, Options, ParsedOptions, RRuleParseError}; use crate::utils::is_some_and_not_empty; use chrono::prelude::*; use chrono_tz::{Tz, UTC}; @@ -69,7 +69,8 @@ pub fn parse_options(options: &Options) -> Result<ParsedOptions, RRuleParseError partial_options.bymonthday = Some(vec![dtstart.day() as isize]); } Frequenzy::Weekly => { - partial_options.byweekday = Some(vec![dtstart.weekday() as usize]); + partial_options.byweekday = + Some(vec![NWeekday::new(dtstart.weekday() as usize, 1)]); } _ => (), }; @@ -92,9 +93,18 @@ pub fn parse_options(options: &Options) -> Result<ParsedOptions, RRuleParseError } } + let mut byweekday = vec![]; + let mut bynweekday: Vec<Vec<isize>> = vec![]; // byweekday / bynweekday // ! more to do here - if partial_options.byweekday.is_some() { - // partial_options.bynweekday = None; + + if let Some(opts_byweekday) = partial_options.byweekday { + for wday in opts_byweekday { + if wday.n == 1 { + byweekday.push(wday.weekday); + } else { + bynweekday.push(vec![wday.weekday as isize, wday.n]); + } + } } // byhour @@ -126,11 +136,11 @@ pub fn parse_options(options: &Options) -> Result<ParsedOptions, RRuleParseError bynmonthday, byyearday: partial_options.byyearday.unwrap_or(vec![]), byweekno: partial_options.byweekno.unwrap_or(vec![]), - byweekday: partial_options.byweekday.unwrap_or(vec![]), + byweekday, + bynweekday, byhour: partial_options.byhour.unwrap_or(vec![]), byminute: partial_options.byminute.unwrap_or(vec![]), bysecond: partial_options.bysecond.unwrap_or(vec![]), - bynweekday, byeaster: partial_options.byeaster, }) } diff --git a/src/rrulestr.rs b/src/rrulestr.rs index 1d263ed..756a5ae 100644 --- a/src/rrulestr.rs +++ b/src/rrulestr.rs @@ -1,9 +1,9 @@ +use crate::datetime::get_weekday_val; use crate::datetime::DTime; use crate::options::*; use crate::parse_options::parse_options; use crate::rrule::RRule; use crate::rruleset::RRuleSet; -use crate::datetime::get_weekday_val; use chrono::prelude::*; use chrono_tz::{Tz, UTC}; use regex::Regex; @@ -21,6 +21,7 @@ lazy_static! { static ref RDATE_RE: Regex = Regex::new(r"(?m)RDATE(?:;TZID=([^:=]+))?").unwrap(); static ref EXDATE_RE: Regex = Regex::new(r"(?m)EXDATE(?:;TZID=([^:=]+))?").unwrap(); static ref DATETIME_RE: Regex = Regex::new(r"(?m)(VALUE=DATE(-TIME)?)|(TZID=)").unwrap(); + static ref NWEEKDAY_REGEX: Regex = Regex::new(r"(?m)^([+-]?\d{1,2})([A-Z]{2})$").unwrap(); } fn parse_datestring_bit<T: FromStr>( @@ -38,7 +39,6 @@ fn parse_datestring_bit<T: FromStr>( } fn datestring_to_date(dt: &str, tz: &Tz) -> Result<DTime, RRuleParseError> { - let bits = DATESTR_RE.captures(dt); if bits.is_none() { return Err(RRuleParseError(format!("Invalid datetime: {}", dt))); @@ -71,7 +71,7 @@ fn parse_dtstart(s: &str) -> Result<Options, RRuleParseError> { } else { UTC }; - + let dtstart_str = match caps.get(2) { Some(dt) => dt.as_str(), None => return Err(RRuleParseError(format!("Invalid datetime: {}", s))), @@ -154,16 +154,14 @@ fn parse_rrule(line: &str) -> Result<Options, RRuleParseError> { Some(freq) => options.freq = Some(freq), None => return Err(RRuleParseError(format!("Invalid frequenzy: {}", value))), }, - "WKST" => { - match weekday_from_str(value) { - Ok(weekday) => { - options.wkst = Some(get_weekday_val(&weekday)); - } - Err(e) => { - return Err(RRuleParseError(e)); - } + "WKST" => match weekday_from_str(value) { + Ok(weekday) => { + options.wkst = Some(get_weekday_val(&weekday)); } - } + Err(e) => { + return Err(RRuleParseError(e)); + } + }, "COUNT" => { let count = stringval_to_int(value, format!("Invalid count"))?; options.count = Some(count); @@ -266,25 +264,23 @@ fn str_to_weekday(d: &str) -> Result<usize, RRuleParseError> { } } -fn parse_weekday(val: &str) -> Result<Vec<usize>, RRuleParseError> { - val.split(",") - .map(|day| { - if day.len() == 2 { - // MO, TU, ... - return str_to_weekday(day); - } +fn parse_weekday(val: &str) -> Result<Vec<NWeekday>, RRuleParseError> { + let mut wdays = vec![]; + for day in val.split(",") { + if day.len() == 2 { + // MO, TU, ... + let wday = str_to_weekday(day)?; + wdays.push(NWeekday::new(wday, 1)); + continue; + } - // ! NOT SUPPORTED YET - // -1MO, +3FR, 1SO, 13TU ... - // let regex = Regex::new(r"(?m)^([+-]?\d{1,2})([A-Z]{2})$").unwrap(); - // let parts = regex.captures(day).unwrap(); - // let n = parts.get(1).unwrap(); - // let wdaypart = parts.get(2).unwrap(); - // let wday = str_to_weekday(d) - - return str_to_weekday(day); - }) - .collect() + let parts = NWEEKDAY_REGEX.captures(day).unwrap(); + let n = parts.get(1).unwrap().as_str().parse().unwrap(); + let wdaypart = parts.get(2).unwrap(); + let wday = str_to_weekday(wdaypart.as_str())?; + wdays.push(NWeekday::new(wday, n)); + } + Ok(wdays) } fn parse_line(rfc_string: &str) -> Result<Option<Options>, RRuleParseError> { @@ -504,7 +500,8 @@ fn parse_rdate( } fn preprocess_rrule_string(s: &str) -> String { - s.replace("DTSTART;VALUE=DATETIME", "DTSTART").replace("DTSTART;VALUE=DATE", "DTSTART") + s.replace("DTSTART;VALUE=DATETIME", "DTSTART") + .replace("DTSTART;VALUE=DATE", "DTSTART") } pub fn build_rruleset(s: &str) -> Result<RRuleSet, RRuleParseError> { @@ -677,6 +674,39 @@ mod test { } #[test] + fn parses_byday_with_n() { + let cases = vec![ + "DTSTART:20200901T174500\nRRULE:FREQ=MONTHLY;UNTIL=20210504T154500Z;INTERVAL=1;BYDAY=1TU", + "DTSTART;VALUE=DATE:20200902\nRRULE:FREQ=MONTHLY;UNTIL=20210504T220000Z;INTERVAL=1;BYDAY=1WE", + "DTSTART:20200902T100000\nRRULE:FREQ=MONTHLY;UNTIL=20210505T080000Z;INTERVAL=1;BYDAY=1WE", + "DTSTART;VALUE=DATE:20200812\nRRULE:FREQ=MONTHLY;UNTIL=20210524T090000Z;INTERVAL=1;BYDAY=4MO" + ]; + for case in &cases { + let res = build_rruleset(case); + assert!(res.is_ok()); + } + let cases = vec![ + "RRULE:FREQ=MONTHLY;UNTIL=20210504T154500Z;INTERVAL=1;BYDAY=1TU", + "RRULE:FREQ=MONTHLY;UNTIL=20210504T220000Z;INTERVAL=1;BYDAY=1WE", + "RRULE:FREQ=MONTHLY;UNTIL=20210505T080000Z;INTERVAL=1;BYDAY=-1WE", + "RRULE:FREQ=MONTHLY;UNTIL=20210505T080000Z;INTERVAL=1;BYDAY=12SU", + "RRULE:FREQ=MONTHLY;UNTIL=20210524T090000Z;INTERVAL=1;BYDAY=4MO", + ]; + let opts = vec![ + vec![NWeekday::new(1,1)], + vec![NWeekday::new(2,1)], + vec![NWeekday::new(2,-1)], + vec![NWeekday::new(6, 12)], + vec![NWeekday::new(0,4)] + ]; + for i in 0..cases.len() { + let opts_or_err = parse_string(cases[i]); + assert!(opts_or_err.is_ok()); + assert_eq!(opts_or_err.unwrap().byweekday.unwrap(), opts[i]); + } + } + + #[test] #[ignore = "Only for benching"] fn bench() { let now = std::time::SystemTime::now(); |