ical_rust 0.5.2

A lightweight, efficient, and robust iCalendar (ics) parser and recurrence expander library
Documentation
use std::{cmp::Ordering, ops::Range};
use chrono::{Datelike, Timelike, LocalResult, Utc, TimeZone};

use crate::{
    date_or_date_time::DateOrDateTime,
    rrule::{Options, RRule},
    by_day::ByDay,
    VEvent,
};
use chrono::Duration;

#[derive(Debug, Clone)]
pub struct VEventIterator<'a> {
    event: &'a VEvent,
    last_occurrence: Option<DateOrDateTime>,
    count: u32,
}

impl<'a> VEventIterator<'a> {
    pub(crate) fn new(event: &'a VEvent) -> Self {
        Self {
            event,
            last_occurrence: None,
            count: 0,
        }
    }

    fn get_next_occurrence_according_to_rule(
        &mut self,
        last_occurrence: DateOrDateTime,
        rrule: &RRule,
    ) -> Option<DateOrDateTime> {
        match rrule {
            RRule::Yearly(_rrule) => {
                let next_occurrence = last_occurrence.inc_year(1);
                if !rrule.is_expired(next_occurrence) {
                    self.last_occurrence = Some(next_occurrence);
                    self.last_occurrence
                } else {
                    None
                }
            }

            RRule::YearlyByMonthByDay(rrule) => {
                let next_occurrence = last_occurrence.next_by_day(&rrule.day);
                if next_occurrence.year() == last_occurrence.year()
                    && next_occurrence.month() == rrule.month as u32
                    && next_occurrence > last_occurrence
                {
                    if !rrule.is_expired(next_occurrence) {
                        self.last_occurrence = Some(next_occurrence);
                        self.last_occurrence
                    } else {
                        None
                    }
                } else {
                    let next_year = last_occurrence.year() + rrule.common_options().interval.unwrap_or(1) as i32;
                    let target_month_start = last_occurrence
                        .substitute(
                            Some(next_year),
                            Some(rrule.month as u32),
                            Some(1),
                            None,
                            None,
                            None,
                        )
                        .unwrap();
                    
                    let mut next_occurrence = target_month_start;
                    match &rrule.day {
                        ByDay::Simple(weekdays) => {
                            if !weekdays.contains(&target_month_start.date().weekday()) {
                                next_occurrence = target_month_start.next_by_day(&rrule.day);
                            }
                        }
                        ByDay::Delta(_) => {
                            next_occurrence = target_month_start.next_by_day(&rrule.day);
                        }
                    }

                    if !rrule.is_expired(next_occurrence) {
                        self.last_occurrence = Some(next_occurrence);
                        self.last_occurrence
                    } else {
                        None
                    }
                }
            }

            RRule::YearlyByMonthByMonthDay(_rrule) => {
                let next_occurrence = last_occurrence.inc_year(1);
                if !rrule.is_expired(next_occurrence) {
                    self.last_occurrence = Some(next_occurrence);
                    self.last_occurrence
                } else {
                    None
                }
            }

            RRule::MonthlyByMonthDay(rrule) => {
                let mut year = last_occurrence.year();
                let mut month = last_occurrence.month() + 1;
                if month > 12 {
                    month = 1;
                    year += 1;
                }
                let target_day = rrule.month_day as u32;
                let mut d = target_day;
                let mut date = Utc.with_ymd_and_hms(year, month, d, 0, 0, 0);
                while matches!(date, LocalResult::None) && d > 1 {
                    d -= 1;
                    date = Utc.with_ymd_and_hms(year, month, d, 0, 0, 0);
                }
                let target_date = date.unwrap();
                let next_occurrence = match last_occurrence {
                    DateOrDateTime::WholeDay(_) => DateOrDateTime::WholeDay(target_date),
                    DateOrDateTime::DateTime(dt) => DateOrDateTime::DateTime(
                        Utc.with_ymd_and_hms(
                            target_date.year(),
                            target_date.month(),
                            target_date.day(),
                            dt.hour(),
                            dt.minute(),
                            dt.second(),
                        )
                        .unwrap(),
                    ),
                };

                if !rrule.is_expired(next_occurrence) {
                    self.last_occurrence = Some(next_occurrence);
                    self.last_occurrence
                } else {
                    None
                }
            }

            RRule::MonthlyByDay(rrule) => {
                let next_month = last_occurrence
                    .substitute(
                        Some(if last_occurrence.month() == 12 {
                            last_occurrence.year() + 1
                        } else {
                            last_occurrence.year()
                        }),
                        Some(if last_occurrence.month() == 12 {
                            1
                        } else {
                            last_occurrence.month() + 1
                        }),
                        Some(1),
                        None,
                        None,
                        None,
                    )
                    .unwrap();

                // Calculate 1SU or -1SU... done in DateOrDatetime
                let next_occurrence = next_month.next_by_day(&rrule.day);

                if !rrule.is_expired(next_occurrence) {
                    self.last_occurrence = Some(next_occurrence);
                    self.last_occurrence
                } else {
                    None
                }
            }

            RRule::Weekly(rrule) => {
                let next_occurrence = last_occurrence + Duration::days(7);

                if !rrule.is_expired(next_occurrence) {
                    self.last_occurrence = Some(next_occurrence);
                    self.last_occurrence
                } else {
                    None
                }
            }

            RRule::WeeklyByDay(rrule) => {
                let interval = rrule.common_options().interval.unwrap_or(1) as i64;
                let mut next_occurrence = last_occurrence.next_by_day(&rrule.day);

                loop {
                    let dt_start_monday = self.event.dt_start.date() - Duration::days(self.event.dt_start.date().weekday().num_days_from_monday() as i64);
                    let next_monday = next_occurrence.date() - Duration::days(next_occurrence.date().weekday().num_days_from_monday() as i64);
                    let diff_days = (next_monday - dt_start_monday).num_days();
                    let diff_weeks = diff_days / 7;

                    if diff_weeks % interval == 0 {
                        break;
                    }
                    next_occurrence = next_occurrence.next_by_day(&rrule.day);
                }

                if !rrule.is_expired(next_occurrence) {
                    self.last_occurrence = Some(next_occurrence);
                    self.last_occurrence
                } else {
                    None
                }
            }

            RRule::Daily(rrule) => {
                let next_occurrence = last_occurrence + Duration::days(1);

                if !rrule.is_expired(next_occurrence) {
                    self.last_occurrence = Some(next_occurrence);
                    self.last_occurrence
                } else {
                    None
                }
            }
        }
    }

