diff options
Diffstat (limited to 'src/rrulestr.rs')
-rw-r--r-- | src/rrulestr.rs | 524 |
1 files changed, 524 insertions, 0 deletions
diff --git a/src/rrulestr.rs b/src/rrulestr.rs new file mode 100644 index 0000000..607513f --- /dev/null +++ b/src/rrulestr.rs @@ -0,0 +1,524 @@ +use crate::options::*; +use crate::rrule::RRule; +use crate::rruleset::RRuleSet; +use chrono::prelude::*; +use chrono::DateTime; +use chrono_tz::Tz; +use once_cell::sync::Lazy; +use regex::Regex; + +static DATESTR_RE: Lazy<Regex> = + Lazy::new(|| Regex::new(r"(?m)^(\d{4})(\d{2})(\d{2})(T(\d{2})(\d{2})(\d{2})Z?)?$").unwrap()); +static DTSTART_RE: Lazy<Regex> = + Lazy::new(|| Regex::new(r"(?m)DTSTART(?:;TZID=([^:=]+?))?(?::|=)([^;\s]+)").unwrap()); + +static RRUle_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^(?:RRULE|EXRULE):").unwrap()); + +#[derive(Debug, Clone)] +pub struct PartialOptions { + pub freq: Option<Frequenzy>, + pub interval: Option<usize>, + pub count: Option<u32>, + pub until: Option<DateTime<Utc>>, + pub tzid: Option<String>, + pub dtstart: Option<DateTime<Utc>>, + pub wkst: Option<usize>, + pub bysetpos: Option<Vec<isize>>, + pub bymonth: Option<Vec<usize>>, + pub bymonthday: Option<Vec<isize>>, + pub bynmonthday: Option<Vec<isize>>, + pub byyearday: Option<Vec<isize>>, + pub byweekno: Option<Vec<isize>>, + pub byweekday: Option<Vec<usize>>, + pub byhour: Option<Vec<usize>>, + pub byminute: Option<Vec<usize>>, + pub bysecond: Option<Vec<usize>>, + pub bynweekday: Option<Vec<Vec<isize>>>, + pub byeaster: Option<isize>, +} + +impl PartialOptions { + pub fn new() -> Self { + Self { + freq: None, + interval: None, + count: None, + until: None, + tzid: None, + dtstart: None, + wkst: None, + bysetpos: None, + bymonth: None, + bymonthday: None, + bynmonthday: None, + byyearday: None, + byweekno: None, + byweekday: None, + byhour: None, + byminute: None, + bysecond: None, + bynweekday: None, + byeaster: None, + } + } + + fn is_some_or_none<'a, T>(prop1: &'a Option<T>, prop2: &'a Option<T>) -> &'a Option<T> { + if prop2.is_some() { + return prop2; + } + prop1 + } + + pub fn concat(opt1: &Self, opt2: &Self) -> Self { + Self { + freq: Self::is_some_or_none(&opt1.freq, &opt2.freq).clone(), + interval: Self::is_some_or_none(&opt1.interval, &opt2.interval).clone(), + count: Self::is_some_or_none(&opt1.count, &opt2.count).clone(), + until: Self::is_some_or_none(&opt1.until, &opt2.until).clone(), + tzid: Self::is_some_or_none(&opt1.tzid, &opt2.tzid).clone(), + dtstart: Self::is_some_or_none(&opt1.dtstart, &opt2.dtstart).clone(), + wkst: Self::is_some_or_none(&opt1.wkst, &opt2.wkst).clone(), + bysetpos: Self::is_some_or_none(&opt1.bysetpos, &opt2.bysetpos).clone(), + bymonth: Self::is_some_or_none(&opt1.bymonth, &opt2.bymonth).clone(), + bymonthday: Self::is_some_or_none(&opt1.bymonthday, &opt2.bymonthday).clone(), + bynmonthday: Self::is_some_or_none(&opt1.bynmonthday, &opt2.bynmonthday).clone(), + byyearday: Self::is_some_or_none(&opt1.byyearday, &opt2.byyearday).clone(), + byweekno: Self::is_some_or_none(&opt1.byweekno, &opt2.byweekno).clone(), + byweekday: Self::is_some_or_none(&opt1.byweekday, &opt2.byweekday).clone(), + byhour: Self::is_some_or_none(&opt1.byhour, &opt2.byhour).clone(), + byminute: Self::is_some_or_none(&opt1.byminute, &opt2.byminute).clone(), + bysecond: Self::is_some_or_none(&opt1.bysecond, &opt2.bysecond).clone(), + bynweekday: Self::is_some_or_none(&opt1.bynweekday, &opt2.bynweekday).clone(), + byeaster: Self::is_some_or_none(&opt1.byeaster, &opt2.byeaster).clone(), + } + } +} + +fn datestring_to_date(dt: &str) -> DateTime<Utc> { + let bits = DATESTR_RE.captures(dt).unwrap(); + return Utc + .ymd( + bits.get(1).unwrap().as_str().parse::<i32>().unwrap(), + bits.get(2).unwrap().as_str().parse::<u32>().unwrap(), + bits.get(3).unwrap().as_str().parse::<u32>().unwrap(), + ) + .and_hms( + bits.get(5).unwrap().as_str().parse::<u32>().unwrap(), + bits.get(6).unwrap().as_str().parse::<u32>().unwrap(), + bits.get(7).unwrap().as_str().parse::<u32>().unwrap(), + ); +} + +fn parse_dtstart(s: &str) -> Option<PartialOptions> { + let caps = DTSTART_RE.captures(s); + println!("captures -----------------------"); + println!("{:?}", caps); + + match caps { + Some(caps) => { + let mut options = PartialOptions::new(); + options.dtstart = Some(datestring_to_date(caps.get(2).unwrap().as_str())); + options.tzid = if let Some(tzid) = caps.get(1) { + Some(String::from(tzid.as_str())) + } else { + Some(String::from("UTC")) + }; + Some(options) + } + None => None, + } +} + +fn from_str_to_freq(s: &str) -> Option<Frequenzy> { + match s.to_uppercase().as_str() { + "YEARLY" => Some(Frequenzy::YEARLY), + "MONTHLY" => Some(Frequenzy::MONTHLY), + "WEEKLY" => Some(Frequenzy::WEEKLY), + "DAILY" => Some(Frequenzy::DAILY), + "HOURLY" => Some(Frequenzy::HOURLY), + "MINUTELY" => Some(Frequenzy::MINUTELY), + "SECONDLY" => Some(Frequenzy::SECONDLY), + _ => None, + } +} + +fn parse_rrule(line: &str) -> PartialOptions { + let stripped_line = if line.starts_with("RRULE:") { + &line[6..] + } else { + line + }; + + + let mut options = parse_dtstart(stripped_line).unwrap_or(PartialOptions::new()); + + let attrs = RRUle_RE.replace(line, ""); + let attrs = attrs.split(";"); + + for attr in attrs { + println!("Attr: {}", attr); + let l: Vec<&str> = attr.split("=").collect(); + + let key = l[0]; + let mut value = ""; + if l.len() > 1 { + value = l[1]; + } + println!("Ket: {}", key); + match key.to_uppercase().as_str() { + "FREQ" => { + options.freq = Some(from_str_to_freq(value).unwrap()); + } + "WKST" => { + options.wkst = Some(value.parse::<usize>().unwrap()); + } + "COUNT" => { + options.count = Some(value.parse::<u32>().unwrap()); + } + "INTERVAL" => { + options.interval = Some(value.parse::<usize>().unwrap()); + } + "BYSETPOS" => { + let bysetpos = value.split(",").map(|d| d.parse::<isize>().unwrap()).collect(); + options.bysetpos = Some(bysetpos); + } + "BYMONTH" => { + let bymonth = value.split(",").map(|d| d.parse::<usize>().unwrap()).collect(); + options.bymonth = Some(bymonth); + } + "BYMONTHDAY" => { + let bymonthday = value.split(",").map(|d| d.parse::<isize>().unwrap()).collect(); + options.bymonthday = Some(bymonthday); + } + "BYYEARDAY" => { + let byyearday = value.split(",").map(|d| d.parse::<isize>().unwrap()).collect(); + options.byyearday = Some(byyearday); + } + "BYWEEKNO" => { + let byweekno = value.split(",").map(|d| d.parse::<isize>().unwrap()).collect(); + options.byweekno = Some(byweekno); + } + "BYHOUR" => { + let byhour = value.split(",").map(|d| d.parse::<usize>().unwrap()).collect(); + options.byhour = Some(byhour); + } + "BYMINUTE" => { + let byminute = value.split(",").map(|d| d.parse::<usize>().unwrap()).collect(); + options.byminute = Some(byminute); + } + "BYSECOND" => { + let bysecond = value.split(",").map(|d| d.parse::<usize>().unwrap()).collect(); + options.bysecond = Some(bysecond); + } + "BYWEEKDAY" | "BYDAY" => { + options.byweekday = Some(parse_weekday(value)); + } + "DTSTART" | "TZID" => { + // for backwards compatibility + let dtstart_opts = parse_dtstart(line).unwrap(); + println!("After parsing tzid"); + println!("{:?}", dtstart_opts); + options.tzid = Some(dtstart_opts.tzid.unwrap()); + options.dtstart = Some(dtstart_opts.dtstart.unwrap()); + } + "UNTIL" => { + options.until = Some(datestring_to_date(value)); + } + "BYEASTER" => { + options.byeaster = Some(value.parse::<isize>().unwrap()); + } + _ => panic!("Invalid property: {}", key), + }; + } + + options +} + +fn str_to_weekday(d: &str) -> usize { + match d.to_uppercase().as_str() { + "MO" => 0, + "TU" => 1, + "WE" => 2, + "TH" => 3, + "FR" => 4, + "SA" => 5, + "SU" => 6, + _ => panic!("Invalid weekday: {}", d), + } +} + +fn parse_weekday(val: &str) -> Vec<usize> { + val.split(",").map(|day| { + if day.len() == 2 { + // MO, TU, ... + return str_to_weekday(day); + } + + // ! 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() +} + +fn parse_line(rfc_string: &str) -> Option<PartialOptions> { + let re = Regex::new(r"(?m)^\s+|\s+$").unwrap(); + let rfc_string = re.replace(rfc_string, ""); + if rfc_string.is_empty() { + return None; + } + + let re = Regex::new(r"(?m)^([A-Z]+?)[:;]").unwrap(); + let rfc_string_upper = rfc_string.to_uppercase(); + let header = re.captures(&rfc_string_upper); + + + + let rfc_string = rfc_string.to_string(); + if header.is_none() { + return Some(parse_rrule(&rfc_string)); + } + let header = header.unwrap(); + let key = header.get(1).unwrap().as_str(); + + match key { + "EXRULE" | "RRULE" => Some(parse_rrule(&rfc_string)), + "DTSTART" => Some(parse_dtstart(&rfc_string).unwrap()), + _ => panic!("Unsupported RFC prop {} in {}", key, &rfc_string) + } +} + +#[derive(Debug)] +struct ParsedLine { + name: String, + params: Vec<String>, + value: String +} + +fn break_down_line(line: &str) -> ParsedLine { + let parsed_line_name = extract_name(String::from(line)); + let params: Vec<&str> = parsed_line_name.name.split(";").collect(); + + ParsedLine { + name: params[0].to_uppercase(), + params: params[1..].iter().map(|s| String::from(*s)).collect(), + value: String::from(parsed_line_name.value) + } +} + +struct LineName { + name: String, + value: String +} + +fn extract_name(line: String) -> LineName { + if !line.contains(":") { + return LineName { + name: String::from("RRULE"), + value: line + }; + } + + let parts: Vec<&str> = line.split(":").collect(); + let name = parts[0]; + let value = parts[1..].join(""); + + LineName { + name: String::from(name), + value + } +} + +fn parse_string(rfc_string: &str) -> PartialOptions { + let options: Vec<PartialOptions> = rfc_string.split("\n").map(|l| parse_line(l)).filter(|x| x.is_some()) + .map(|o| o.unwrap()) + .collect(); + + if options.len() == 1 { + println!("Options i got from str: {}", rfc_string); + println!("{:?}", options[0]); + return options[0].clone(); + } + + PartialOptions::concat(&options[0], &options[1]) +} + +#[derive(Debug)] +struct ParsedInput { + rrule_vals: Vec<PartialOptions>, + rdate_vals: Vec<DateTime<Utc>>, + exrule_vals: Vec<PartialOptions>, + exdate_vals: Vec<DateTime<Utc>>, + dtstart: Option<DateTime<Utc>>, + tzid: Option<String>, +} + +fn parse_input(s: &str) -> ParsedInput { + let mut rrule_vals = vec![]; + let mut rdate_vals = vec![]; + let mut exrule_vals = vec![]; + let mut exdate_vals = vec![]; + + let PartialOptions { + dtstart, + mut tzid, + .. + } = parse_dtstart(s).unwrap(); + + + let mut lines: Vec<&str> = s.split("\n").collect(); + for line in &lines { + let parsed_line = break_down_line(line); + println!("Parsed line: {:?}", parsed_line); + + match parsed_line.name.to_uppercase().as_str() { + "RRULE" => { + if !parsed_line.params.is_empty() { + panic!("Unsupported RRULE value"); + } + if parsed_line.value.is_empty() { + continue; + } + + rrule_vals.push(parse_string(line)); + } + "RDATE" => { + let re = Regex::new(r"(?m)RDATE(?:;TZID=([^:=]+))?").unwrap(); + let matches = re.captures(line).unwrap(); + if tzid.is_none() && matches.get(1).is_some() { + tzid = Some(String::from(matches.get(1).unwrap().as_str())); + } + + rdate_vals.append(&mut parse_rdate(&parsed_line.value, parsed_line.params)); + } + "EXRULE" => { + exrule_vals.push(parse_string(&parsed_line.value)); + } + "EXDATE" => { + exdate_vals.append(&mut parse_rdate(&parsed_line.value, parsed_line.params)); + } + "DTSTART" => (), + _ => panic!("Unsupported property: {}", parsed_line.name) + } + } + + return ParsedInput { + dtstart, + tzid, + // fn it_works_2() { + // let options = build_rule("DTSTART:19970902T090000Z\nRRULE:FREQ=YEARLY;COUNT=3\n"); + // println!("?????????????=================?????????????"); + // println!("{:?}", options); + // } + // fn it_works_2() { + // let options = build_rule("DTSTART:19970902T090000Z\nRRULE:FREQ=YEARLY;COUNT=3\n"); + // println!("?????????????=================?????????????"); + // println!("{:?}", options); + // } + rrule_vals, + rdate_vals, + exrule_vals, + exdate_vals + } +} + +fn validate_date_param(params: Vec<&str>){ + let re = Regex::new(r"(?m)(VALUE=DATE(-TIME)?)|(TZID=)").unwrap(); + + for param in ¶ms { + if re.captures(param).unwrap().len() == 0 { + panic!("Unsupported RDATE/EXDATE parm: {}", param); + } + } +} + +fn parse_rdate(rdateval: &str, params: Vec<String>) -> Vec<DateTime<Utc>> { + let params: Vec<&str> = params.iter().map(|p| p.as_str()).collect(); + validate_date_param(params); + + rdateval.split(",").map(|datestr| datestring_to_date(datestr)).collect() +} + +fn build_rule(s: &str) -> RRuleSet { + let ParsedInput { + mut rrule_vals, + rdate_vals, + mut exrule_vals, + exdate_vals, + dtstart, + tzid, + .. + } = parse_input(s); + + let mut rset = RRuleSet::new(); + if rrule_vals.len() > 1 || + !rdate_vals.is_empty() || + !exrule_vals.is_empty() || + !exdate_vals.is_empty() { + + rset.dtstart = dtstart; + // rset.tzid(tzid || undefined); + + for rruleval in rrule_vals.iter_mut() { + rruleval.tzid = tzid.clone(); + rruleval.dtstart = dtstart; + + // let rrule = RRule::new(rruleval.clone()); + // rset.rrule(rrule); + } + + for rdate in rdate_vals { + rset.rdate(rdate); + } + + for exrule in exrule_vals.iter_mut() { + exrule.tzid = tzid.clone(); + exrule.dtstart = dtstart; + + // let exrule = RRule::new(exrule.clone()); + // rset.rrule(exrule); + } + + for exdate in exdate_vals { + rset.exdate(exdate); + } + + // if (options.compatible && options.dtstart) rset.rdate(dtstart!); + return rset; + } + + + rset +} + + +#[cfg(test)] +mod test { + use super::*; + + // #[test] + // fn it_works_2() { + // let options = build_rule("DTSTART:19970902T090000Z\nRRULE:FREQ=YEARLY;COUNT=3\n"); + // println!("?????????????=================?????????????"); + // println!("{:?}", options); + // } + + // #[test] + // fn it_works() { + // let options = build_rule("RRULE:UNTIL=19990404T110000Z;DTSTART=19990104T110000Z;FREQ=WEEKLY;BYDAY=TU,WE"); + // println!("?????????????=================?????????????"); + // println!("{:?}", options); + // } + + #[test] + fn it_works() { + let options = build_rule("RRULE:UNTIL=19990404T110000Z;DTSTART;TZID=America/New_York:19990104T110000Z;FREQ=WEEKLY;BYDAY=TU,WE"); + println!("?????????????=================?????????????"); + println!("{:?}", options); + } +} |