Skip to main content

kosher_rust/limudim/
tehillim_monthly.rs

1use icu_calendar::{Date, cal::Hebrew};
2
3use crate::{
4    calendar::HebrewHolidayCalendar,
5    limudim::{
6        Limud,
7        interval::Interval,
8        limud::{CycleFinder, InternalLimud},
9    },
10};
11
12/// Cumulative ending psalm for each day of the month (0-indexed by day-1)
13/// Day 1: psalms 1-9, Day 2: psalms 10-17, etc.
14const DEFAULT_UNITS: [u8; 30] = [
15    9, 17, 22, 28, 34, 38, 43, 48, 54, 59, 65, 68, 71, 76, 78, 82, 87, 89, 96, 103, 105, 107, 112, 118, 119, 119, 134,
16    139, 144, 150,
17];
18
19/// Represents a Tehillim (Psalms) reading unit
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[cfg_attr(feature = "defmt", derive(defmt::Format))]
22#[allow(missing_docs)]
23pub enum TehillimUnit {
24    /// A range of complete psalms (e.g., psalms 1-9)
25    Psalms { start: u8, end: u8 },
26    /// A range of verses within a single psalm (for Psalm 119)
27    PsalmVerses {
28        psalm: u8,
29        start_verse: u16,
30        end_verse: u16,
31    },
32}
33
34#[derive(Default)]
35/// Calculates the Tehillim (Psalms) monthly schedule.
36pub struct TehillimMonthly;
37
38/// Find the 1st of the current Hebrew month and the last day of the month
39fn find_monthly_cycle(date: Date<Hebrew>) -> Option<(Date<Hebrew>, Date<Hebrew>)> {
40    let year = date.year().extended_year();
41    let month = date.input_month();
42
43    // Start of cycle: 1st of current month
44    let start = Date::try_new_hebrew_v2(year, month, 1).ok()?;
45
46    // End of cycle: last day of current month
47    let days_in_month = date.days_in_month();
48    let end = Date::try_new_hebrew_v2(year, month, days_in_month).ok()?;
49
50    Some((start, end))
51}
52
53impl InternalLimud<TehillimUnit> for TehillimMonthly {
54    fn cycle_finder(&self) -> CycleFinder {
55        CycleFinder::Perpetual(find_monthly_cycle)
56    }
57
58    fn unit_for_interval(&self, interval: &Interval, _limud_date: &Date<Hebrew>) -> Option<TehillimUnit> {
59        let iteration = interval.iteration;
60
61        // Special cases for Psalm 119 on days 25 and 26
62        if iteration == 25 {
63            return Some(TehillimUnit::PsalmVerses {
64                psalm: 119,
65                start_verse: 1,
66                end_verse: 96,
67            });
68        }
69
70        if iteration == 26 {
71            return Some(TehillimUnit::PsalmVerses {
72                psalm: 119,
73                start_verse: 97,
74                end_verse: 176,
75            });
76        }
77
78        // Normal psalm range calculation
79        let (start, mut stop) = if iteration == 1 {
80            (1, DEFAULT_UNITS[0])
81        } else {
82            let prev_end = DEFAULT_UNITS[(iteration - 2) as usize];
83            let curr_end = DEFAULT_UNITS[(iteration - 1) as usize];
84            (prev_end + 1, curr_end)
85        };
86
87        // On the 29th day of a 29-day month, include the next day's reading too
88        let day = interval.end_date.day_of_month().0;
89        let days_in_month = interval.end_date.days_in_month();
90        if day == 29 && days_in_month == 29 && iteration < 30 {
91            stop = DEFAULT_UNITS[iteration as usize];
92        }
93
94        Some(TehillimUnit::Psalms { start, end: stop })
95    }
96}
97impl Limud<TehillimUnit> for TehillimMonthly {}
98
99#[cfg(test)]
100#[allow(clippy::expect_used, clippy::unwrap_used)]
101mod tests {
102    use crate::calendar::month::{SHEVAT, TEVET};
103
104    use super::*;
105
106    #[test]
107    fn tehillim_monthly_simple_date() {
108        // JewishDate(5778, 10, 8) - 8th of Teves
109        let test_date = Date::try_new_hebrew_v2(5778, TEVET, 8).unwrap();
110        let limud = TehillimMonthly.limud(test_date).expect("limud exists");
111        // Day 8 should be psalms 44-48
112        assert_eq!(limud, TehillimUnit::Psalms { start: 44, end: 48 });
113    }
114
115    #[test]
116    fn tehillim_monthly_beginning_of_month() {
117        // JewishDate(5778, 10, 1) - 1st of Teves
118        let test_date = Date::try_new_hebrew_v2(5778, TEVET, 1).unwrap();
119        let limud = TehillimMonthly.limud(test_date).expect("limud exists");
120        // Day 1 should be psalms 1-9
121        assert_eq!(limud, TehillimUnit::Psalms { start: 1, end: 9 });
122    }
123
124    #[test]
125    fn tehillim_monthly_end_of_short_month() {
126        // JewishDate(5778, 10, 29) - 29th of Teves (29-day month)
127        let test_date = Date::try_new_hebrew_v2(5778, TEVET, 29).unwrap();
128        let limud = TehillimMonthly.limud(test_date).expect("limud exists");
129        // Day 29 of a 29-day month should include day 30's reading: psalms 140-150
130        assert_eq!(limud, TehillimUnit::Psalms { start: 140, end: 150 });
131    }
132
133    #[test]
134    fn tehillim_monthly_end_of_long_month() {
135        // JewishDate(5778, 11, 30) - 30th of Shevat (30-day month)
136        let test_date = Date::try_new_hebrew_v2(5778, SHEVAT, 30).unwrap();
137        let limud = TehillimMonthly.limud(test_date).expect("limud exists");
138        // Day 30 should be psalms 145-150
139        assert_eq!(limud, TehillimUnit::Psalms { start: 145, end: 150 });
140    }
141
142    #[test]
143    fn tehillim_monthly_29th_day_of_long_month() {
144        // JewishDate(5778, 11, 29) - 29th of Shevat (30-day month)
145        let test_date = Date::try_new_hebrew_v2(5778, SHEVAT, 29).unwrap();
146        let limud = TehillimMonthly.limud(test_date).expect("limud exists");
147        // Day 29 of a 30-day month should be psalms 140-144 only
148        assert_eq!(limud, TehillimUnit::Psalms { start: 140, end: 144 });
149    }
150
151    #[test]
152    fn tehillim_monthly_day_25_special_case() {
153        // JewishDate(5778, 11, 25) - 25th of Shevat
154        let test_date = Date::try_new_hebrew_v2(5778, SHEVAT, 25).unwrap();
155        let limud = TehillimMonthly.limud(test_date).expect("limud exists");
156        // Day 25 is Psalm 119 verses 1-96
157        assert_eq!(
158            limud,
159            TehillimUnit::PsalmVerses {
160                psalm: 119,
161                start_verse: 1,
162                end_verse: 96
163            }
164        );
165    }
166
167    #[test]
168    fn tehillim_monthly_day_26_special_case() {
169        // JewishDate(5778, 11, 26) - 26th of Shevat
170        let test_date = Date::try_new_hebrew_v2(5778, SHEVAT, 26).unwrap();
171        let limud = TehillimMonthly.limud(test_date).expect("limud exists");
172        // Day 26 is Psalm 119 verses 97-176
173        assert_eq!(
174            limud,
175            TehillimUnit::PsalmVerses {
176                psalm: 119,
177                start_verse: 97,
178                end_verse: 176
179            }
180        );
181    }
182}