aboutsummaryrefslogblamecommitdiff
path: root/aero-ical/src/parser.rs
blob: ca271a53dafc42a965db3289770a02cc466dbb50 (plain) (tree)
1
2
3
4
5
6
7
8

                      

                                             
                                        


                                                












                                                                         



                                                                 
                                      

                            



















                                                                   







                                                   


                                               


                                                                  

                                                     


                                                     

                                                     



                                                                        

                                                     


                                                            

                                                    


                                                            

                                                     



                                                                                                  

                                                       



                                                                                                

                                                       


                                                            











                                                                   




                                                     






                                                           
                                                                 


           
                           



                                                           
                                                                     

     

                            
                                                                                                   



                                                                                                              
                                                                 

     
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<chrono::DateTime<chrono::Utc>> {
    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());
    }
}