rrules 0.2.3

A library for working with recurrence rules
Documentation
use crate::RecurrenceInvalid;
use chrono::{DateTime, Duration, Utc};
use std::str::FromStr;

pub fn extract_start_date(s: &str) -> Result<DateTime<Utc>, RecurrenceInvalid> {
    use regex::Regex;
    let re =
        Regex::new(r"DTSTART=(?P<date>[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z)")
            .unwrap();
    let caps = re.captures(s).ok_or(RecurrenceInvalid {
        message: "No DTSTART found".to_string(),
    })?;
    let date = caps.name("date").ok_or(RecurrenceInvalid {
        message: "No date found".to_string(),
    })?;
    let date = DateTime::<Utc>::from_str(date.as_str()).map_err(|e| RecurrenceInvalid {
        message: format!("Invalid date: {e}"),
    })?;
    Ok(date)
}

pub fn extract_end_date(s: &str) -> Result<Option<DateTime<Utc>>, RecurrenceInvalid> {
    use regex::Regex;
    let re = Regex::new(r"DTEND=(?P<date>[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z)")
        .unwrap();
    let caps = match re.captures(s).ok_or(RecurrenceInvalid {
        message: "No DTEND found".to_string(),
    }) {
        Ok(caps) => caps,
        Err(_) => return Ok(None),
    };
    let date = caps.name("date").ok_or(RecurrenceInvalid {
        message: "No date found".to_string(),
    })?;
    let date = DateTime::<Utc>::from_str(date.as_str()).map_err(|e| RecurrenceInvalid {
        message: format!("Invalid date: {e}"),
    })?;
    Ok(Some(date))
}

pub fn extract_duration(s: &str) -> Result<Duration, RecurrenceInvalid> {
    let seconds = extract_seconds_duration(s).unwrap_or(Duration::seconds(0));
    let minutes = extract_minutes_duration(s).unwrap_or(Duration::minutes(0));
    let hours = extract_hours_duration(s).unwrap_or(Duration::hours(0));
    let days = extract_days_duration(s).unwrap_or(Duration::days(0));
    let weeks = extract_weeks_duration(s).unwrap_or(Duration::weeks(0));
    let duration = seconds + minutes + hours + days + weeks;
    Ok(duration)
}

pub fn extract_seconds_duration(s: &str) -> Result<Duration, RecurrenceInvalid> {
    use regex::Regex;
    let re = Regex::new(r"DURATION=PT(?P<seconds>[0-9]+)S").unwrap();
    let caps = re.captures(s).ok_or(RecurrenceInvalid {
        message: "No DURATION found".to_string(),
    })?;
    let seconds = parse_duration_pair(&caps, "seconds");
    let seconds = seconds.parse::<i64>().map_err(|e| RecurrenceInvalid {
        message: format!("Invalid seconds: {e}"),
    })?;
    let duration = Duration::seconds(seconds);
    Ok(duration)
}

pub fn extract_minutes_duration(s: &str) -> Result<Duration, RecurrenceInvalid> {
    use regex::Regex;
    let re = Regex::new(r"DURATION=PT(?P<minutes>[0-9]+)M").unwrap();
    let caps = re.captures(s).ok_or(RecurrenceInvalid {
        message: "No DURATION found".to_string(),
    })?;
    let minutes = parse_duration_pair(&caps, "minutes");
    let minutes = minutes.parse::<i64>().map_err(|e| RecurrenceInvalid {
        message: format!("Invalid minutes: {e}"),
    })?;
    let duration = Duration::minutes(minutes);
    Ok(duration)
}

pub fn extract_hours_duration(s: &str) -> Result<Duration, RecurrenceInvalid> {
    use regex::Regex;
    let re = Regex::new(r"DURATION=PT(?P<hours>[0-9]+)H").unwrap();
    let caps = re.captures(s).ok_or(RecurrenceInvalid {
        message: "No DURATION found".to_string(),
    })?;
    let hours = parse_duration_pair(&caps, "hours");
    let hours = hours.parse::<i64>().map_err(|e| RecurrenceInvalid {
        message: format!("Invalid hours: {e}"),
    })?;
    let duration = Duration::hours(hours);
    Ok(duration)
}

pub fn extract_days_duration(s: &str) -> Result<Duration, RecurrenceInvalid> {
    use regex::Regex;
    let re = Regex::new(r"DURATION=P(?P<days>[0-9]+)D").unwrap();
    let caps = re.captures(s).ok_or(RecurrenceInvalid {
        message: "No DURATION found".to_string(),
    })?;
    let days = parse_duration_pair(&caps, "days");
    let days = days.parse::<i64>().map_err(|e| RecurrenceInvalid {
        message: format!("Invalid days: {e}"),
    })?;
    let duration = Duration::days(days);
    Ok(duration)
}

