rrule 0.14.0

A pure Rust implementation of recurrence rules as defined in the iCalendar RFC.
Documentation
use std::{collections::HashMap, str::FromStr};

use log::warn;

use crate::{
    parser::{
        datetime::{datestring_to_date, parse_timezone},
        ParseError,
    },
    Tz,
};

use super::{content_line_parts::ContentLineCaptures, parameters::parse_parameters};

#[derive(Debug, Hash, PartialEq, Eq)]
pub enum DateParameter {
    Timezone,
    Value,
}

impl FromStr for DateParameter {
    type Err = ParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let param = match &s.to_uppercase()[..] {
            "TZID" => Self::Timezone,
            "VALUE" => Self::Value,
            _ => return Err(ParseError::UnrecognizedParameter(s.into())),
        };
        Ok(param)
    }
}

impl TryFrom<ContentLineCaptures<'_>> for Vec<chrono::DateTime<Tz>> {
    type Error = ParseError;

    fn try_from(value: ContentLineCaptures) -> Result<Self, Self::Error> {
        let parameters: HashMap<DateParameter, String> = value
            .parameters
            .map(parse_parameters)
            .transpose()?
            .unwrap_or_default();

        match parameters
            .get(&DateParameter::Value)
            .map(|val| val.to_ascii_lowercase())
            .as_deref()
        {
            Some("date") => {
                warn!(
                    "Parameter `DATE` is not supported for property name: `{}`. The dates will be interpreter with the `DATE-TIME` parameter instead.",
                    value.property_name
                );
            }
            Some("period") => {
                warn!(
                    "Parameter `PERIOD` is not supported for property name: `{}`. The dates will be interpreter with the `DATE-TIME` parameter instead.",
                    value.property_name
                );
            }
            Some("date-time") => {}
            Some(param) => {
                warn!(
                    "Encountered unexpected parameter `{param}` for property name: `{}`",
                    value.property_name
                );
            }
            None => {}
        }

        let timezone = parameters
            .get(&DateParameter::Timezone)
            .map(|tz| parse_timezone(tz))
            .transpose()?;
        let property = format!("{}", value.property_name);

        let mut dates = vec![];
        for val in value.value.split(',') {
            if val.is_empty() {
                continue;
            }
            let datetime = datestring_to_date(val, timezone, &property)?;
            dates.push(datetime);
        }

        Ok(dates)
    }
}

#[cfg(test)]
mod tests {
    use chrono::TimeZone;

    use crate::{core::Tz, parser::content_line::PropertyName};

    use super::*;

    const UTC: Tz = Tz::UTC;

    #[test]
    fn parses_date_content_line() {
        let tests = [
            (
                ContentLineCaptures {
                    property_name: PropertyName::RDate,
                    parameters: None,
                    value: "19970714T123000Z",
                },
                vec![UTC.with_ymd_and_hms(1997, 7, 14, 12, 30, 0).unwrap()],
            ),
            (
                ContentLineCaptures {
                    property_name: PropertyName::RDate,
                    parameters: None,
                    value: "19970714T123000",
                },
                vec![Tz::LOCAL.with_ymd_and_hms(1997, 7, 14, 12, 30, 0).unwrap()],
            ),
            (
                ContentLineCaptures {
                    property_name: PropertyName::RDate,
                    parameters: Some("VALUE=DATE;TZID=UTC"),
                    value: "19970101,19970120,19970217,19970421",
                },
                vec![
                    UTC.with_ymd_and_hms(1997, 1, 1, 0, 0, 0).unwrap(),
                    UTC.with_ymd_and_hms(1997, 1, 20, 0, 0, 0).unwrap(),
                    UTC.with_ymd_and_hms(1997, 2, 17, 0, 0, 0).unwrap(),
                    UTC.with_ymd_and_hms(1997, 4, 21, 0, 0, 0).unwrap(),
                ],
            ),
        ];

        for (input, expected_output) in tests {
            let output = TryFrom::try_from(input);
            assert_eq!(output, Ok(expected_output));
        }
    }
}