Skip to main content

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}