1use icu_calendar::{
2 Date,
3 cal::Hebrew,
4 types::{Month, Weekday},
5};
6
7use crate::{
8 calendar::{
9 HebrewHolidayCalendar,
10 month::{AV, NISAN, SIVAN, TISHREI},
11 },
12 limudim::{
13 HebrewDateExt, Limud,
14 cycle::Cycle,
15 interval::Interval,
16 limud::{CycleFinder, InternalLimud},
17 },
18};
19
20#[allow(clippy::expect_used)]
21fn from_hebrew_date(year: i32, month: Month, day: u8) -> Date<Hebrew> {
22 Date::try_new_hebrew_v2(year, month, day).expect("hard-coded Hebrew date should be valid")
23}
24
25fn day_of_week_number(date: Date<Hebrew>) -> i32 {
26 match date.weekday() {
27 Weekday::Sunday => 1,
28 Weekday::Monday => 2,
29 Weekday::Tuesday => 3,
30 Weekday::Wednesday => 4,
31 Weekday::Thursday => 5,
32 Weekday::Friday => 6,
33 Weekday::Saturday => 7,
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39#[cfg_attr(feature = "defmt", derive(defmt::Format))]
40pub enum PirkeiAvosUnit {
41 Single(u8),
43 Combined(u8, u8),
45}
46
47#[derive(Default)]
48pub struct PirkeiAvos {
50 pub in_israel: bool,
52}
53
54impl InternalLimud<PirkeiAvosUnit> for PirkeiAvos {
55 fn cycle_finder(&self) -> CycleFinder {
56 if self.in_israel {
57 CycleFinder::Perpetual(Self::find_yearly_cycle_israel)
58 } else {
59 CycleFinder::Perpetual(Self::find_yearly_cycle_diaspora)
60 }
61 }
62
63 fn unit_for_interval(&self, interval: &Interval, _limud_date: &Date<Hebrew>) -> Option<PirkeiAvosUnit> {
64 let iteration = interval.iteration;
65
66 if iteration < 19 {
68 let chapter = ((iteration - 1) % 6) + 1;
69 return Some(PirkeiAvosUnit::Single(chapter as u8));
70 }
71
72 let days_until_end = interval.end_date.days_until(&interval.cycle.end_date)?;
75 let weeks_remain = days_until_end.div_ceil(7);
76
77 match weeks_remain {
78 0 => Some(PirkeiAvosUnit::Combined(5, 6)),
79 1 => Some(PirkeiAvosUnit::Combined(3, 4)),
80 2 => {
81 if (iteration - 1) % 6 == 0 {
83 Some(PirkeiAvosUnit::Combined(1, 2))
84 } else {
85 Some(PirkeiAvosUnit::Single(((iteration - 1) % 6 + 1) as u8))
86 }
87 }
88 3 => Some(PirkeiAvosUnit::Single(1)),
89 _ => {
90 let chapter = ((iteration - 1) % 6) + 1;
92 Some(PirkeiAvosUnit::Single(chapter as u8))
93 }
94 }
95 }
96 fn interval_end_calculation(_cycle: Cycle, hebrew_date: Date<Hebrew>) -> Option<Date<Hebrew>> {
97 let day_number = day_of_week_number(hebrew_date);
99 hebrew_date.add_days(7 - day_number)
100 }
101
102 fn is_skip_interval(&self, interval: &Interval) -> bool {
103 let end_month = interval.end_date.input_month();
104 let end_day = interval.end_date.day_of_month().0;
105
106 if end_month == AV && end_day == 8 {
108 return true;
109 }
110
111 if end_month == AV && end_day == 9 {
113 return true;
114 }
115
116 if !self.in_israel && end_month == SIVAN && end_day == 7 {
118 return true;
119 }
120
121 false
122 }
123}
124impl Limud<PirkeiAvosUnit> for PirkeiAvos {}
125
126impl PirkeiAvos {
127 pub fn new(in_israel: bool) -> Self {
135 Self { in_israel }
136 }
137
138 fn find_yearly_cycle_israel(date: Date<Hebrew>) -> Option<(Date<Hebrew>, Date<Hebrew>)> {
139 Some(Self::find_yearly_cycle(true, date))
140 }
141
142 fn find_yearly_cycle_diaspora(date: Date<Hebrew>) -> Option<(Date<Hebrew>, Date<Hebrew>)> {
143 Some(Self::find_yearly_cycle(false, date))
144 }
145
146 fn find_yearly_cycle(in_israel: bool, date: Date<Hebrew>) -> (Date<Hebrew>, Date<Hebrew>) {
150 let year = date.year().extended_year();
151
152 let anchor_day = if in_israel { 22 } else { 23 };
154 let cycle_start_this_year = from_hebrew_date(year, NISAN, anchor_day);
155
156 let (start_date, cycle_year) = if date >= cycle_start_this_year {
158 (cycle_start_this_year, year)
159 } else {
160 let prev_year_start = from_hebrew_date(year - 1, NISAN, anchor_day);
162 (prev_year_start, year - 1)
163 };
164
165 let rosh_hashana = from_hebrew_date(cycle_year + 1, TISHREI, 1);
167 let day_number = day_of_week_number(rosh_hashana);
168 let end_date = rosh_hashana.add_days(-day_number).unwrap_or(rosh_hashana);
170
171 (start_date, end_date)
172 }
173}
174
175#[cfg(test)]
176#[allow(clippy::expect_used)]
177mod tests {
178 use crate::calendar::month::ELUL;
179
180 use super::*;
181
182 #[test]
185 fn test_simple_date() {
186 let test_date = from_hebrew_date(5778, SIVAN, 1);
188 let calculator = PirkeiAvos::new(false);
189 let limud = calculator.limud(test_date).expect("limud exists");
190 assert_eq!(limud, PirkeiAvosUnit::Single(6));
192 }
193
194 #[test]
195 fn test_near_end_of_cycle() {
196 let test_date = from_hebrew_date(5778, ELUL, 20);
198 let calculator = PirkeiAvos::new(false);
199 let limud = calculator.limud(test_date).expect("limud exists");
200 assert_eq!(limud, PirkeiAvosUnit::Combined(3, 4));
202 }
203
204 #[test]
205 fn test_after_cycle_completes() {
206 let test_date = from_hebrew_date(5778, ELUL, 29);
208 let calculator = PirkeiAvos::new(false);
209 let limud = calculator.limud(test_date);
210 assert!(limud.is_none());
211 }
212
213 #[test]
214 fn test_before_cycle_starts() {
215 let test_date = from_hebrew_date(5778, NISAN, 20);
217 let calculator = PirkeiAvos::new(false);
218 let limud = calculator.limud(test_date);
219 assert!(limud.is_none());
220 }
221
222 #[test]
223 fn test_8th_day_pesach_outside_israel() {
224 let test_date = from_hebrew_date(5778, NISAN, 22);
226 let calculator = PirkeiAvos::new(false);
227 let limud = calculator.limud(test_date);
228 assert!(limud.is_none());
229 }
230
231 #[test]
232 fn test_day_after_pesach_outside_israel() {
233 let test_date = from_hebrew_date(5778, NISAN, 23);
235 let calculator = PirkeiAvos::new(false);
236 let limud = calculator.limud(test_date).expect("limud exists");
237 assert_eq!(limud, PirkeiAvosUnit::Single(1));
239 }
240
241 #[test]
242 fn test_compounding_before_cycle_end_outside_israel() {
243 let test_date = from_hebrew_date(5778, ELUL, 14);
245 let calculator = PirkeiAvos::new(false);
246 let limud = calculator.limud(test_date).expect("limud exists");
247 assert_eq!(limud, PirkeiAvosUnit::Combined(1, 2));
248
249 let test_date2 = from_hebrew_date(5778, ELUL, 15);
251 let limud2 = calculator.limud(test_date2).expect("limud exists");
252 assert_eq!(limud2, PirkeiAvosUnit::Combined(3, 4));
253 }
254
255 #[test]
256 fn test_8th_day_pesach_in_israel() {
257 let test_date = from_hebrew_date(5778, NISAN, 22);
259 let calculator = PirkeiAvos::new(true);
260 let limud = calculator.limud(test_date).expect("limud exists");
261 assert_eq!(limud, PirkeiAvosUnit::Single(1));
263 }
264
265 #[test]
266 fn test_day_after_pesach_in_israel() {
267 let test_date = from_hebrew_date(5778, NISAN, 23);
269 let calculator = PirkeiAvos::new(true);
270 let limud = calculator.limud(test_date).expect("limud exists");
271 assert_eq!(limud, PirkeiAvosUnit::Single(2));
273 }
274
275 #[test]
276 fn test_compounding_before_cycle_end_in_israel() {
277 let test_date = from_hebrew_date(5778, ELUL, 21);
279 let calculator = PirkeiAvos::new(true);
280 let limud = calculator.limud(test_date).expect("limud exists");
281 assert_eq!(limud, PirkeiAvosUnit::Combined(3, 4));
282 }
283
284 #[test]
285 fn test_7_sivan_on_shabbos_outside_israel() {
286 let test_date = from_hebrew_date(5769, SIVAN, 3);
289 let calculator = PirkeiAvos::new(false);
290 let limud = calculator.limud(test_date);
291 assert!(limud.is_none());
295 }
296
297 #[test]
298 fn test_iteration_following_7_sivan_on_shabbos_outside_israel() {
299 let test_date = from_hebrew_date(5769, SIVAN, 8);
301 let calculator = PirkeiAvos::new(false);
302 let limud = calculator.limud(test_date).expect("limud exists");
303 assert_eq!(limud, PirkeiAvosUnit::Single(1));
305 }
306
307 #[test]
308 fn test_7_sivan_on_shabbos_in_israel() {
309 let test_date = from_hebrew_date(5769, SIVAN, 3);
311 let calculator = PirkeiAvos::new(true);
312 let limud = calculator.limud(test_date).expect("limud exists");
313 assert_eq!(limud, PirkeiAvosUnit::Single(1));
315 }
316
317 #[test]
318 fn test_iteration_following_7_sivan_on_shabbos_in_israel() {
319 let test_date = from_hebrew_date(5769, SIVAN, 8);
321 let calculator = PirkeiAvos::new(true);
322 let limud = calculator.limud(test_date).expect("limud exists");
323 assert_eq!(limud, PirkeiAvosUnit::Single(2));
325 }
326}