kosher_rust/calendar/
mod.rs1pub mod month;
27
28pub mod holiday;
30
31pub mod parsha;
33
34pub mod prelude {
40 pub use super::holiday::{Holiday, HolidayIterator};
41 pub use super::month;
42 pub use super::month::HebrewMonthExt;
43 pub use super::parsha::Parsha;
44 pub use super::{HebrewCalendar, HebrewCalendarDate, HebrewHolidayCalendar, YearLengthType};
45}
46
47#[cfg(test)]
48#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
49mod tests;
50
51use holiday::{Holiday, HolidayIterator};
52use icu_calendar::options::DateAddOptions;
53use icu_calendar::types::{DateDuration, Month, Weekday};
54use icu_calendar::{AsCalendar, Date, Iso, cal::Hebrew};
55use jiff_icu::ConvertFrom;
56use month::*;
57use num_enum::{IntoPrimitive, TryFromPrimitive};
58pub use parsha::Parsha;
59use parsha::get_parsha_list;
60
61pub trait HebrewCalendarDate {
66 fn hebrew_date(&self) -> Date<Hebrew>;
68}
69
70impl<C> HebrewCalendarDate for Date<C>
71where
72 C: AsCalendar,
73{
74 #[inline]
75 fn hebrew_date(&self) -> Date<Hebrew> {
76 self.to_calendar(Hebrew)
77 }
78}
79
80impl HebrewCalendarDate for jiff::civil::Date {
81 #[inline]
82 fn hebrew_date(&self) -> Date<Hebrew> {
83 let iso_date = Date::<Iso>::convert_from(*self);
84 iso_date.to_calendar(Hebrew)
85 }
86}
87
88#[derive(Debug, PartialEq, Eq, Clone, Copy, IntoPrimitive, TryFromPrimitive)]
94#[repr(u8)]
95pub enum YearLengthType {
96 Chaserim = 0,
98 Kesidran = 1,
100 Shelaimim = 2,
102}
103
104pub trait HebrewHolidayCalendar: HebrewCalendarDate {
109 type HolidayIter: Iterator<Item = Holiday>;
111
112 fn input_month(&self) -> Month;
114
115 fn holidays(&self, in_israel: bool, use_modern_holidays: bool) -> Self::HolidayIter;
122
123 fn is_assur_bemelacha(&self, in_israel: bool) -> bool;
125
126 fn has_candle_lighting(&self, in_israel: bool) -> bool;
128
129 fn is_aseres_yemei_teshuva(&self) -> bool;
131
132 fn todays_parsha(&self, in_israel: bool) -> Option<Parsha>;
134
135 fn special_parsha(&self, in_israel: bool) -> Option<Parsha>;
137
138 fn upcoming_parsha(&self, in_israel: bool) -> Option<Parsha>;
147}
148
149impl<T> HebrewHolidayCalendar for T
150where
151 T: HebrewCalendarDate,
152{
153 type HolidayIter = HolidayIterator;
154
155 #[inline]
156 fn is_assur_bemelacha(&self, in_israel: bool) -> bool {
157 self.hebrew_date().weekday() == Weekday::Saturday
158 || self.holidays(in_israel, false).any(|i| i.is_assur_bemelacha())
159 }
160 #[inline]
161 fn has_candle_lighting(&self, in_israel: bool) -> bool {
162 self.hebrew_date()
163 .try_added_with_options(DateDuration::for_days(1), DateAddOptions::default())
164 .map(|next_day| next_day.is_assur_bemelacha(in_israel))
165 .unwrap_or(false)
166 }
167
168 #[inline]
169 fn input_month(&self) -> Month {
170 self.hebrew_date().month().to_input()
171 }
172
173 #[inline]
174 fn holidays(&self, in_israel: bool, use_modern_holidays: bool) -> Self::HolidayIter {
175 HolidayIterator {
176 iter: holiday::all_rules().iter(),
177 date: self.hebrew_date(),
178 in_israel,
179 use_modern_holidays,
180 }
181 }
182
183 fn is_aseres_yemei_teshuva(&self) -> bool {
184 let date = self.hebrew_date();
185 date.input_month() == TISHREI && date.day_of_month().0 <= 10
186 }
187
188 fn todays_parsha(&self, in_israel: bool) -> Option<Parsha> {
189 let date = self.hebrew_date();
190 if date.weekday() != Weekday::Saturday {
191 return None;
192 }
193
194 let parsha_list = get_parsha_list(&date, in_israel)?;
195
196 let rosh_hashana_day_of_week = get_hebrew_elapsed_days(date.year().extended_year())? % 7;
197 let day = rosh_hashana_day_of_week + date.day_of_year().0 as i32;
198 let week_index = usize::try_from(day / 7).ok()?;
199 parsha_list.get(week_index).copied().flatten()
200 }
201
202 fn special_parsha(&self, in_israel: bool) -> Option<Parsha> {
203 let date = self.hebrew_date();
204 if date.weekday() != Weekday::Saturday {
205 return None;
206 }
207
208 let month = date.input_month();
209 let day = date.day_of_month().0;
210 let is_leap = Hebrew::is_hebrew_leap_year(date.year().extended_year());
211
212 if ((month == SHEVAT && !is_leap) || (month == ADARI && is_leap)) && (day == 25 || day == 27 || day == 29) {
214 return Some(Parsha::Shekalim);
215 }
216
217 if month == ADAR {
218 if day == 1 {
219 return Some(Parsha::Shekalim);
220 }
221 if day == 8 || day == 9 || day == 11 || day == 13 {
223 return Some(Parsha::Zachor);
224 }
225 if day == 18 || day == 20 || day == 22 || day == 23 {
227 return Some(Parsha::Parah);
228 }
229 if day == 25 || day == 27 || day == 29 {
231 return Some(Parsha::Hachodesh);
232 }
233 }
234
235 if month == NISAN {
236 if day == 1 {
237 return Some(Parsha::Hachodesh);
238 }
239 if (8..=14).contains(&day) {
241 return Some(Parsha::Hagadol);
242 }
243 }
244
245 if month == AV {
246 if (4..=9).contains(&day) {
248 return Some(Parsha::Chazon);
249 }
250 if (10..=16).contains(&day) {
252 return Some(Parsha::Nachamu);
253 }
254 }
255
256 if month == TISHREI {
257 if (3..=8).contains(&day) {
259 return Some(Parsha::Shuva);
260 }
261 }
262
263 if self.todays_parsha(in_israel) == Some(Parsha::Beshalach) {
265 return Some(Parsha::Shira);
266 }
267
268 None
269 }
270
271 fn upcoming_parsha(&self, in_israel: bool) -> Option<Parsha> {
272 let days_to_shabbos = match self.hebrew_date().weekday() {
273 Weekday::Monday => 5,
274 Weekday::Tuesday => 4,
275 Weekday::Wednesday => 3,
276 Weekday::Thursday => 2,
277 Weekday::Friday => 1,
278 Weekday::Saturday => 7,
279 Weekday::Sunday => 6,
280 };
281
282 let mut date = self
283 .hebrew_date()
284 .try_added_with_options(DateDuration::for_days(days_to_shabbos), DateAddOptions::default())
285 .ok()?;
286
287 for _ in 0..60 {
289 if let Some(parshah) = date.todays_parsha(in_israel) {
290 return Some(parshah);
291 }
292
293 date = date
294 .try_added_with_options(DateDuration::for_days(7), DateAddOptions::default())
295 .ok()?;
296 }
297
298 None
299 }
300}
301
302const CHALAKIM_MOLAD_TOHU: i64 = 31524;
304const CHALAKIM_PER_MONTH: i64 = 765433;
306const CHALAKIM_PER_DAY: i64 = 25920;
308
309pub(crate) fn chalakim_since_molad_tohu(year: i32, month: Month) -> Option<i64> {
311 let month_of_year = month.hebrew_month_of_year(year)?;
312 let months_elapsed = (235 * ((year - 1) / 19))
313 + (12 * ((year - 1) % 19))
314 + ((7 * ((year - 1) % 19) + 1) / 19)
315 + (month_of_year as i32 - 1);
316
317 Some(CHALAKIM_MOLAD_TOHU + (CHALAKIM_PER_MONTH * months_elapsed as i64))
318}
319
320pub(super) fn get_hebrew_elapsed_days(year: i32) -> Option<i32> {
323 let chalakim_since = chalakim_since_molad_tohu(year, TISHREI)?;
324 let molad_day = chalakim_since / CHALAKIM_PER_DAY;
325 let molad_parts = chalakim_since - molad_day * CHALAKIM_PER_DAY;
326 let mut rosh_hashana_day = molad_day;
327
328 if (molad_parts >= 19440)
329 || (((molad_day % 7) == 2) && (molad_parts >= 9924) && !Hebrew::is_hebrew_leap_year(year))
330 || (((molad_day % 7) == 1) && (molad_parts >= 16789) && (Hebrew::is_hebrew_leap_year(year - 1)))
331 {
332 rosh_hashana_day += 1;
333 }
334
335 if ((rosh_hashana_day % 7) == 0) || ((rosh_hashana_day % 7) == 3) || ((rosh_hashana_day % 7) == 5) {
336 rosh_hashana_day += 1;
337 }
338
339 Some(rosh_hashana_day as i32)
340}
341
342pub trait HebrewCalendar {
346 fn days_in_hebrew_year(year: i32) -> Option<i32>;
348
349 fn days_in_hebrew_month(year: i32, month: Month) -> Option<u8>;
353
354 fn is_cheshvan_long(year: i32) -> Option<bool>;
356
357 fn is_kislev_short(year: i32) -> Option<bool>;
359
360 fn is_hebrew_leap_year(year: i32) -> bool;
362
363 fn cheshvan_kislev_kviah(year: i32) -> Option<YearLengthType>;
365}
366
367impl HebrewCalendar for Hebrew {
368 #[inline]
369 fn days_in_hebrew_year(year: i32) -> Option<i32> {
370 Some(get_hebrew_elapsed_days(year + 1)? - get_hebrew_elapsed_days(year)?)
371 }
372
373 #[inline]
374 fn days_in_hebrew_month(year: i32, month: Month) -> Option<u8> {
375 month.hebrew_month_of_year(year)?;
376
377 Some(match month {
378 IYYAR | TAMMUZ | ELUL | TEVET => 29,
379 ḤESHVAN if Self::is_cheshvan_long(year)? => 30,
380 ḤESHVAN => 29,
381 KISLEV if Self::is_kislev_short(year)? => 29,
382 KISLEV => 30,
383 ADARI => 30,
384 ADAR => 29,
385 TISHREI | SHEVAT | NISAN | SIVAN | AV => 30,
386 _ => return None,
387 })
388 }
389
390 #[inline]
391 fn is_cheshvan_long(year: i32) -> Option<bool> {
392 Some(Self::days_in_hebrew_year(year)? % 10 == 5)
393 }
394
395 #[inline]
396 fn is_kislev_short(year: i32) -> Option<bool> {
397 Some(Self::days_in_hebrew_year(year)? % 10 == 3)
398 }
399
400 #[inline]
401 fn is_hebrew_leap_year(year: i32) -> bool {
402 let year_in_cycle = ((year - 1) % 19) + 1;
403 matches!(year_in_cycle, 3 | 6 | 8 | 11 | 14 | 17 | 19)
404 }
405
406 #[inline]
407 fn cheshvan_kislev_kviah(year: i32) -> Option<YearLengthType> {
408 Some(if Self::is_cheshvan_long(year)? && !Self::is_kislev_short(year)? {
409 YearLengthType::Shelaimim
410 } else if !Self::is_cheshvan_long(year)? && Self::is_kislev_short(year)? {
411 YearLengthType::Chaserim
412 } else {
413 YearLengthType::Kesidran
414 })
415 }
416}