kosher-rust 0.1.0

Rust port of KosherJava: Jewish holidays, halachic times (zmanim), and daily learning schedules. no_std-friendly.
Documentation
use icu_calendar::{Date, cal::Hebrew};

use crate::limudim::{HebrewDateExt, cycle::Cycle};

pub type IntervalEndCalculation = fn(Cycle, Date<Hebrew>) -> Option<Date<Hebrew>>;

pub struct Interval {
    pub start_date: Date<Hebrew>,
    pub end_date: Date<Hebrew>,
    pub iteration: i32,
    pub cycle: Cycle,
}
impl Interval {
    pub fn first_for_cycle(cycle: Cycle, interval_end_calculation: IntervalEndCalculation) -> Option<Self> {
        let start_date = cycle.start_date;
        let iteration = 1;
        let end_date = interval_end_calculation(cycle, start_date)?;
        Some(Self {
            start_date,
            end_date,
            iteration,
            cycle,
        })
    }
    pub fn next(&self, interval_end_calculation: IntervalEndCalculation) -> Option<Self> {
        self._next_for_iteration(self.iteration + 1, interval_end_calculation)
    }
    pub fn skip(&self, interval_end_calculation: IntervalEndCalculation) -> Option<Self> {
        self._next_for_iteration(self.iteration, interval_end_calculation)
    }
    fn _next_for_iteration(
        &self,
        new_iteration: i32,
        interval_end_calculation: IntervalEndCalculation,
    ) -> Option<Self> {
        if self.end_date >= self.cycle.end_date {
            return None;
        }
        let new_start_date = self.end_date.add_days(1)?;
        let new_end_date = interval_end_calculation(self.cycle, new_start_date)?;
        Some(Self {
            start_date: new_start_date,
            end_date: new_end_date,
            iteration: new_iteration,
            cycle: self.cycle,
        })
    }
    pub fn contains(&self, date: Date<Hebrew>) -> bool {
        self.start_date <= date && date <= self.end_date
    }
}

#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
    use icu_calendar::{Date, cal::Hebrew};

    use crate::limudim::{HebrewDateExt, cycle::Cycle, from_gregorian_date};

    use super::*;

    fn single_day_interval(_cycle: Cycle, date: Date<Hebrew>) -> Option<Date<Hebrew>> {
        Some(date)
    }

    #[test]
    fn contains_is_inclusive() {
        let cycle = Cycle {
            start_date: from_gregorian_date(2020, 1, 1),
            end_date: from_gregorian_date(2020, 1, 7),
            iteration: Some(1),
        };
        let interval = Interval::first_for_cycle(cycle, single_day_interval).expect("interval");
        assert!(interval.contains(from_gregorian_date(2020, 1, 1)));
        assert!(interval.contains(interval.end_date));
        assert!(!interval.contains(from_gregorian_date(2020, 1, 2)));
    }

    #[test]
    fn next_advances_iteration_within_cycle() {
        let cycle = Cycle {
            start_date: from_gregorian_date(2020, 1, 1),
            end_date: from_gregorian_date(2020, 1, 10),
            iteration: Some(1),
        };
        let first = Interval::first_for_cycle(cycle, single_day_interval).expect("first interval");
        let second = first.next(single_day_interval).expect("second interval");
        assert_eq!(second.iteration, 2);
        assert_eq!(second.start_date, first.end_date.add_days(1).expect("next day"));
    }

    #[test]
    fn skip_keeps_iteration() {
        let cycle = Cycle {
            start_date: from_gregorian_date(2020, 1, 1),
            end_date: from_gregorian_date(2020, 1, 10),
            iteration: Some(1),
        };
        let first = Interval::first_for_cycle(cycle, single_day_interval).expect("first interval");
        let skipped = first.skip(single_day_interval).expect("skipped interval");
        assert_eq!(skipped.iteration, first.iteration);
    }
}