use chrono::TimeDelta; use nom::branch::alt; use nom::bytes::complete::{tag, tag_no_case}; use nom::character::complete as nomchar; use nom::combinator::{map, map_opt, opt, value}; use nom::sequence::{pair, tuple}; use nom::IResult; use aero_dav::caltypes as cal; //@FIXME too simple, we have 4 cases in practices: // - floating datetime // - floating datetime with a tzid as param so convertible to tz datetime // - utc datetime // - floating(?) date (without time) pub fn date_time(dt: &str) -> Option> { tracing::trace!(raw_time = dt, "VEVENT raw time"); let tmpl = match dt.chars().last() { Some('Z') => cal::UTC_DATETIME_FMT, Some(_) => { tracing::warn!( raw_time = dt, "floating datetime is not properly supported yet" ); cal::FLOATING_DATETIME_FMT } None => return None, }; chrono::NaiveDateTime::parse_from_str(dt, tmpl) .ok() .map(|v| v.and_utc()) } /// RFC3389 Duration Value /// /// ```abnf /// dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week) /// dur-date = dur-day [dur-time] /// dur-time = "T" (dur-hour / dur-minute / dur-second) /// dur-week = 1*DIGIT "W" /// dur-hour = 1*DIGIT "H" [dur-minute] /// dur-minute = 1*DIGIT "M" [dur-second] /// dur-second = 1*DIGIT "S" /// dur-day = 1*DIGIT "D" /// ``` pub fn dur_value(text: &str) -> IResult<&str, TimeDelta> { map_opt( tuple(( dur_sign, tag_no_case("P"), alt((dur_date, dur_time, dur_week)), )), |(sign, _, delta)| delta.checked_mul(sign), )(text) } fn dur_sign(text: &str) -> IResult<&str, i32> { map(opt(alt((value(1, tag("+")), value(-1, tag("-"))))), |x| { x.unwrap_or(1) })(text) } fn dur_date(text: &str) -> IResult<&str, TimeDelta> { map(pair(dur_day, opt(dur_time)), |(day, time)| { day + time.unwrap_or(TimeDelta::zero()) })(text) } fn dur_time(text: &str) -> IResult<&str, TimeDelta> { map( pair(tag_no_case("T"), alt((dur_hour, dur_minute, dur_second))), |(_, x)| x, )(text) } fn dur_week(text: &str) -> IResult<&str, TimeDelta> { map_opt(pair(nomchar::i64, tag_no_case("W")), |(i, _)| { TimeDelta::try_weeks(i) })(text) } fn dur_day(text: &str) -> IResult<&str, TimeDelta> { map_opt(pair(nomchar::i64, tag_no_case("D")), |(i, _)| { TimeDelta::try_days(i) })(text) } fn dur_hour(text: &str) -> IResult<&str, TimeDelta> { map_opt( tuple((nomchar::i64, tag_no_case("H"), opt(dur_minute))), |(i, _, mm)| TimeDelta::try_hours(i).map(|hours| hours + mm.unwrap_or(TimeDelta::zero())), )(text) } fn dur_minute(text: &str) -> IResult<&str, TimeDelta> { map_opt( tuple((nomchar::i64, tag_no_case("M"), opt(dur_second))), |(i, _, ms)| TimeDelta::try_minutes(i).map(|min| min + ms.unwrap_or(TimeDelta::zero())), )(text) } fn dur_second(text: &str) -> IResult<&str, TimeDelta> { map_opt(pair(nomchar::i64, tag_no_case("S")), |(i, _)| { TimeDelta::try_seconds(i) })(text) } #[cfg(test)] mod tests { use super::*; #[test] fn rfc5545_example1() { // A duration of 15 days, 5 hours, and 20 seconds would be: let to_parse = "P15DT5H0M20S"; let (_, time_delta) = dur_value(to_parse).unwrap(); assert_eq!( time_delta, TimeDelta::try_days(15).unwrap() + TimeDelta::try_hours(5).unwrap() + TimeDelta::try_seconds(20).unwrap() ); } #[test] fn rfc5545_example2() { // A duration of 7 weeks would be: let to_parse = "P7W"; let (_, time_delta) = dur_value(to_parse).unwrap(); assert_eq!(time_delta, TimeDelta::try_weeks(7).unwrap()); } #[test] fn rfc4791_example1() { // 10 minutes before let to_parse = "-PT10M"; let (_, time_delta) = dur_value(to_parse).unwrap(); assert_eq!(time_delta, TimeDelta::try_minutes(-10).unwrap()); } #[test] fn ical_org_example1() { // The following example is for a "VALARM" calendar component that specifies an email alarm // that will trigger 2 days before the scheduled due DATE-TIME of a to-do with which it is associated. let to_parse = "-P2D"; let (_, time_delta) = dur_value(to_parse).unwrap(); assert_eq!(time_delta, TimeDelta::try_days(-2).unwrap()); } }