calendrical_calculations/hebrew_keviyah.rs
1// This file is part of ICU4X.
2//
3// The contents of this file implement algorithms from Calendrical Calculations
4// by Reingold & Dershowitz, Cambridge University Press, 4th edition (2018),
5// which have been released as Lisp code at <https://github.com/EdReingold/calendar-code2/>
6// under the Apache-2.0 license. Accordingly, this file is released under
7// the Apache License, Version 2.0 which can be found at the calendrical_calculations
8// package root or at http://www.apache.org/licenses/LICENSE-2.0.
9
10// The algorithms in this file are rather well-published in multiple places,
11// though the resource that was primarily used was
12// J Jean Adler's _A Short History of the Jewish Fixed Calendar_, found
13// at <https://hakirah.org/vol20Ajdler.pdf>, with more detailed appendices
14// at <https://www.hakirah.org/vol20AjdlerAppendices.pdf>.
15// Most of the math can be found on Wikipedia as well,
16// at <https://en.wikipedia.org/wiki/Hebrew_calendar#The_four_gates>
17
18//! Alternate, more efficient structures for working with the Hebrew Calendar
19//! using the keviyah and Four Gates system
20//!
21//! The main entry point for this code is [`YearInfo::compute_for()`] and [`YearInfo::year_containing_rd()`],
22//! which will efficiently calculate certain information about a Hebrew year, given the Hebrew year
23//! or a date that falls within it, and produce it as a [`YearInfo`].
24//!
25//! From there, you can compute additional information via [`YearInfo::new_year()`] and by accessing
26//! the methods on [`YearInfo::keviyah`].
27//!
28//!
29//! # How this code works:
30//!
31//! ## How the Hebrew Calendar works
32//!
33//! The Hebrew calendar is a lunisolar calendar following a Metonic cycle: every 19 years, the pattern of
34//! leap years repeats. However, the precise month lengths vary from cycle to cycle: There are a handful of
35//! corrections performed to ensure that:
36//!
37//! - The lunar conjunction happens on or before the first day of the month
38//! - Yom Kippur is not before or after the Sabbath
39//! - Hoshana Rabbah is not on the Sabbath
40//!
41//! These corrections can be done using systematic calculations, which this code attempts to efficiently perform.
42//!
43//! ## Molad
44//!
45//! A molad is the time of a conjunction, the moment when the new moon occurs. The "Molad Tishrei" is
46//! the conjunction corresponding to the month Tishrei, the first month, so it is the molad that starts the new year.
47//! In this file we'll typically use "molad" to refer to the molad Tishrei of a year.
48//!
49//! The Hebrew calendar does not always start on the day of the molad Tishrei: it may be postponed one or two days.
50//! However, the time in the week that the molad occurs is sufficient to know when it gets postponed to.
51//!
52//! ## Keviyah
53//!
54//! See also: the [`Keviyah`] type.
55//!
56//! This is the core concept being used here. Everything you need to know about the characteristics
57//! of a hebrew year can be boiled down to a notion called the "keviyah" of a year. This
58//! encapsulates three bits of information:
59//!
60//! - What day of the week the year starts on
61//! - What the month lengths are
62//! - What day of the week Passover starts on.
63//!
64//! While this seems like many possible combinations, only fourteen of them are possible.
65//!
66//! Knowing the Keviyah of the year you can understand exactly what the lengths of each month are.
67//! Furthermore, if you know the week the year falls in, you can additionally understand what
68//! the precise day of the new year is.
69//!
70//! [`YearInfo`] encapsulates these two pieces of information: the [`Keviyah`] and the number of weeks
71//! since the epoch of the Hebrew calendar.
72//!
73//! ## The Four Gates table
74//!
75//! This is an efficient lookup based way of calculating the [`Keviyah`] for a year. In the Metonic cycle,
76//! there are four broad types of year: leap years, years preceding leap years, years succeeding leap years,
77//! and years sandwiched between leap years. For each of these year types, there is a partitioning
78//! of the week into seven segments, and the [`Keviyah`] of that year depends on which segment the molad falls
79//! in.
80//!
81//! So to calculate the [`Keviyah`] of a year, we can calculate its molad, pick the right partitioning based on the
82//! year type, and see where the molad falls in that table.
83
84use crate::hebrew::HEBREW_EPOCH;
85use crate::helpers::i64_to_saturated_i32;
86use crate::rata_die::RataDie;
87
88// A note on time notation
89//
90// Hebrew timekeeping has some differences from standard timekeeping. A Hebrew day is split into 24
91// hours, each split into 1080 ḥalakim ("parts", abbreviated "ḥal" or "p"). Furthermore, the Hebrew
92// day for calendrical purposes canonically starts at 6PM the previous evening, e.g. Hebrew Monday
93// starts on Sunday 6PM. (For non-calendrical observational purposes this varies and is based on
94// the sunset, but that is not too relevant for the algorithms here.)
95//
96// In this file an unqualified day of the week will refer to a standard weekday, and Hebrew weekdays
97// will be referred to as "Hebrew Sunday" etc. Sometimes the term "regular" or "standard" will be used
98// to refer to a standard weekday when we particularly wish to avoid ambiguity.
99//
100// Hebrew weeks start on Sunday. A common notation for times of the week looks like 2-5-204, which
101// means "second Hebrew Day of the week, 5h 204 ḥal", which is 5h 204 ḥal after the start of Hebrew
102// Monday (which is 23h:204ḥal on standard Sunday).
103//
104// Some resources will use ḥalakim notation when talking about time during a standard day. This
105// document will use standard `:` notation for this, as used above with 23h:204ḥal being equal to
106// 5h 204ḥal. In other words, if a time is notated using dashes or spaces, it is relative to the
107// hebrew start of day, whereas if it is notated using a colon, it is relative to midnight.
108//
109// Finally, Adjler, the resource we are using, uses both inclusive and exclusive time notation. It
110// is typical across resources using the 2-5-204 notation for the 2 to be "second day" as opposed
111// to "two full days have passed" (i.e., on the first day). However *in the context of
112// calculations* Adjler will use 1-5-204 to refer to stuff happening on Hebrew Monday, and clarify
113// it as (2)-5-204. This is because zero-indexing works better in calculations.
114//
115// Comparing these algorithms with the source in Adjler should be careful about this. All other
116// resources seem to universally 1-index in the dashes notation. This file will only use
117// zero-indexed notation when explicitly disambiguated, usually when talking about intervals.
118
119/// Calculate the number of months preceding the molad Tishrei for a given hebrew year (Tishrei is the first month)
120#[inline]
121fn months_preceding_molad(h_year: i32) -> i64 {
122 // Ft = INT((235N + 1 / 19))
123 // Where N = h_year - 1 (number of elapsed years since epoch)
124 // This math essentially comes from the Metonic cycle of 19 years containing
125 // 235 months: 12 months per year, plus an extra month for each of the 7 leap years.
126
127 (235 * (i64::from(h_year) - 1) + 1).div_euclid(19)
128}
129
130/// Conveniently create a constant for a ḥalakim (by default in 1-indexed notation). Produces a constant
131/// that tracks the number of ḥalakim since the beginning of the week
132macro_rules! ḥal {
133 ($d:literal-$h:literal-$p:literal) => {{
134 const CONSTANT: i32 = (($d - 1) * 24 + $h) * 1080 + $p;
135 CONSTANT
136 }};
137 (0-indexed $d:literal-$h:literal-$p:literal) => {{
138 const CONSTANT: i32 = ($d * 24 + $h) * 1080 + $p;
139 CONSTANT
140 }};
141}
142
143/// The molad Beherad is the first molad, i.e. the molad of the epoch year.
144/// It occurred on Oct 6, 3761 BC, 23h:204ḥal (Jerusalem Time, Julian Calendar)
145///
146/// Which is the second Hebrew day of the week (Hebrew Monday), 5h 204ḥal, 2-5-204.
147/// ("Beharad" בהרד is just a way of writing 2-5-204, ב-ה-רד using Hebrew numerals)
148///
149/// This is 31524ḥal after the start of the week (Saturday 6PM)
150///
151/// From Adjler Appendix A
152const MOLAD_BEHERAD_OFFSET: i32 = ḥal!(2 - 5 - 204);
153
154/// The amount of time a Hebrew lunation takes (in ḥalakim). This is not exactly the amount of time
155/// taken by one revolution of the moon (the real world seldom has events that are perfect integer
156/// multiples of 1080ths of an hour), but it is what the Hebrew calendar uses. This does mean that
157/// there will be drift over time with the actual state of the celestial sphere, however that is
158/// irrelevant since the actual state of the celestial sphere is not what is used for the Hebrew
159/// calendar.
160///
161/// This is 29-12-793 in zero-indexed notation. It is equal to 765433ḥal.
162/// From Adjler Appendix A
163const HEBREW_LUNATION_TIME: i32 = ḥal!(0-indexed 29-12-793);
164
165/// The number of ḥalakim in a week
166///
167/// (This is 181440)
168const ḤALAKIM_IN_WEEK: i64 = 1080 * 24 * 7;
169
170/// The minumum hebrew year supported by this code (this is the minimum value for i32)
171pub const HEBREW_MIN_YEAR: i32 = i32::MIN;
172/// The minumum R.D. supported by this code (this code will clamp outside of it)
173// (this constant is verified by tests)
174pub const HEBREW_MIN_RD: RataDie = RataDie::new(-784362951979);
175/// The maximum hebrew year supported by this code (this is the maximum alue for i32)
176// (this constant is verified by tests)
177pub const HEBREW_MAX_YEAR: i32 = i32::MAX;
178/// The maximum R.D. supported by this code (this is the last day in [`HEBREW_MAX_YEAR`])
179// (this constant is verified by tests)
180pub const HEBREW_MAX_RD: RataDie = RataDie::new(784360204356);
181
182/// Given a Hebrew Year, returns its molad specified as:
183///
184/// - The number of weeks since the week of Beharad (Oct 6, 3761 BCE Julian)
185/// - The number of ḥalakim since the start of the week (Hebrew Sunday, starting on Saturday at 18:00)
186#[inline]
187fn molad_details(h_year: i32) -> (i64, i32) {
188 let months_preceding = months_preceding_molad(h_year);
189
190 // The molad tishri expressed in parts since the beginning of the week containing Molad of Beharad
191 // Formula from Adjler Appendix A
192 let molad = MOLAD_BEHERAD_OFFSET as i64 + months_preceding * HEBREW_LUNATION_TIME as i64;
193
194 // Split into quotient and remainder
195 let weeks_since_beharad = molad.div_euclid(ḤALAKIM_IN_WEEK);
196 let in_week = molad.rem_euclid(ḤALAKIM_IN_WEEK);
197
198 let in_week = i32::try_from(in_week);
199 debug_assert!(in_week.is_ok(), "ḤALAKIM_IN_WEEK should fit in an i32");
200
201 (weeks_since_beharad, in_week.unwrap_or(0))
202}
203
204/// Everything about a given year. Can be conveniently packed down into an i64 if needed.
205#[derive(Copy, Clone, Eq, PartialEq, Debug)]
206#[allow(clippy::exhaustive_structs)] // This may change but we're fine breaking this crate
207pub struct YearInfo {
208 /// The Keviyah of the year
209 pub keviyah: Keviyah,
210 /// How many full weeks have passed since the week of Beharad
211 pub weeks_since_beharad: i64,
212}
213
214impl YearInfo {
215 /// Compute the [`YearInfo`] for a given year
216 #[inline]
217 pub fn compute_for(h_year: i32) -> Self {
218 let (mut weeks_since_beharad, ḥalakim) = molad_details(h_year);
219
220 let cycle_type = MetonicCycleType::for_h_year(h_year);
221
222 let keviyah = keviyah_for(cycle_type, ḥalakim);
223
224 // The last six hours of Hebrew Saturday (i.e. after noon on Regular Saturday)
225 // get unconditionally postponed to Monday according to the Four Gates table. This
226 // puts us in a new week!
227 if ḥalakim >= ḥal!(7 - 18 - 0) {
228 weeks_since_beharad += 1;
229 }
230
231 Self {
232 keviyah,
233 weeks_since_beharad,
234 }
235 }
236
237 /// Returns the [`YearInfo`] and `h_year` for the year containing `date`
238 ///
239 /// This will clamp the R.D. such that the hebrew year is within range for i32
240 #[inline]
241 pub fn year_containing_rd(date: RataDie) -> (Self, i32) {
242 // 35975351/98496 is the mean year length for a Hebrew year
243 // (from Reingold, ch 8.2, in implementation for fixed-from-hebrew)
244 //
245 // +1 because the epoch is new year of year 1
246 // Before the epoch the division will round up (towards 0), so we need to
247 // subtract 1, which is the same as not adding the 1.
248 let mut h_year = i64_to_saturated_i32(
249 (date - HEBREW_EPOCH) * 98496 / 35975351 + (date >= HEBREW_EPOCH) as i64,
250 );
251
252 let mut year = Self::compute_for(h_year);
253
254 if date < year.new_year() && h_year > i32::MIN {
255 h_year -= 1;
256 year = Self::compute_for(h_year)
257 } else if date >= year.new_year() + year.keviyah.year_length() as i64 && h_year < i32::MAX {
258 h_year += 1;
259 year = Self::compute_for(h_year)
260 }
261
262 (year, h_year)
263 }
264
265 /// Compute the date of New Year's Day
266 #[inline]
267 pub fn new_year(self) -> RataDie {
268 // Beharad started on Monday
269 const BEHARAD_START_OF_YEAR: StartOfYear = StartOfYear::Monday;
270 let days_since_beharad = (self.weeks_since_beharad * 7)
271 + self.keviyah.start_of_year() as i64
272 - BEHARAD_START_OF_YEAR as i64;
273 HEBREW_EPOCH + days_since_beharad
274 }
275}
276
277/// The Keviyah (קביעה) of a year.
278///
279/// A year may be one of fourteen types, categorized by the day of
280/// week of the new year (the first number, 1 = Sunday), the type of year (Deficient, Regular,
281/// Complete), and the weekday of the first day of Passover. The last segment disambiguates
282/// between cases that have the same first two but differ on whether they are leap years (since
283/// Passover happens in Nisan, after the leap month Adar).
284///
285/// The discriminant values of these entries are according to
286/// the positions these keviyot appear in the Four Gates table,
287/// with the leap year ones being offset by 7. We don't directly rely on this
288/// property but it is useful for potential bitpacking, and we use it as a way
289/// to double-check that the four gates code is set up correctly. We do directly
290/// rely on the leap-keviyot being after the regular ones (and starting with בחה) in `is_leap`.
291///
292/// For people unsure if their editor supports bidirectional text,
293/// the first Keviyah (2D3) is Bet (ב), Ḥet (ח), Gimel (ג).
294///
295/// (The Hebrew values are used in code for two reasons: firstly, Rust identifiers
296/// can't start with a number, and secondly, sources differ on the Latin alphanumeric notation
297/// but use identical Hebrew notation)
298#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
299#[allow(clippy::exhaustive_enums)] // There are only 14 keviyot (and always have been)
300pub enum Keviyah {
301 // Regular years
302 /// 2D3
303 בחג = 0,
304 /// 2C5
305 בשה = 1,
306 /// 3R5
307 גכה = 2,
308 /// 5R7
309 הכז = 3,
310 /// 5C1
311 השא = 4,
312 /// 7D1
313 זחא = 5,
314 /// 7C3
315 זשג = 6,
316
317 // Leap years
318 /// 2D5
319 בחה = 7,
320 /// 2C7
321 בשז = 8,
322 /// 3R7
323 גכז = 9,
324 /// 5D1
325 החא = 10,
326 /// 5C3
327 השג = 11,
328 /// 7D3
329 זחג = 12,
330 /// 7C5
331 זשה = 13,
332}
333
334/// The type of year it is
335#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
336#[allow(clippy::exhaustive_enums)] // This is intrinsic to the calendar
337pub enum YearType {
338 /// חסרה: both Ḥeshvan and Kislev have 29 days
339 Deficient = -1,
340 /// כסדרה: Ḥeshvan has 29, Kislev has 30
341 Regular = 0,
342 /// שלמה: both Ḥeshvan and Kislev have 30 days
343 Complete = 1,
344}
345
346impl YearType {
347 /// The length correction from a regular year (354/385)
348 fn length_correction(self) -> i8 {
349 self as i8
350 }
351}
352/// The day of the new year. Only these four days are permitted.
353#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
354#[allow(clippy::exhaustive_enums)] // This is intrinsic to the calendar
355pub enum StartOfYear {
356 // Compiler forced me to document these, <https://en.wikipedia.org/wiki/Nowe_Ateny>
357 /// Monday (everyone knows what Monday is)
358 Monday = 2,
359 /// Tuesday (everyone knows what Tuesday is)
360 Tuesday = 3,
361 /// Thursday (everyone knows what Thursday is)
362 Thursday = 5,
363 /// Saturday (everyone knows what Saturday is)
364 Saturday = 7,
365}
366
367/// Normalized month constant for Tishrei
368///
369/// These are not ordinal months, rather these are the month number in a regular year
370/// Adar, Adar I and Adar II all normalize to 6
371pub const TISHREI: u8 = 1;
372/// Normalized month constant (see [`TISHREI`])
373pub const ḤESHVAN: u8 = 2;
374/// Normalized month constant (see [`TISHREI`])
375pub const KISLEV: u8 = 3;
376/// Normalized month constant (see [`TISHREI`])
377pub const TEVET: u8 = 4;
378/// Normalized month constant (see [`TISHREI`])
379pub const SHEVAT: u8 = 5;
380/// Normalized month constant (see [`TISHREI`])
381pub const ADAR: u8 = 6;
382/// Normalized month constant (see [`TISHREI`])
383pub const NISAN: u8 = 7;
384/// Normalized month constant (see [`TISHREI`])
385pub const IYYAR: u8 = 8;
386/// Normalized month constant (see [`TISHREI`])
387pub const SIVAN: u8 = 9;
388/// Normalized month constant (see [`TISHREI`])
389pub const TAMMUZ: u8 = 10;
390/// Normalized month constant (see [`TISHREI`])
391pub const AV: u8 = 11;
392/// Normalized month constant (see [`TISHREI`])
393pub const ELUL: u8 = 12;
394
395impl Keviyah {
396 /// Get the type of year for this Keviyah.
397 ///
398 /// Comes from the second letter in this Keviyah:
399 /// ח = D, כ = R, ש = C
400 #[inline]
401 pub fn year_type(self) -> YearType {
402 match self {
403 Self::בחג => YearType::Deficient,
404 Self::בשה => YearType::Complete,
405 Self::גכה => YearType::Regular,
406 Self::הכז => YearType::Regular,
407 Self::השא => YearType::Complete,
408 Self::זחא => YearType::Deficient,
409 Self::זשג => YearType::Complete,
410 Self::בחה => YearType::Deficient,
411 Self::בשז => YearType::Complete,
412 Self::גכז => YearType::Regular,
413 Self::החא => YearType::Deficient,
414 Self::השג => YearType::Complete,
415 Self::זחג => YearType::Deficient,
416 Self::זשה => YearType::Complete,
417 }
418 }
419 /// Get the day of the new year for this Keviyah
420 ///
421 /// Comes from the first letter in this Keviyah:
422 /// ב = 2 = Monday, ג = 3 = Tuesday, ה = 5 = Thursday, ז = 7 = Saturday
423 #[inline]
424 pub fn start_of_year(self) -> StartOfYear {
425 match self {
426 Self::בחג => StartOfYear::Monday,
427 Self::בשה => StartOfYear::Monday,
428 Self::גכה => StartOfYear::Tuesday,
429 Self::הכז => StartOfYear::Thursday,
430 Self::השא => StartOfYear::Thursday,
431 Self::זחא => StartOfYear::Saturday,
432 Self::זשג => StartOfYear::Saturday,
433 Self::בחה => StartOfYear::Monday,
434 Self::בשז => StartOfYear::Monday,
435 Self::גכז => StartOfYear::Tuesday,
436 Self::החא => StartOfYear::Thursday,
437 Self::השג => StartOfYear::Thursday,
438 Self::זחג => StartOfYear::Saturday,
439 Self::זשה => StartOfYear::Saturday,
440 }
441 }
442
443 /// Given an ordinal, civil month (1-indexed month starting at Tishrei)
444 /// return its length
445 // this function is branch-free
446 #[inline]
447 pub fn month_len(self, ordinal_month: u8) -> u8 {
448 let year_type = self.year_type();
449 let is_leap = self.is_leap();
450
451 // In a leap year, ordinals after Adar correspond to the previous month number.
452 let month_number = ordinal_month - (is_leap && ordinal_month >= 6) as u8;
453
454 29 + (
455 // Months with odd month numbers are long.
456 month_number % 2
457 // Ḥeshvan is long in complete years
458 + (ordinal_month == 2 && year_type == YearType::Complete) as u8
459 // Kislev is short in deficient years
460 - (ordinal_month == 3 && year_type == YearType::Deficient) as u8
461 )
462 }
463
464 /// Get the number of days preceding this month
465 // this function is branch-free
466 #[inline]
467 pub fn days_preceding(self, ordinal_month: u8) -> u16 {
468 let year_type = self.year_type();
469 let is_leap = self.is_leap();
470
471 // In a leap year, ordinals after Adar correspond to the previous month number.
472 let month_number = ordinal_month - (is_leap && ordinal_month > 6) as u8;
473
474 29 * (ordinal_month as u16 - 1)
475 + (
476 // Months with odd month numbers are long.
477 month_number / 2
478 // Adar I is long
479 + (is_leap && ordinal_month > 6) as u8
480 // Ḥeshvan is long in complete years
481 + (ordinal_month > 2 && year_type == YearType::Complete) as u8
482 // Kislev is short in deficient years
483 - (ordinal_month > 3 && year_type == YearType::Deficient) as u8
484 ) as u16
485 }
486
487 /// Given a 1-indexed day of the year, return the ordinal month and day as (month, day).
488 pub fn month_day_for(self, day_of_year: u16) -> (u8, u8) {
489 // We divide by 30, not 29, to account for the case where all months before this
490 // were length 30 (possible near the beginning of the year)
491 let mut month = ((day_of_year - 1) / 30) as u8 + 1;
492 let mut days_before_month = self.days_preceding(month);
493 let mut last_day_of_month = self.days_preceding(month + 1);
494
495 while day_of_year > last_day_of_month {
496 month += 1;
497 days_before_month = last_day_of_month;
498 last_day_of_month = self.days_preceding(month + 1);
499 }
500
501 (month, (day_of_year - days_before_month) as u8)
502 }
503
504 /// Return the last ordinal month and day in this year as (month, day)
505 #[inline]
506 pub fn last_month_day_in_year(self) -> (u8, u8) {
507 (12 + self.is_leap() as u8, 29)
508 }
509
510 /// Whether this year is a leap year
511 #[inline]
512 pub fn is_leap(self) -> bool {
513 debug_assert_eq!(Self::בחה as u8, 7, "Representation of keviyot changed!");
514 // Because we have arranged our keviyot such that all leap keviyot come after
515 // the regular ones, this just a comparison
516 self >= Self::בחה
517 }
518
519 /// Given the hebrew year for this Keviyah, calculate the [`YearInfo`]
520 #[inline]
521 pub fn year_info(self, h_year: i32) -> YearInfo {
522 let (mut weeks_since_beharad, ḥalakim) = molad_details(h_year);
523
524 // The last six hours of Hebrew Saturday (i.e. after noon on Regular Saturday)
525 // get unconditionally postponed to Monday according to the Four Gates table. This
526 // puts us in a new week!
527 if ḥalakim >= ḥal!(7 - 18 - 0) {
528 weeks_since_beharad += 1;
529 }
530
531 YearInfo {
532 keviyah: self,
533 weeks_since_beharad,
534 }
535 }
536
537 /// How many days are in this year
538 #[inline]
539 pub fn year_length(self) -> u16 {
540 let base_year_length = if self.is_leap() { 384 } else { 354 };
541
542 (base_year_length + i16::from(self.year_type().length_correction())) as u16
543 }
544 /// Construct this from an integer between 0 and 13
545 ///
546 /// Potentially useful for bitpacking
547 #[inline]
548 pub fn from_integer(integer: u8) -> Self {
549 debug_assert!(
550 integer < 14,
551 "Keviyah::from_integer() takes in a number between 0 and 13 inclusive"
552 );
553 match integer {
554 0 => Self::בחג,
555 1 => Self::בשה,
556 2 => Self::גכה,
557 3 => Self::הכז,
558 4 => Self::השא,
559 5 => Self::זחא,
560 6 => Self::זשג,
561 7 => Self::בחה,
562 8 => Self::בשז,
563 9 => Self::גכז,
564 10 => Self::החא,
565 11 => Self::השג,
566 12 => Self::זחג,
567 _ => Self::זשה,
568 }
569 }
570}
571
572// Four Gates Table
573// ======================
574//
575// The Four Gates table is a table that takes the time of week of the molad
576// and produces a Keviyah for the year
577/// "Metonic cycle" in general refers to any 19-year repeating pattern used by lunisolar
578/// calendars. The Hebrew calendar uses one where years 3, 6, 8, 11, 14, 17, 19
579/// are leap years.
580///
581/// The Hebrew calendar further categorizes regular years as whether they come before/after/or
582/// between leap years, and this is used when performing lookups.
583#[derive(Copy, Clone, Eq, PartialEq, Debug)]
584enum MetonicCycleType {
585 /// Before a leap year (2, 5, 10, 13, 16)
586 LMinusOne,
587 /// After a leap year (1, 4, 9, 12, 15)
588 LPlusOne,
589 /// Between leap years (7. 18)
590 LPlusMinusOne,
591 /// Leap year (3, 6, 8, 11, 14, 17, 19)
592 Leap,
593}
594
595impl MetonicCycleType {
596 fn for_h_year(h_year: i32) -> Self {
597 // h_year is 1-indexed, and our metonic cycle year numberings
598 // are 1-indexed, so we really need to do `(h_year - 1) % 19 + 1`
599 //
600 // However, that is equivalent to `h_year % 19` provided you handle the
601 // fact that that operation will produce 0 instead of 19.
602 // Both numbers end up in our wildcard leap year arm so that's fine.
603 let remainder = h_year.rem_euclid(19);
604 match remainder {
605 // These numbers are 1-indexed
606 2 | 5 | 10 | 13 | 16 => Self::LMinusOne,
607 1 | 4 | 9 | 12 | 15 => Self::LPlusOne,
608 7 | 18 => Self::LPlusMinusOne,
609 _ => {
610 debug_assert!(matches!(remainder, 3 | 6 | 8 | 11 | 14 | 17 | 0 | 19));
611 Self::Leap
612 }
613 }
614 }
615}
616
617// The actual Four Gates tables.
618//
619// Each entry is a range (ending at the next entry), and it corresponds to the equivalent discriminant value of the Keviyah type.
620// Leap and regular years map to different Keviyah values, however regular years all map to the same set of
621// seven values, with differing ḥalakim bounds for each. The first entry in the Four Gates table straddles the end of the previous week
622// and the beginning of this one.
623//
624// The regular-year tables only differ by their third and last entries (We may be able to write this as more compact code)
625//
626// You can reference these tables from https://en.wikipedia.org/wiki/Hebrew_calendar#The_four_gates
627// or from Adjler (Appendix 4). Be sure to look at the Adjler table referring the "modern calendar", older tables
628// use slightly different numbers.
629const FOUR_GATES_LMINUSONE: [i32; 7] = [
630 ḥal!(7 - 18 - 0),
631 ḥal!(1 - 9 - 204),
632 ḥal!(2 - 18 - 0),
633 ḥal!(3 - 9 - 204),
634 ḥal!(5 - 9 - 204),
635 ḥal!(5 - 18 - 0),
636 ḥal!(6 - 9 - 204),
637];
638const FOUR_GATES_LPLUSONE: [i32; 7] = [
639 ḥal!(7 - 18 - 0),
640 ḥal!(1 - 9 - 204),
641 ḥal!(2 - 15 - 589),
642 ḥal!(3 - 9 - 204),
643 ḥal!(5 - 9 - 204),
644 ḥal!(5 - 18 - 0),
645 ḥal!(6 - 0 - 408),
646];
647
648const FOUR_GATES_LPLUSMINUSONE: [i32; 7] = [
649 ḥal!(7 - 18 - 0),
650 ḥal!(1 - 9 - 204),
651 ḥal!(2 - 15 - 589),
652 ḥal!(3 - 9 - 204),
653 ḥal!(5 - 9 - 204),
654 ḥal!(5 - 18 - 0),
655 ḥal!(6 - 9 - 204),
656];
657
658const FOUR_GATES_LEAP: [i32; 7] = [
659 ḥal!(7 - 18 - 0),
660 ḥal!(1 - 20 - 491),
661 ḥal!(2 - 18 - 0),
662 ḥal!(3 - 18 - 0),
663 ḥal!(4 - 11 - 695),
664 ḥal!(5 - 18 - 0),
665 ḥal!(6 - 20 - 491),
666];
667
668/// Perform the four gates calculation, giving you the Keviyah for a given year type and
669/// the ḥalakim-since-beginning-of-week of its molad Tishri
670#[inline]
671fn keviyah_for(year_type: MetonicCycleType, ḥalakim: i32) -> Keviyah {
672 let gate = match year_type {
673 MetonicCycleType::LMinusOne => FOUR_GATES_LMINUSONE,
674 MetonicCycleType::LPlusOne => FOUR_GATES_LPLUSONE,
675 MetonicCycleType::LPlusMinusOne => FOUR_GATES_LPLUSMINUSONE,
676 MetonicCycleType::Leap => FOUR_GATES_LEAP,
677 };
678
679 // Calculate the non-leap and leap keviyot for this year
680 // This could potentially be made more efficient by just finding
681 // the right window on `gate` and transmuting, but this unrolled loop should be fine too.
682 let keviyot = if ḥalakim >= gate[0] || ḥalakim < gate[1] {
683 (Keviyah::בחג, Keviyah::בחה)
684 } else if ḥalakim < gate[2] {
685 (Keviyah::בשה, Keviyah::בשז)
686 } else if ḥalakim < gate[3] {
687 (Keviyah::גכה, Keviyah::גכז)
688 } else if ḥalakim < gate[4] {
689 (Keviyah::הכז, Keviyah::החא)
690 } else if ḥalakim < gate[5] {
691 (Keviyah::השא, Keviyah::השג)
692 } else if ḥalakim < gate[6] {
693 (Keviyah::זחא, Keviyah::זחג)
694 } else {
695 (Keviyah::זשג, Keviyah::זשה)
696 };
697
698 // We have conveniently set the discriminant value of Keviyah to match the four gates index
699 // Let's just assert to make sure the table above is correct.
700 debug_assert!(
701 keviyot.0 as u8 + 7 == keviyot.1 as u8,
702 "The table above should produce matching-indexed keviyot for the leap/non-leap year"
703 );
704 #[cfg(debug_assertions)]
705 #[expect(clippy::indexing_slicing)] // debug_assertion code
706 if keviyot.0 as u8 == 0 {
707 // The first entry in the gates table straddles the ends of the week
708 debug_assert!(
709 ḥalakim >= gate[keviyot.0 as usize] || ḥalakim < gate[(keviyot.0 as usize + 1) % 7],
710 "The table above should produce the right indexed keviyah, instead found {keviyot:?} for time {ḥalakim} (year type {year_type:?})"
711 );
712 } else {
713 // Other entries must properly bound the ḥalakim
714 debug_assert!(
715 ḥalakim >= gate[keviyot.0 as usize] && ḥalakim < gate[(keviyot.0 as usize + 1) % 7],
716 "The table above should produce the right indexed keviyah, instead found {keviyot:?} for time {ḥalakim} (year type {year_type:?})"
717 );
718 }
719
720 if year_type == MetonicCycleType::Leap {
721 keviyot.1
722 } else {
723 keviyot.0
724 }
725}
726
727#[cfg(test)]
728mod test {
729 use super::*;
730 use crate::hebrew::BookHebrew;
731
732 #[test]
733 fn test_consts() {
734 assert_eq!(MOLAD_BEHERAD_OFFSET, 31524);
735 assert_eq!(ḤALAKIM_IN_WEEK, 181440);
736 // Adjler's printed value for this constant is incorrect (as confirmed by Adjler over email).
737 // Adjler is correct about the value being ḥal!(0-indexed 29-12-793).
738 // which matches the math used in `crate::hebrew::molad()` from Calendrical Calculations.
739 //
740 // The correct constant is seen in <https://en.wikibooks.org/wiki/Computer_Programming/Hebrew_calendar>
741 assert_eq!(HEBREW_LUNATION_TIME, 765433);
742 }
743
744 #[test]
745 fn test_roundtrip_days() {
746 for h_year in (1..10).chain(5775..5795).chain(10000..10010) {
747 let year_info = YearInfo::compute_for(h_year);
748 let ny = year_info.new_year();
749 for day in 1..=year_info.keviyah.year_length() {
750 let offset_date = ny + i64::from(day) - 1;
751 let (offset_yearinfo, offset_h_year) = YearInfo::year_containing_rd(offset_date);
752
753 assert_eq!(
754 offset_h_year, h_year,
755 "Backcomputed h_year should be same for day {day} in Hebrew Year {h_year}"
756 );
757 assert_eq!(
758 offset_yearinfo, year_info,
759 "Backcomputed YearInfo should be same for day {day} in Hebrew Year {h_year}"
760 );
761
762 let (month, day2) = year_info.keviyah.month_day_for(day);
763
764 let days_preceding = year_info.keviyah.days_preceding(month);
765
766 assert_eq!(
767 days_preceding + u16::from(day2),
768 day,
769 "{h_year}-{month}-{day2} should round trip for day-of-year {day}"
770 )
771 }
772 }
773 }
774 #[test]
775 fn test_book_parity() {
776 let mut last_year = None;
777 for h_year in (1..100).chain(5600..5900).chain(10000..10100) {
778 let book_date = BookHebrew::from_civil_date(h_year, 1, 1);
779 let book_ny = BookHebrew::fixed_from_book_hebrew(book_date);
780 let kv_yearinfo = YearInfo::compute_for(h_year);
781 let kv_ny = kv_yearinfo.new_year();
782 assert_eq!(
783 book_ny,
784 kv_ny,
785 "Book and Keviyah-based years should match for Hebrew Year {h_year}. Got YearInfo {kv_yearinfo:?}"
786 );
787 let book_is_leap = BookHebrew::is_hebrew_leap_year(h_year);
788 assert_eq!(
789 book_is_leap,
790 kv_yearinfo.keviyah.is_leap(),
791 "Book and Keviyah-based years should match for Hebrew Year {h_year}. Got YearInfo {kv_yearinfo:?}"
792 );
793
794 let book_year_len = BookHebrew::days_in_book_hebrew_year(h_year);
795 let book_year_type = match book_year_len {
796 355 | 385 => YearType::Complete,
797 354 | 384 => YearType::Regular,
798 353 | 383 => YearType::Deficient,
799 _ => unreachable!("Found unexpected book year len {book_year_len}"),
800 };
801 assert_eq!(
802 book_year_type,
803 kv_yearinfo.keviyah.year_type(),
804 "Book and Keviyah-based years should match for Hebrew Year {h_year}. Got YearInfo {kv_yearinfo:?}"
805 );
806
807 let kv_recomputed_yearinfo = kv_yearinfo.keviyah.year_info(h_year);
808 assert_eq!(
809 kv_recomputed_yearinfo,
810 kv_yearinfo,
811 "Recomputed YearInfo should match for Hebrew Year {h_year}. Got YearInfo {kv_yearinfo:?}"
812 );
813
814 let year_len = kv_yearinfo.keviyah.year_length();
815
816 let month_range = if kv_yearinfo.keviyah.is_leap() {
817 1..14
818 } else {
819 1..13
820 };
821
822 let mut days_preceding = 0;
823
824 for month in month_range {
825 let kv_month_len = kv_yearinfo.keviyah.month_len(month);
826 let book_date = BookHebrew::from_civil_date(h_year, month, 1);
827 let book_month_len =
828 BookHebrew::last_day_of_book_hebrew_month(book_date.year, book_date.month);
829 assert_eq!(kv_month_len, book_month_len, "Month lengths should be same for ordinal hebrew month {month} in year {h_year}. Got YearInfo {kv_yearinfo:?}");
830
831 assert_eq!(days_preceding, kv_yearinfo.keviyah.days_preceding(month), "Days preceding should be the sum of preceding days for ordinal hebrew month {month} in year {h_year}. Got YearInfo {kv_yearinfo:?}");
832 days_preceding += u16::from(kv_month_len);
833 }
834
835 for offset in [0, 1, 100, year_len - 100, year_len - 2, year_len - 1] {
836 let offset_date = kv_ny + offset.into();
837 let (offset_yearinfo, offset_h_year) = YearInfo::year_containing_rd(offset_date);
838
839 assert_eq!(offset_h_year, h_year, "Backcomputed h_year should be same for date {offset_date:?} in Hebrew Year {h_year} (offset from ny {offset})");
840 assert_eq!(offset_yearinfo, kv_yearinfo, "Backcomputed YearInfo should be same for date {offset_date:?} in Hebrew Year {h_year} (offset from ny {offset})");
841 }
842
843 if let Some((last_h_year, predicted_ny)) = last_year {
844 if last_h_year + 1 == h_year {
845 assert_eq!(predicted_ny, kv_ny, "{last_h_year}'s YearInfo predicts New Year {predicted_ny:?}, which does not match current new year. Got YearInfo {kv_yearinfo:?}");
846 }
847 }
848
849 last_year = Some((h_year, kv_ny + year_len.into()))
850 }
851 }
852 #[test]
853 fn test_minmax() {
854 let min = YearInfo::compute_for(HEBREW_MIN_YEAR);
855 let min_ny = min.new_year();
856 assert_eq!(min_ny, HEBREW_MIN_RD);
857
858 let (recomputed_yi, recomputed_y) = YearInfo::year_containing_rd(min_ny);
859 assert_eq!(recomputed_y, HEBREW_MIN_YEAR);
860 assert_eq!(recomputed_yi, min);
861
862 let max = YearInfo::compute_for(HEBREW_MAX_YEAR);
863 let max_ny = max.new_year();
864 // -1 since max_ny is also a part of the year
865 let max_last = max_ny + i64::from(max.keviyah.year_length()) - 1;
866 assert_eq!(max_last, HEBREW_MAX_RD);
867
868 let (recomputed_yi, recomputed_y) = YearInfo::year_containing_rd(max_last);
869 assert_eq!(recomputed_y, HEBREW_MAX_YEAR);
870 assert_eq!(recomputed_yi, max);
871 }
872
873 #[test]
874 fn test_leap_agreement() {
875 for year0 in -1000..1000 {
876 let year1 = year0 + 1;
877 let info0 = YearInfo::compute_for(year0);
878 let info1 = YearInfo::compute_for(year1);
879 let num_months = (info1.new_year() - info0.new_year()) / 29;
880 if info0.keviyah.is_leap() {
881 assert_eq!(num_months, 13, "{year0}");
882 } else {
883 assert_eq!(num_months, 12, "{year0}");
884 }
885 }
886 }
887 #[test]
888 fn test_issue_6262() {
889 // These are years where the molad ḥalakim is *exactly* ḥal!(7 - 18 - 0), we need
890 // to ensure the Saturday wraparound logic works correctly
891
892 let rds = [
893 // 72036-07-10
894 (26310435, 75795),
895 // 189394-12-06
896 (69174713, 193152),
897 ];
898
899 for (rd, expected_year) in rds {
900 let rd = RataDie::new(rd);
901 let (yi, year) = YearInfo::year_containing_rd(rd);
902 assert_eq!(year, expected_year);
903
904 let yi_recomputed = yi.keviyah.year_info(year);
905 assert_eq!(yi, yi_recomputed);
906 // Double check that these testcases are on the boundary
907 let (_weeks, ḥalakim) = molad_details(year);
908 assert_eq!(ḥalakim, ḥal!(7 - 18 - 0));
909 }
910 }
911}