    fn get_next_occurrence_according_to_rule_and_iterations(&mut self) -> Option<DateOrDateTime> {
        if let Some(last_occurrence) = self.last_occurrence {
            self.event.rrule.as_ref().and_then(|rrule| {
                if rrule.is_out_of_count(self.count) {
                    return None;
                }
                let mut next_occurrence = Some(last_occurrence);
                let mut iterations = match rrule {
                    RRule::WeeklyByDay(_) | RRule::YearlyByMonthByDay(_) => 1,
                    _ => rrule.common_options().interval.unwrap_or(1),
                };
                while iterations > 0 && next_occurrence.is_some() {
                    next_occurrence =
                        self.get_next_occurrence_according_to_rule(next_occurrence.unwrap(), rrule);
                    iterations -= 1;
                }

                next_occurrence
            })
        } else {
            self.last_occurrence = Some(self.event.dt_start);
            Some(self.event.dt_start)
        }
    }
}

impl<'a> Iterator for VEventIterator<'a> {
    type Item = Range<DateOrDateTime>;

    fn next(&mut self) -> Option<Self::Item> {
        log::trace!("function next({:?}) called", self);

        let mut next = self.get_next_occurrence_according_to_rule_and_iterations();
        log::trace!("next == {:?}", next);

        loop {
            // remove dates appearing in ExDate field
            if let Some(next_non_empty) = next {
                log::trace!("next_non_empty == {:?}", next_non_empty);

                if !self.event.exdates.iter().any(|exdate| {
                    // we check only for date comparison and not time because of the weird handling
                    // of timezones in EXDATE. This should be enough since the repetition can be at
                    // most per day.
                    next_non_empty.date().cmp(&exdate.date_time.date()) == Ordering::Equal
                }) {
                    // keep count
                    self.count += 1;

                    // calculate how long it's supposed to last
                    let delta = self.event.dt_end - self.event.dt_start;
                    let next_non_empty_end = next_non_empty + delta;
                    return Some(Range {
                        start: next_non_empty,
                        end: next_non_empty_end,
                    });
                } else {
                    next = self.get_next_occurrence_according_to_rule_and_iterations();
                }
            } else {
                return None;
            }
        }
    }
}