pub fn extract_weeks_duration(s: &str) -> Result<Duration, RecurrenceInvalid> {
    use regex::Regex;
    let re = Regex::new(r"DURATION=P(?P<weeks>[0-9]+)W").unwrap();
    let caps = re.captures(s).ok_or(RecurrenceInvalid {
        message: "No DURATION found".to_string(),
    })?;
    let weeks = parse_duration_pair(&caps, "weeks");
    let weeks = weeks.parse::<i64>().map_err(|e| RecurrenceInvalid {
        message: format!("Invalid weeks: {e}"),
    })?;
    let duration = Duration::weeks(weeks);
    Ok(duration)
}

fn parse_duration_pair<'a>(caps: &'a regex::Captures, key: &'a str) -> &'a str {
    if let Some(d) = caps.name(key) {
        d.as_str()
    } else {
        ""
    }
}

#[cfg(test)]
mod test_helpers {
    use crate::recurrences::serializers::helpers::{extract_duration, extract_start_date};
    use chrono::{DateTime, Duration, Utc};
    use std::str::FromStr;

    #[test]
    fn test_extract_start() {
        let value = "FREQ=SECONDLY;INTERVAL=1;COUNT=10;DTSTART=2020-01-01T00:00:00Z";
        let expected = DateTime::<Utc>::from_str("2020-01-01T00:00:00Z").unwrap();
        let actual = extract_start_date(&value).unwrap();
        assert_eq!(actual, expected);
    }

    #[test]
    fn test_extract_start_no_dtstart() {
        let value = "FREQ=SECONDLY;INTERVAL=1;COUNT=10";
        let result = extract_start_date(&value);
        assert!(result.is_err());
    }

    #[test]
    fn test_extract_start_no_date() {
        let value = "FREQ=SECONDLY;INTERVAL=1;COUNT=10;DTSTART=";
        let result = extract_start_date(&value);
        assert!(result.is_err());
    }

    #[test]
    fn test_extract_start_invalid_date() {
        let value = "FREQ=SECONDLY;INTERVAL=1;COUNT=10;DTSTART=2020-01-01T00:00:00";
        let result = extract_start_date(&value);
        assert!(result.is_err());
    }

    #[test]
    fn test_extract_duration_no_duration() {
        let value = "FREQ=SECONDLY;INTERVAL=1;COUNT=10;DTSTART=2020-01-01T00:00:00Z";
        let result = extract_duration(&value);
        assert!(result.is_ok());
        let duration = result.unwrap();
        assert_eq!(duration, Duration::seconds(0));
    }

    #[test]
    fn test_extract_duration_invalid_returns_0() {
        let value = "FREQ=SECONDLY;INTERVAL=1;COUNT=10;DTSTART=2020-01-01T00:00:00Z;DURATION=PT1";
        let result = extract_duration(&value);
        assert!(result.is_ok());
        let duration = result.unwrap();
        assert_eq!(duration, Duration::seconds(0));
    }

    #[test]
    fn test_extract_seconds_duration() {
        let value = "FREQ=SECONDLY;INTERVAL=1;COUNT=10;DTSTART=2020-01-01T00:00:00Z;DURATION=PT1S";
        let actual = extract_duration(&value).unwrap();
        let expected = Duration::seconds(1);
        assert_eq!(actual, expected);
    }

    #[test]
    fn test_extract_minutes_duration() {
        let value = "FREQ=SECONDLY;INTERVAL=1;COUNT=10;DTSTART=2020-01-01T00:00:00Z;DURATION=PT1M";
        let actual = extract_duration(&value).unwrap();
        let expected = Duration::minutes(1);
        assert_eq!(actual, expected);
    }

    #[test]
    fn test_extract_hours_duration() {
        let value = "FREQ=SECONDLY;INTERVAL=1;COUNT=10;DTSTART=2020-01-01T00:00:00Z;DURATION=PT1H";
        let actual = extract_duration(&value).unwrap();
        let expected = Duration::hours(1);
        assert_eq!(actual, expected);
    }

    #[test]
    fn test_extract_days_duration() {
        let value = "FREQ=SECONDLY;INTERVAL=1;COUNT=10;DTSTART=2020-01-01T00:00:00Z;DURATION=P1D";
        let actual = extract_duration(&value).unwrap();
        let expected = Duration::days(1);
        assert_eq!(actual, expected);
    }

    #[test]
    fn test_extract_weeks_duration() {
        let value = "FREQ=SECONDLY;INTERVAL=1;COUNT=10;DTSTART=2020-01-01T00:00:00Z;DURATION=P1W";
        let actual = extract_duration(&value).unwrap();
        let expected = Duration::weeks(1);
        assert_eq!(actual, expected);
    }
}