ical_rust 0.8.6

A lightweight, efficient, and robust iCalendar (ics) parser and recurrence expander library
Documentation
use crate::{
    by_day::{WeekdayRule, parse_by_day, to_chrono_weekday, weekday_to_ical_str},
    date_or_date_time::DateOrDateTime,
    errors::RRuleParseError,
    frequency::Frequency,
    string_to_date_or_datetime,
};
use std::{fmt, str::FromStr};

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RRule {
    pub frequency: Frequency,
    pub interval: u32,
    pub count: Option<u32>,
    pub until: Option<DateOrDateTime>,
    pub by_month: Vec<u8>,
    pub by_month_day: Vec<i8>,
    pub by_day: Vec<WeekdayRule>,
    pub by_set_pos: Vec<i16>,
    pub wkst: Option<chrono::Weekday>,
}

impl RRule {
    pub fn is_out_of_count(&self, count: u32) -> bool {
        log::trace!(
            "is_out_of_count(self == {:?}, count == {}) called",
            self,
            count
        );
        self.count
            .map(|rrule_count| count >= rrule_count)
            .unwrap_or(false)
    }

    pub fn is_expired(&self, dt: DateOrDateTime) -> bool {
        log::debug!("is_expired(self == {:?}, dt == {:?}) called", self, dt);
        self.until.map(|until| dt > until).unwrap_or(false)
    }
}

impl fmt::Display for RRule {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "FREQ={}", self.frequency)?;
        if self.interval != 1 {
            write!(f, ";INTERVAL={}", self.interval)?;
        }
        if let Some(count) = self.count {
            write!(f, ";COUNT={count}")?;
        }
        if let Some(until) = self.until {
            match until {
                DateOrDateTime::WholeDay(d) => write!(f, ";UNTIL={}", d.format("%Y%m%d"))?,
                DateOrDateTime::DateTime { utc, .. } => {
                    write!(f, ";UNTIL={}", utc.format("%Y%m%dT%H%M%SZ"))?
                }
            }
        }
        if !self.by_month.is_empty() {
            let months = self
                .by_month
                .iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>()
                .join(",");
            write!(f, ";BYMONTH={months}")?;
        }
        if !self.by_month_day.is_empty() {
            let month_days = self
                .by_month_day
                .iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>()
                .join(",");
            write!(f, ";BYMONTHDAY={month_days}")?;
        }
        if !self.by_day.is_empty() {
            let days = self
                .by_day
                .iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>()
                .join(",");
            write!(f, ";BYDAY={days}")?;
        }
        if !self.by_set_pos.is_empty() {
            let pos = self
                .by_set_pos
                .iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>()
                .join(",");
            write!(f, ";BYSETPOS={pos}")?;
        }
        if let Some(wkst) = self.wkst {
            write!(f, ";WKST={}", weekday_to_ical_str(wkst))?;
        }
        Ok(())
    }
}

fn find_rrule_token<'a>(tokens: &[&'a str], prefix: &str) -> Option<&'a str> {
    tokens.iter().find_map(|item| {
        if item.len() >= prefix.len()
            && item.is_char_boundary(prefix.len())
            && item[..prefix.len()].eq_ignore_ascii_case(prefix)
        {
            Some(&item[prefix.len()..])
        } else {
            None
        }
    })
}

impl FromStr for RRule {
    type Err = RRuleParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let tokens: Vec<_> = s.split(';').collect();
        let freq = find_rrule_token(&tokens, "FREQ=")
            .ok_or_else(|| RRuleParseError::MissingFrequencyToken { line: s.to_owned() })?;
        let frequency = freq
            .parse()
            .map_err(|err| RRuleParseError::FrequencyParseError { err })?;

        let interval: u32 = find_rrule_token(&tokens, "INTERVAL=")
            .map(|item| item.parse())
            .transpose()?
            .unwrap_or(1);

        if interval == 0 {
            return Err(RRuleParseError::InvalidInterval { interval });
        }

        let until: Option<DateOrDateTime> = find_rrule_token(&tokens, "UNTIL=")
            .map(string_to_date_or_datetime)
            .transpose()?;

        let count: Option<u32> = find_rrule_token(&tokens, "COUNT=")
            .map(|item| item.parse())
            .transpose()?;

        if count.is_some() && until.is_some() {
            return Err(RRuleParseError::BothCountAndUntilSpecified);
        }

        let by_month: Vec<u8> = find_rrule_token(&tokens, "BYMONTH=")
            .map(|item| {
                item.split(',')
                    .map(|m| m.parse::<u8>())
                    .collect::<Result<Vec<u8>, _>>()
            })
            .transpose()?
            .unwrap_or_default();

        for &month in &by_month {
            if !(1..=12).contains(&month) {
                return Err(RRuleParseError::InvalidByMonth { month });
            }
        }

        let by_month_day: Vec<i8> = find_rrule_token(&tokens, "BYMONTHDAY=")
            .map(|item| {
                item.split(',')
                    .map(|d| d.parse::<i8>())
                    .collect::<Result<Vec<i8>, _>>()
            })
            .transpose()?
            .unwrap_or_default();

        for &day in &by_month_day {
            if day == 0 || !(-31..=31).contains(&day) {
                return Err(RRuleParseError::InvalidByMonthDay { day });
            }
        }

        let by_day: Vec<WeekdayRule> = find_rrule_token(&tokens, "BYDAY=")
            .map(parse_by_day)
            .transpose()
            .map_err(|error| RRuleParseError::ByDayParserError {
                error,
                line: s.to_owned(),
            })?
            .unwrap_or_default();

        let by_set_pos: Vec<i16> = find_rrule_token(&tokens, "BYSETPOS=")
            .map(|item| {
                item.split(',')
                    .map(|p| p.parse::<i16>())
                    .collect::<Result<Vec<i16>, _>>()
            })
            .transpose()?
            .unwrap_or_default();

        for &pos in &by_set_pos {
            if pos == 0 || !(-366..=366).contains(&pos) {
                return Err(RRuleParseError::InvalidBySetPos { pos });
            }
        }

        let wkst: Option<chrono::Weekday> = find_rrule_token(&tokens, "WKST=")
            .map(to_chrono_weekday)
            .transpose()
            .map_err(|error| RRuleParseError::ByDayParserError {
                error,
                line: s.to_owned(),
            })?;

        Ok(Self {
            frequency,
            interval,
            count,
            until,
            by_month,
            by_month_day,
            by_day,
            by_set_pos,
            wkst,
        })
    }
}