diff options
author | Fredrik Meringdal <fmeringdal@hotmail.com> | 2020-11-01 22:36:38 +0100 |
---|---|---|
committer | Fredrik Meringdal <fmeringdal@hotmail.com> | 2020-11-01 22:36:38 +0100 |
commit | 000c7c9900cbe93f5a72bbbef3ad4eaa3906b132 (patch) | |
tree | e74d1bfe48f446f75a00dac875988a71d47fb1b4 /src | |
parent | ed603b788b23535b086f2a7491627e018e8dce5d (diff) | |
download | rust_rrule-000c7c9900cbe93f5a72bbbef3ad4eaa3906b132.zip |
FromStr impl for RRule and RRuleSet + docs
Diffstat (limited to 'src')
-rw-r--r-- | src/datetime.rs | 2 | ||||
-rw-r--r-- | src/iter/mod.rs | 33 | ||||
-rw-r--r-- | src/iter/monthinfo.rs | 2 | ||||
-rw-r--r-- | src/iter/poslist.rs | 2 | ||||
-rw-r--r-- | src/iter/yearinfo.rs | 2 | ||||
-rw-r--r-- | src/lib.rs | 71 | ||||
-rw-r--r-- | src/parse_options.rs | 10 | ||||
-rw-r--r-- | src/rrule.rs | 16 | ||||
-rw-r--r-- | src/rruleset.rs | 11 | ||||
-rw-r--r-- | src/rrulestr.rs | 78 | ||||
-rw-r--r-- | src/utils.rs (renamed from src/iter/utils.rs) | 7 |
11 files changed, 155 insertions, 79 deletions
diff --git a/src/datetime.rs b/src/datetime.rs index 8099ad4..3740018 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -1,6 +1,6 @@ use chrono::prelude::*; use chrono::Utc; -use chrono_tz::{Tz}; +use chrono_tz::Tz; pub type DTime = DateTime<Tz>; diff --git a/src/iter/mod.rs b/src/iter/mod.rs index c9fa858..5e6e4d1 100644 --- a/src/iter/mod.rs +++ b/src/iter/mod.rs @@ -6,7 +6,6 @@ mod poslist; use poslist::build_poslist; mod easter; mod masks; -mod utils; use crate::datetime::{from_ordinal, get_weekday_val, DTime, Time}; use crate::options::*; @@ -210,24 +209,26 @@ pub fn increment_counter_date( let hours = counter_date.hour() as usize; if filtered { // Jump to one iteration before next day - minutes_inc = (1439. - ((hours*60+minutes) as f32/options.interval as f32)).floor() as usize *options.interval; + minutes_inc = (1439. - ((hours * 60 + minutes) as f32 / options.interval as f32)) + .floor() as usize + * options.interval; } - let mut counter_date = counter_date + Duration::minutes(minutes_inc as i64); loop { counter_date = counter_date + Duration::minutes(options.interval as i64); let minutes = counter_date.minute() as usize; let hours = counter_date.hour() as usize; - if (options.byhour.is_empty() || includes(&options.byhour, &hours)) && ( - options.byminute.is_empty() || includes(&options.byminute, &minutes)) { - break; + if (options.byhour.is_empty() || includes(&options.byhour, &hours)) + && (options.byminute.is_empty() || includes(&options.byminute, &minutes)) + { + break; } } counter_date - }, + } Frequenzy::Secondly => { let mut seconds_inc = 0; let seconds = counter_date.second() as usize; @@ -235,10 +236,12 @@ pub fn increment_counter_date( let hours = counter_date.hour() as usize; if filtered { // Jump to one iteration before next day - seconds_inc = (86399. - ((hours*3600+minutes*60+seconds) as f32/options.interval as f32)).floor() as usize *options.interval; + seconds_inc = (86399. + - ((hours * 3600 + minutes * 60 + seconds) as f32 / options.interval as f32)) + .floor() as usize + * options.interval; } - let mut counter_date = counter_date + Duration::seconds(seconds_inc as i64); loop { counter_date = counter_date + Duration::seconds(options.interval as i64); @@ -246,16 +249,16 @@ pub fn increment_counter_date( let minutes = counter_date.minute() as usize; let hours = counter_date.hour() as usize; - if (options.byhour.is_empty() || includes(&options.byhour, &hours)) && ( - options.byminute.is_empty() || includes(&options.byminute, &minutes)) && ( - options.bysecond.is_empty() || includes(&options.bysecond, &seconds) - ) { - break; + if (options.byhour.is_empty() || includes(&options.byhour, &hours)) + && (options.byminute.is_empty() || includes(&options.byminute, &minutes)) + && (options.bysecond.is_empty() || includes(&options.bysecond, &seconds)) + { + break; } } counter_date - }, + } } } diff --git a/src/iter/monthinfo.rs b/src/iter/monthinfo.rs index 43fcef8..b5becfe 100644 --- a/src/iter/monthinfo.rs +++ b/src/iter/monthinfo.rs @@ -1,5 +1,5 @@ -use crate::iter::utils::pymod; use crate::options::*; +use crate::utils::pymod; #[derive(Debug)] pub struct MonthInfo { diff --git a/src/iter/poslist.rs b/src/iter/poslist.rs index 9166073..635c1d3 100644 --- a/src/iter/poslist.rs +++ b/src/iter/poslist.rs @@ -1,7 +1,7 @@ use crate::datetime::from_ordinal; use crate::datetime::{DTime, Time}; use crate::iter::iterinfo::IterInfo; -use crate::iter::utils::pymod; +use crate::utils::pymod; use chrono::prelude::*; use chrono_tz::Tz; diff --git a/src/iter/yearinfo.rs b/src/iter/yearinfo.rs index 308a205..eb422f1 100644 --- a/src/iter/yearinfo.rs +++ b/src/iter/yearinfo.rs @@ -1,7 +1,7 @@ use crate::datetime::{get_weekday_val, get_year_len, to_ordinal}; use crate::iter::masks::MASKS; -use crate::iter::utils::pymod; use crate::options::*; +use crate::utils::pymod; use chrono::prelude::*; #[derive(Debug)] @@ -1,37 +1,49 @@ -//! A partial implementation of recurrence rules as defined in the iCalendar RFC. +//! A performant rust implementation of recurrence rules as defined in the iCalendar RFC. +//! +//! RRule provides two main types for working with recurrence rules: +//! - `RRule`: For working with a single recurrence rule without any exception dates (exdates / exrules) and no additonal dates (rdate). +//! - `RRuleSet`: For working with a collection of rrule`s, exrule`s, rdate`s and exdate`s. Both the rrule and exrule +//! properties are represented by the `RRule` type and the rdate and exdate properties are represented by the DateTime<Tz> type +//! provided by the [chrono](https://crates.io/crates/chrono) and [chrono-tz](https://crates.io/crates/chrono-tz) crates. +//! +//! # Building RRule and RRuleSet types +//! Both of the types implements the `std::str::FromStr` trait so that they can be parsed and built from a `str`. `RRule` +//! can additionally be constructured from the `Option` type which help build the recurrence rule. `RRuleSet` +//! can also be built by composing mutliple `RRule`s for its rrule and exrule properties and DateTime<Tz> for its +//! exdate and rdate properties. See the examples below. +//! +//! # Interface +//! `RRule` and `RRuleSet` have the same interface for generating recurrences. The four methods for "querying" recurrences are: +//! - `all`: Generate all recurrences that matches the rules +//! - `between`: Generate all recurrences that matches the rules and are between two given dates +//! - `before`: Generate the last recurrence that matches the rules and is before a given date +//! - `after`: Generate the first recurrence that matches the rules and is after a given date //! //! -//! //! # Examples //! -//! RRule quickstart from rrulestring +//! Quick start by parsing strings //! //! ``` //! extern crate rrule; //! -//! use rrule::build_rrule; +//! use rrule::RRule; //! -//! // Parse a RRule string, return a RRule type -//! let mut rrule = build_rrule("DTSTART:20120201T093000Z\nRRULE:FREQ=WEEKLY;INTERVAL=5;UNTIL=20130130T230000Z;BYDAY=MO,FR").unwrap(); +//! // Parse a RRule string, return a RRul to get e type +//! let mut rrule: RRule = "DTSTART:20120201T093000Z\nRRULE:FREQ=WEEKLY;INTERVAL=5;UNTIL=20130130T230000Z;BYDAY=MO,FR".parse().unwrap(); //! assert_eq!(rrule.all().len(), 21); -//! ``` -//! //! -//! RRuleSet quickstart from rrulestring -//! -//! ``` -//! extern crate rrule; //! -//! use rrule::build_rruleset; +//! use rrule::RRuleSet; //! //! // Parse a RRuleSet string, return a RRuleSet type -//! let mut rrule = build_rruleset("DTSTART:20120201T023000Z\nRRULE:FREQ=MONTHLY;COUNT=5\nRDATE:20120701T023000Z,20120702T023000Z\nEXRULE:FREQ=MONTHLY;COUNT=2\nEXDATE:20120601T023000Z").unwrap(); -//! assert_eq!(rrule.all().len(), 4); +//! let mut rrule_set: RRuleSet = "DTSTART:20120201T023000Z\nRRULE:FREQ=MONTHLY;COUNT=5\nRDATE:20120701T023000Z,20120702T023000Z\nEXRULE:FREQ=MONTHLY;COUNT=2\nEXDATE:20120601T023000Z".parse().unwrap(); +//! assert_eq!(rrule_set.all().len(), 4); //! ``` //! //! //! -//! Using `Options` instead of rrule strings to build RRule and RRuleSet +//! Using `Options` to build RRule //! //! ``` //! extern crate rrule; @@ -40,7 +52,7 @@ //! //! use chrono::prelude::*; //! use chrono_tz::UTC; -//! use rrule::{RRule, RRuleSet, Options, Frequenzy, Weekday}; +//! use rrule::{RRule, Options, Frequenzy, Weekday}; //! //! // Build options that starts first day in 2020 at 9:00AM and occurs daily 5 times //! let mut options = Options::new() @@ -60,15 +72,21 @@ //! assert_eq!(recurrences[i].hour(), 9); //! } //! assert_eq!(recurrences.len(), 5); +//! ``` //! //! +//! Construct RRuleSet from one rrule and exrule. +//! The rrule will occur weekly on Tuesday and Wednesday and the exrule +//! will occur weekly on Wednesday, and therefore the end result will contain +//! weekly recurrences just on Wednesday. +//! ``` +//! extern crate rrule; +//! extern crate chrono; +//! extern crate chrono_tz; //! -//! -//! -//! // Construct RRuleSet from one rrule and exrule -//! // The rrule will occur weekly on Tuesday and Wednesday and the exrule -//! // will occur weekly on Wednesday, and therefore the end result will contain -//! // weekly recurrences just on Wednesday. +//! use chrono::prelude::*; +//! use chrono_tz::UTC; +//! use rrule::{RRule, RRuleSet, Options, Frequenzy, Weekday}; //! //! //! // Build options for rrule that occurs weekly on Tuesday and Wednesday @@ -102,7 +120,8 @@ //! rrule_set.exrule(exrule); //! //! let recurrences = rrule_set.all(); -//! +//! +//! // Check that all the recurrences are on a Tuesday //! for occurence in &recurrences { //! assert_eq!(occurence.weekday(), Weekday::Tue); //! } @@ -162,7 +181,7 @@ //! // If you want to get back the DateTimes in another timezone you can just iterate over the result //! // and convert them to another timezone by using the with_timzone method provided by the DateTime type. //! // Refer to the chrono and chrono-tz crates for more documenation on working with the DateTime type. -//! +//! //! // Example of converting to mocow timezone //! use chrono_tz::Europe::Moscow; //! @@ -191,9 +210,9 @@ mod rrule_iter; mod rruleset; mod rruleset_iter; mod rrulestr; +mod utils; pub use crate::options::{Frequenzy, Options, ParsedOptions}; pub use crate::rrule::RRule; pub use crate::rruleset::RRuleSet; -pub use crate::rrulestr::{build_rrule, build_rruleset}; pub use chrono::Weekday; diff --git a/src/parse_options.rs b/src/parse_options.rs index 0387e51..1f86311 100644 --- a/src/parse_options.rs +++ b/src/parse_options.rs @@ -1,8 +1,9 @@ use crate::options::{Frequenzy, Options, ParsedOptions, RRuleParseError}; +use crate::utils::is_some_and_not_empty; use chrono::prelude::*; use chrono_tz::{Tz, UTC}; -// TODO: Validation +// TODO: More validation here pub fn parse_options(options: &Options) -> Result<ParsedOptions, RRuleParseError> { let mut default_partial_options = Options::new(); default_partial_options.interval = Some(1); @@ -133,10 +134,3 @@ pub fn parse_options(options: &Options) -> Result<ParsedOptions, RRuleParseError byeaster: partial_options.byeaster, }) } - -fn is_some_and_not_empty<T>(v: &Option<Vec<T>>) -> bool { - match v { - Some(v) => !v.is_empty(), - None => false, - } -} diff --git a/src/rrule.rs b/src/rrule.rs index 68f77ad..c2a080e 100644 --- a/src/rrule.rs +++ b/src/rrule.rs @@ -1,8 +1,10 @@ use crate::iter::iter; use crate::options::*; use crate::rrule_iter::*; +use crate::rrulestr::build_rrule; use chrono::prelude::*; use chrono_tz::Tz; +use std::str::FromStr; #[derive(Clone, Debug)] pub struct RRule { @@ -27,7 +29,7 @@ impl RRule { let res = iter(&mut iter_res, &mut self.options); res } - + /// Returns the last recurrence before the given datetime instance. /// The inc keyword defines what happens if dt is an recurrence. /// With inc == true, if dt itself is an recurrence, it will be returned. @@ -40,7 +42,7 @@ impl RRule { }; let mut iter_res = RRuleIterRes::new(QueryMethodTypes::Before, iter_args); - let recurrences = iter(&mut iter_res, &mut self.options); + let recurrences = iter(&mut iter_res, &mut self.options); if recurrences.is_empty() { None } else { @@ -59,7 +61,7 @@ impl RRule { dt: Some(dt), }; let mut iter_res = RRuleIterRes::new(QueryMethodTypes::After, iter_args); - let recurrences = iter(&mut iter_res, &mut self.options); + let recurrences = iter(&mut iter_res, &mut self.options); if recurrences.is_empty() { None } else { @@ -88,3 +90,11 @@ impl RRule { iter(&mut iter_res, &mut self.options) } } + +impl FromStr for RRule { + type Err = RRuleParseError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + build_rrule(s) + } +} diff --git a/src/rruleset.rs b/src/rruleset.rs index 4bea086..e8ddf11 100644 --- a/src/rruleset.rs +++ b/src/rruleset.rs @@ -1,8 +1,11 @@ use crate::datetime::DTime; +use crate::options::RRuleParseError; use crate::rrule::RRule; use crate::rruleset_iter::RRuleSetIter; +use crate::rrulestr::build_rruleset; use chrono::prelude::*; use chrono_tz::{Tz, UTC}; +use std::str::FromStr; #[derive(Debug)] pub struct RRuleSet { @@ -87,6 +90,14 @@ impl RRuleSet { } } +impl FromStr for RRuleSet { + type Err = RRuleParseError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + build_rruleset(s) + } +} + #[cfg(test)] mod test_iter_set { use super::*; diff --git a/src/rrulestr.rs b/src/rrulestr.rs index 62a2e29..b798275 100644 --- a/src/rrulestr.rs +++ b/src/rrulestr.rs @@ -22,7 +22,6 @@ static EXDATE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)EXDATE(?:;TZID=([^ static DATETIME_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)(VALUE=DATE(-TIME)?)|(TZID=)").unwrap()); - fn parse_datestring_bit<T: FromStr>( bits: ®ex::Captures, i: usize, @@ -106,7 +105,11 @@ fn stringval_to_int<T: FromStr>(val: &str, err_msg: String) -> Result<T, RRulePa } } -fn stringval_to_intvec<T: FromStr + Ord + PartialEq + Copy, F: Fn(T) -> bool>(val: &str, accept: F, err_msg: String) -> Result<Vec<T>, RRuleParseError> { +fn stringval_to_intvec<T: FromStr + Ord + PartialEq + Copy, F: Fn(T) -> bool>( + val: &str, + accept: F, + err_msg: String, +) -> Result<Vec<T>, RRuleParseError> { let mut parsed_vals = vec![]; for val in val.split(",") { let val = stringval_to_int(val, err_msg.clone())?; @@ -168,35 +171,58 @@ fn parse_rrule(line: &str) -> Result<Options, RRuleParseError> { options.interval = Some(interval); } "BYSETPOS" => { - let bysetpos = stringval_to_intvec(value, |_pos| true, format!("Invalid bysetpos value"))?; + let bysetpos = + stringval_to_intvec(value, |_pos| true, format!("Invalid bysetpos value"))?; options.bysetpos = Some(bysetpos); } "BYMONTH" => { - let bymonth = stringval_to_intvec(value, |month| month <= 11, format!("Invalid bymonth value"))?; + let bymonth = stringval_to_intvec( + value, + |month| month <= 11, + format!("Invalid bymonth value"), + )?; options.bymonth = Some(bymonth); } "BYMONTHDAY" => { - let bymonthday = stringval_to_intvec(value, |monthday| monthday >= 0 && monthday <= 31, format!("Invalid bymonthday value"))?; + let bymonthday = stringval_to_intvec( + value, + |monthday| monthday >= 0 && monthday <= 31, + format!("Invalid bymonthday value"), + )?; options.bymonthday = Some(bymonthday); } "BYYEARDAY" => { - let byyearday = stringval_to_intvec(value, |yearday| yearday >= -366 && yearday <= 366, format!("Invalid byyearday value"))?; + let byyearday = stringval_to_intvec( + value, + |yearday| yearday >= -366 && yearday <= 366, + format!("Invalid byyearday value"), + )?; options.byyearday = Some(byyearday); } "BYWEEKNO" => { - let byweekno = stringval_to_intvec(value, |weekno| weekno >= 0 && weekno <= 53, format!("Invalid byweekno value"))?; + let byweekno = stringval_to_intvec( + value, + |weekno| weekno >= 0 && weekno <= 53, + format!("Invalid byweekno value"), + )?; options.byweekno = Some(byweekno); } "BYHOUR" => { - let byhour = stringval_to_intvec(value, |hour| hour < 24, format!("Invalid byhour value"))?; + let byhour = + stringval_to_intvec(value, |hour| hour < 24, format!("Invalid byhour value"))?; options.byhour = Some(byhour); } "BYMINUTE" => { - let byminute = stringval_to_intvec(value, |minute| minute < 60, format!("Invalid byminute value"))?; + let byminute = stringval_to_intvec( + value, + |minute| minute < 60, + format!("Invalid byminute value"), + )?; options.byminute = Some(byminute); } "BYSECOND" => { - let bysecond = stringval_to_intvec(value, |sec| sec < 60, format!("Invalid bysecond value"))?; + let bysecond = + stringval_to_intvec(value, |sec| sec < 60, format!("Invalid bysecond value"))?; options.bysecond = Some(bysecond); } "BYWEEKDAY" | "BYDAY" => { @@ -275,7 +301,12 @@ fn parse_line(rfc_string: &str) -> Result<Option<Options>, RRuleParseError> { let header = header.unwrap(); let key = match header.get(1) { Some(k) => k.as_str(), - None => return Err(RRuleParseError(format!("Invalid rfc_string: {}", rfc_string))) + None => { + return Err(RRuleParseError(format!( + "Invalid rfc_string: {}", + rfc_string + ))) + } }; match key { @@ -345,7 +376,6 @@ fn parse_string(rfc_string: &str) -> Result<Options, RRuleParseError> { Ok(Options::concat(&options[0], &options[1])) } - #[derive(Debug)] struct ParsedInput { rrule_vals: Vec<Options>, @@ -391,7 +421,7 @@ fn parse_input(s: &str) -> Result<ParsedInput, RRuleParseError> { "RDATE" => { let matches = match RDATE_RE.captures(line) { Some(m) => m, - None => return Err(RRuleParseError(format!("Invalid RDATE specified"))) + None => return Err(RRuleParseError(format!("Invalid RDATE specified"))), }; let mut tz = UTC; if let Some(tzid) = matches.get(1) { @@ -407,7 +437,7 @@ fn parse_input(s: &str) -> Result<ParsedInput, RRuleParseError> { "EXDATE" => { let matches = match EXDATE_RE.captures(line) { Some(m) => m, - None => return Err(RRuleParseError(format!("Invalid EXDATE specified"))) + None => return Err(RRuleParseError(format!("Invalid EXDATE specified"))), }; let tz: Tz = if let Some(tzid) = matches.get(1) { String::from(tzid.as_str()).parse().unwrap_or(UTC) @@ -444,10 +474,12 @@ fn validate_date_param(params: Vec<&str>) -> Result<(), RRuleParseError> { for param in ¶ms { match DATETIME_RE.captures(param) { Some(caps) if caps.len() > 0 => (), - _ => return Err(RRuleParseError(format!( - "Unsupported RDATE/EXDATE parm: {}", - param - ))) + _ => { + return Err(RRuleParseError(format!( + "Unsupported RDATE/EXDATE parm: {}", + param + ))) + } } } Ok(()) @@ -460,7 +492,7 @@ fn parse_rdate( ) -> Result<Vec<DTime>, RRuleParseError> { let params: Vec<&str> = params.iter().map(|p| p.as_str()).collect(); validate_date_param(params)?; - + let mut rdatevals = vec![]; for datestr in rdateval.split(",") { rdatevals.push(datestring_to_date(datestr, tz)?); @@ -612,8 +644,8 @@ mod test { assert!(res.is_err()); assert_eq!(res.err().unwrap().0, "Invalid byhour value"); - - let res = build_rruleset("DTSTART:20120201T120000Z\nRRULE:FREQ=DAILY;COUNT=5;BYHOUR=5,6,25"); + let res = + build_rruleset("DTSTART:20120201T120000Z\nRRULE:FREQ=DAILY;COUNT=5;BYHOUR=5,6,25"); assert!(res.is_err()); assert_eq!(res.err().unwrap().0, "Invalid byhour value"); } @@ -624,8 +656,8 @@ mod test { assert!(res.is_err()); assert_eq!(res.err().unwrap().0, "Invalid byminute value"); - - let res = build_rruleset("DTSTART:20120201T120000Z\nRRULE:FREQ=DAILY;COUNT=5;BYMINUTE=4,5,64"); + let res = + build_rruleset("DTSTART:20120201T120000Z\nRRULE:FREQ=DAILY;COUNT=5;BYMINUTE=4,5,64"); assert!(res.is_err()); assert_eq!(res.err().unwrap().0, "Invalid byminute value"); } diff --git a/src/iter/utils.rs b/src/utils.rs index a134d9a..ef09d54 100644 --- a/src/iter/utils.rs +++ b/src/utils.rs @@ -6,3 +6,10 @@ pub fn pymod(a: isize, b: isize) -> isize { } r } + +pub fn is_some_and_not_empty<T>(v: &Option<Vec<T>>) -> bool { + match v { + Some(v) => !v.is_empty(), + None => false, + } +} |