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::helpers::i64_to_i32;
85use crate::rata_die::RataDie;
86use core::cmp::Ordering;
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/// From Reingold (ch 8.2, in implementation for fixed-from-hebrew)
166const HEBREW_APPROX_YEAR_LENGTH: f64 = 35975351.0 / 98496.0;
167
168/// The number of ḥalakim in a week
169///
170/// (This is 181440)
171const ḤALAKIM_IN_WEEK: i64 = 1080 * 24 * 7;
172
173/// The Hebrew calendar epoch. It did not need to be postponed, so it occurs on Hebrew Monday, Oct 7, 3761 BCE (Julian),
174/// the same as the Molad Beherad.
175///
176/// (note that the molad Beherad occurs on standard Sunday, but because it happens after 6PM it is still Hebrew Monday)
177const HEBREW_CALENDAR_EPOCH: RataDie = crate::julian::fixed_from_julian_book_version(-3761, 10, 7);
178
179/// The minumum hebrew year supported by this code (this is the minimum value for i32)
180pub const HEBREW_MIN_YEAR: i32 = i32::MIN;
181/// The minumum R.D. supported by this code (this code will clamp outside of it)
182// (this constant is verified by tests)
183pub const HEBREW_MIN_RD: RataDie = RataDie::new(-784362951979);
184/// The maximum hebrew year supported by this code (this is the maximum alue for i32)
185// (this constant is verified by tests)
186pub const HEBREW_MAX_YEAR: i32 = i32::MAX;
187/// The maximum R.D. supported by this code (this is the last day in [`HEBREW_MAX_YEAR`])
188// (this constant is verified by tests)
189pub const HEBREW_MAX_RD: RataDie = RataDie::new(784360204356);
190
191/// Given a Hebrew Year, returns its molad specified as:
192///
193/// - The number of weeks since the week of Beharad (Oct 6, 3761 BCE Julian)
194/// - The number of ḥalakim since the start of the week (Hebrew Sunday, starting on Saturday at 18:00)
195#[inline]
196fn molad_details(h_year: i32) -> (i64, i32) {
197    let months_preceding = months_preceding_molad(h_year);
198
199    // The molad tishri expressed in parts since the beginning of the week containing Molad of Beharad
200    // Formula from Adjler Appendix A
201    let molad = MOLAD_BEHERAD_OFFSET as i64 + months_preceding * HEBREW_LUNATION_TIME as i64;
202
203    // Split into quotient and remainder
204    let weeks_since_beharad = molad.div_euclid(ḤALAKIM_IN_WEEK);
205    let in_week = molad.rem_euclid(ḤALAKIM_IN_WEEK);
206
207    let in_week = i32::try_from(in_week);
208    debug_assert!(in_week.is_ok(), "ḤALAKIM_IN_WEEK should fit in an i32");
209
210    (weeks_since_beharad, in_week.unwrap_or(0))
211}
212
213/// Everything about a given year. Can be conveniently packed down into an i64 if needed.
214#[derive(Copy, Clone, Eq, PartialEq, Debug)]
215#[allow(clippy::exhaustive_structs)] // This may change but we're fine breaking this crate
216pub struct YearInfo {
217    /// The Keviyah of the year
218    pub keviyah: Keviyah,
219    /// How many full weeks have passed since the week of Beharad
220    pub weeks_since_beharad: i64,
221}
222
223impl YearInfo {
224    /// Compute the YearInfo for a given year
225    #[inline]
226    pub fn compute_for(h_year: i32) -> Self {
227        let (mut weeks_since_beharad, ḥalakim) = molad_details(h_year);
228
229        let cycle_type = MetonicCycleType::for_h_year(h_year);
230
231        let keviyah = keviyah_for(cycle_type, ḥalakim);
232
233        // The last six hours of Hebrew Saturday (i.e. after noon on Regular Saturday)
234        // get unconditionally postponed to Monday according to the Four Gates table. This
235        // puts us in a new week!
236        if ḥalakim >= ḥal!(7 - 18 - 0) {
237            weeks_since_beharad += 1;
238        }
239
240        Self {
241            keviyah,
242            weeks_since_beharad,
243        }
244    }
245
246    /// Returns the YearInfo and h_year for the year containing `date`
247    ///
248    /// This will clamp the R.D. such that the hebrew year is within range for i32
249    #[inline]
250    pub fn year_containing_rd(date: RataDie) -> (Self, i32) {
251        #[allow(unused_imports)]
252        use core_maths::*;
253
254        let date = date.clamp(HEBREW_MIN_RD, HEBREW_MAX_RD);
255
256        let days_since_epoch = (date - HEBREW_CALENDAR_EPOCH) as f64;
257        let maybe_approx =
258            i64_to_i32(1 + days_since_epoch.div_euclid(HEBREW_APPROX_YEAR_LENGTH) as i64);
259        let approx = maybe_approx.unwrap_or_else(|e| e.saturate());
260
261        let yi = Self::compute_for(approx);
262
263        // compute if yi ⩼ rd
264        let cmp = yi.compare(date);
265
266        let (yi, h_year) = match cmp {
267            // The approx year is a year greater. Go one year down
268            Ordering::Greater => {
269                let prev = approx.saturating_sub(1);
270                (Self::compute_for(prev), prev)
271            }
272            // Bullseye
273            Ordering::Equal => (yi, approx),
274            // The approx year is a year lower. Go one year up.
275            Ordering::Less => {
276                let next = approx.saturating_add(1);
277                (Self::compute_for(next), next)
278            }
279        };
280
281        debug_assert!(yi.compare(date).is_eq(),
282                      "Date {date:?} calculated approximately to Hebrew Year {approx} (comparison: {cmp:?}), \
283                       should be contained in adjacent year {h_year} but that year is still {:?} it", yi.compare(date));
284
285        (yi, h_year)
286    }
287
288    /// Compare this year against a date. Returns Ordering::Greater
289    /// when this year is after the given date
290    ///
291    /// i.e. this is computing self ⩼ rd
292    fn compare(self, rd: RataDie) -> Ordering {
293        let ny = self.new_year();
294        let len = self.keviyah.year_length();
295
296        if rd < ny {
297            Ordering::Greater
298        } else if rd >= ny + len.into() {
299            Ordering::Less
300        } else {
301            Ordering::Equal
302        }
303    }
304
305    /// Compute the date of New Year's Day
306    #[inline]
307    pub fn new_year(self) -> RataDie {
308        // Beharad started on Monday
309        const BEHARAD_START_OF_YEAR: StartOfYear = StartOfYear::Monday;
310        let days_since_beharad = (self.weeks_since_beharad * 7)
311            + self.keviyah.start_of_year() as i64
312            - BEHARAD_START_OF_YEAR as i64;
313        HEBREW_CALENDAR_EPOCH + days_since_beharad
314    }
315}
316
317/// The Keviyah (קביעה) of a year.
318///
319/// A year may be one of fourteen types, categorized by the day of
320/// week of the new year (the first number, 1 = Sunday), the type of year (Deficient, Regular,
321/// Complete), and the day of week of the first day of Passover. The last segment disambiguates
322/// between cases that have the same first two but differ on whether they are leap years (since
323/// Passover happens in Nisan, after the leap month Adar).
324///
325/// The discriminant values of these entries are according to
326/// the positions these keviyot appear in the Four Gates table,
327/// with the leap year ones being offset by 7. We don't directly rely on this
328/// property but it is useful for potential bitpacking, and we use it as a way
329/// to double-check that the four gates code is set up correctly. We do directly
330/// rely on the leap-keviyot being after the regular ones (and starting with בחה) in is_leap.
331///
332/// For people unsure if their editor supports bidirectional text,
333/// the first Keviyah (2D3) is Bet (ב), Ḥet (ח), Gimel (ג).
334///
335/// (The Hebrew values are used in code for two reasons: firstly, Rust identifiers
336/// can't start with a number, and secondly, sources differ on the Latin alphanumeric notation
337/// but use identical Hebrew notation)
338#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
339#[allow(clippy::exhaustive_enums)] // There are only 14 keviyot (and always have been)
340pub enum Keviyah {
341    // Regular years
342    /// 2D3
343    בחג = 0,
344    /// 2C5
345    בשה = 1,
346    /// 3R5
347    גכה = 2,
348    /// 5R7
349    הכז = 3,
350    /// 5C1
351    השא = 4,
352    /// 7D1
353    זחא = 5,
354    /// 7C3
355    זשג = 6,
356
357    // Leap years
358    /// 2D5
359    בחה = 7,
360    /// 2C7
361    בשז = 8,
362    /// 3R7
363    גכז = 9,
364    /// 5D1
365    החא = 10,
366    /// 5C3
367    השג = 11,
368    /// 7D3
369    זחג = 12,
370    /// 7C5
371    זשה = 13,
372}
373
374/// The type of year it is
375#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
376#[allow(clippy::exhaustive_enums)] // This is intrinsic to the calendar
377pub enum YearType {
378    /// חסרה: both Ḥeshvan and Kislev have 29 days
379    Deficient = -1,
380    /// כסדרה: Ḥeshvan has 29, Kislev has 30
381    Regular = 0,
382    /// שלמה: both Ḥeshvan and Kislev have 30 days
383    Complete = 1,
384}
385
386impl YearType {
387    /// The length correction from a regular year (354/385)
388    fn length_correction(self) -> i8 {
389        self as i8
390    }
391
392    /// The length of Ḥeshvan
393    fn ḥeshvan_length(self) -> u8 {
394        if self == Self::Complete {
395            ḤESHVAN_DEFAULT_LEN + 1
396        } else {
397            ḤESHVAN_DEFAULT_LEN
398        }
399    }
400
401    /// The length correction of Kislev
402    fn kislev_length(self) -> u8 {
403        if self == Self::Deficient {
404            KISLEV_DEFAULT_LEN - 1
405        } else {
406            KISLEV_DEFAULT_LEN
407        }
408    }
409}
410/// The day of the new year. Only these four days are permitted.
411#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
412#[allow(clippy::exhaustive_enums)] // This is intrinsic to the calendar
413pub enum StartOfYear {
414    // Compiler forced me to document these, <https://en.wikipedia.org/wiki/Nowe_Ateny>
415    /// Monday (everyone knows what Monday is)
416    Monday = 2,
417    /// Tuesday (everyone knows what Tuesday is)
418    Tuesday = 3,
419    /// Thursday (everyone knows what Thursday is)
420    Thursday = 5,
421    /// Saturday (everyone knows what Saturday is)
422    Saturday = 7,
423}
424
425// Given a constant expression of the type FOO + BAR + BAZ convert
426// every element into a u16 and return
427macro_rules! u16_cvt(
428    // the $first / $rest pattern is needed since
429    // macros cannot use `+` as a separator in repetition
430    ($first:ident $(+ $rest:ident)*) => {
431        {
432            // make sure it is constant
433            // we use as here because it works in consts and in this context
434            // overflow will panic anyway
435            const COMPUTED: u16 = $first as u16 $(+ $rest as u16)*;
436            COMPUTED
437        }
438    };
439);
440
441// Month lengths (ref: https://en.wikipedia.org/wiki/Hebrew_calendar#Months)
442const TISHREI_LEN: u8 = 30;
443// except in Complete years
444const ḤESHVAN_DEFAULT_LEN: u8 = 29;
445// Except in Deficient years
446const KISLEV_DEFAULT_LEN: u8 = 30;
447const TEVET_LEN: u8 = 29;
448const SHEVAT_LEN: u8 = 30;
449const ADARI_LEN: u8 = 30;
450const ADAR_LEN: u8 = 29;
451const NISAN_LEN: u8 = 30;
452const IYYAR_LEN: u8 = 29;
453const SIVAN_LEN: u8 = 30;
454const TAMMUZ_LEN: u8 = 29;
455const AV_LEN: u8 = 30;
456const ELUL_LEN: u8 = 29;
457
458/// Normalized month constant for Tishrei
459///
460/// These are not ordinal months, rather these are the month number in a regular year
461/// Adar, Adar I and Adar II all normalize to 6
462pub const TISHREI: u8 = 1;
463/// Normalized month constant (see [`TISHREI`])
464pub const ḤESHVAN: u8 = 2;
465/// Normalized month constant (see [`TISHREI`])
466pub const KISLEV: u8 = 3;
467/// Normalized month constant (see [`TISHREI`])
468pub const TEVET: u8 = 4;
469/// Normalized month constant (see [`TISHREI`])
470pub const SHEVAT: u8 = 5;
471/// Normalized month constant (see [`TISHREI`])
472pub const ADAR: u8 = 6;
473/// Normalized month constant (see [`TISHREI`])
474pub const NISAN: u8 = 7;
475/// Normalized month constant (see [`TISHREI`])
476pub const IYYAR: u8 = 8;
477/// Normalized month constant (see [`TISHREI`])
478pub const SIVAN: u8 = 9;
479/// Normalized month constant (see [`TISHREI`])
480pub const TAMMUZ: u8 = 10;
481/// Normalized month constant (see [`TISHREI`])
482pub const AV: u8 = 11;
483/// Normalized month constant (see [`TISHREI`])
484pub const ELUL: u8 = 12;
485
486impl Keviyah {
487    /// Get the type of year for this Keviyah.
488    ///
489    /// Comes from the second letter in this Keviyah:
490    /// ח = D, כ = R, ש = C
491    #[inline]
492    pub fn year_type(self) -> YearType {
493        match self {
494            Self::בחג => YearType::Deficient,
495            Self::בשה => YearType::Complete,
496            Self::גכה => YearType::Regular,
497            Self::הכז => YearType::Regular,
498            Self::השא => YearType::Complete,
499            Self::זחא => YearType::Deficient,
500            Self::זשג => YearType::Complete,
501            Self::בחה => YearType::Deficient,
502            Self::בשז => YearType::Complete,
503            Self::גכז => YearType::Regular,
504            Self::החא => YearType::Deficient,
505            Self::השג => YearType::Complete,
506            Self::זחג => YearType::Deficient,
507            Self::זשה => YearType::Complete,
508        }
509    }
510    /// Get the day of the new year for this Keviyah
511    ///
512    /// Comes from the first letter in this Keviyah:
513    /// ב = 2 = Monday, ג = 3 = Tuesday, ה = 5 = Thursday, ז = 7 = Saturday
514    #[inline]
515    pub fn start_of_year(self) -> StartOfYear {
516        match self {
517            Self::בחג => StartOfYear::Monday,
518            Self::בשה => StartOfYear::Monday,
519            Self::גכה => StartOfYear::Tuesday,
520            Self::הכז => StartOfYear::Thursday,
521            Self::השא => StartOfYear::Thursday,
522            Self::זחא => StartOfYear::Saturday,
523            Self::זשג => StartOfYear::Saturday,
524            Self::בחה => StartOfYear::Monday,
525            Self::בשז => StartOfYear::Monday,
526            Self::גכז => StartOfYear::Tuesday,
527            Self::החא => StartOfYear::Thursday,
528            Self::השג => StartOfYear::Thursday,
529            Self::זחג => StartOfYear::Saturday,
530            Self::זשה => StartOfYear::Saturday,
531        }
532    }
533
534    /// Normalize the ordinal month to the "month number" in the year (ignoring
535    /// leap months), i.e. Adar and Adar II are both represented by 6.
536    ///
537    /// Returns None if given the index of Adar I (6 in a leap year)
538    #[inline]
539    fn normalized_ordinal_month(self, ordinal_month: u8) -> Option<u8> {
540        if self.is_leap() {
541            match ordinal_month.cmp(&6) {
542                // Adar I
543                Ordering::Equal => None,
544                Ordering::Less => Some(ordinal_month),
545                Ordering::Greater => Some(ordinal_month - 1),
546            }
547        } else {
548            Some(ordinal_month)
549        }
550    }
551
552    /// Given an ordinal, civil month (1-indexed month starting at Tishrei)
553    /// return its length
554    #[inline]
555    pub fn month_len(self, ordinal_month: u8) -> u8 {
556        // Normalize it to the month number
557        let Some(normalized_ordinal_month) = self.normalized_ordinal_month(ordinal_month) else {
558            return ADARI_LEN;
559        };
560        debug_assert!(normalized_ordinal_month <= 12 && normalized_ordinal_month > 0);
561        match normalized_ordinal_month {
562            TISHREI => TISHREI_LEN,
563            ḤESHVAN => self.year_type().ḥeshvan_length(),
564            KISLEV => self.year_type().kislev_length(),
565            TEVET => TEVET_LEN,
566            SHEVAT => SHEVAT_LEN,
567            ADAR => ADAR_LEN,
568            NISAN => NISAN_LEN,
569            IYYAR => IYYAR_LEN,
570            SIVAN => SIVAN_LEN,
571            TAMMUZ => TAMMUZ_LEN,
572            AV => AV_LEN,
573            ELUL => ELUL_LEN,
574            _ => {
575                debug_assert!(false, "Got unknown month index {ordinal_month}");
576                30
577            }
578        }
579    }
580
581    /// Get the number of days preceding this month
582    #[inline]
583    pub fn days_preceding(self, ordinal_month: u8) -> u16 {
584        // convenience constant to keep the additions smallish
585        // Number of days before (any) Adar in a regular year
586        const BEFORE_ADAR_DEFAULT_LEN: u16 = u16_cvt!(
587            TISHREI_LEN + ḤESHVAN_DEFAULT_LEN + KISLEV_DEFAULT_LEN + TEVET_LEN + SHEVAT_LEN
588        );
589
590        let Some(normalized_ordinal_month) = self.normalized_ordinal_month(ordinal_month) else {
591            // Get Adar I out of the way
592            let corrected =
593                BEFORE_ADAR_DEFAULT_LEN as i16 + i16::from(self.year_type().length_correction());
594            return corrected as u16;
595        };
596        debug_assert!(normalized_ordinal_month <= ELUL && normalized_ordinal_month > 0);
597
598        let year_type = self.year_type();
599
600        let mut days = match normalized_ordinal_month {
601            TISHREI => 0,
602            ḤESHVAN => u16_cvt!(TISHREI_LEN),
603            KISLEV => u16_cvt!(TISHREI_LEN) + u16::from(year_type.ḥeshvan_length()),
604            // Use default lengths after this, we'll apply the correction later
605            // (This helps optimize this into a simple jump table)
606            TEVET => u16_cvt!(TISHREI_LEN + ḤESHVAN_DEFAULT_LEN + KISLEV_DEFAULT_LEN),
607            SHEVAT => u16_cvt!(TISHREI_LEN + ḤESHVAN_DEFAULT_LEN + KISLEV_DEFAULT_LEN + TEVET_LEN),
608            ADAR => BEFORE_ADAR_DEFAULT_LEN,
609            NISAN => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN),
610            IYYAR => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN),
611            SIVAN => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYYAR_LEN),
612            TAMMUZ => {
613                u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYYAR_LEN + SIVAN_LEN)
614            }
615            #[rustfmt::skip]
616            AV => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYYAR_LEN + SIVAN_LEN + TAMMUZ_LEN),
617            #[rustfmt::skip]
618            _ => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYYAR_LEN + SIVAN_LEN + TAMMUZ_LEN + AV_LEN),
619        };
620
621        // If it is after Kislev and Ḥeshvan, we should add the year correction
622        if normalized_ordinal_month > KISLEV {
623            // Ensure the casts are fine
624            debug_assert!(days > 1 && year_type.length_correction().abs() <= 1);
625            days = (days as i16 + year_type.length_correction() as i16) as u16;
626        }
627
628        // In a leap year, after Adar (and including Adar II), we should add
629        // the length of Adar 1
630        if normalized_ordinal_month >= ADAR && self.is_leap() {
631            days += u16::from(ADARI_LEN);
632        }
633
634        days
635    }
636
637    /// Given a day of the year, return the ordinal month and day as (month, day).
638    pub fn month_day_for(self, mut day: u16) -> (u8, u8) {
639        for month in 1..14 {
640            let month_len = self.month_len(month);
641            if let Ok(day) = u8::try_from(day) {
642                if day <= month_len {
643                    return (month, day);
644                }
645            }
646            day -= u16::from(month_len);
647        }
648        debug_assert!(false, "Attempted to get Hebrew date for {day:?}, in keviyah {self:?}, didn't have enough days in the year");
649        self.last_month_day_in_year()
650    }
651
652    /// Return the last ordinal month and day in this year as (month, day)
653    #[inline]
654    pub fn last_month_day_in_year(self) -> (u8, u8) {
655        // Elul is always the last month of the year
656        if self.is_leap() {
657            (13, ELUL_LEN)
658        } else {
659            (12, ELUL_LEN)
660        }
661    }
662
663    /// Whether this year is a leap year
664    #[inline]
665    pub fn is_leap(self) -> bool {
666        debug_assert_eq!(Self::בחה as u8, 7, "Representation of keviyot changed!");
667        // Because we have arranged our keviyot such that all leap keviyot come after
668        // the regular ones, this just a comparison
669        self >= Self::בחה
670    }
671
672    /// Given the hebrew year for this Keviyah, calculate the YearInfo
673    #[inline]
674    pub fn year_info(self, h_year: i32) -> YearInfo {
675        let (mut weeks_since_beharad, ḥalakim) = molad_details(h_year);
676
677        // The last six hours of Hebrew Saturday (i.e. after noon on Regular Saturday)
678        // get unconditionally postponed to Monday according to the Four Gates table. This
679        // puts us in a new week!
680        if ḥalakim >= ḥal!(7 - 18 - 0) {
681            weeks_since_beharad += 1;
682        }
683
684        YearInfo {
685            keviyah: self,
686            weeks_since_beharad,
687        }
688    }
689
690    /// How many days are in this year
691    #[inline]
692    pub fn year_length(self) -> u16 {
693        let base_year_length = if self.is_leap() { 384 } else { 354 };
694
695        (base_year_length + i16::from(self.year_type().length_correction())) as u16
696    }
697    /// Construct this from an integer between 0 and 13
698    ///
699    /// Potentially useful for bitpacking
700    #[inline]
701    pub fn from_integer(integer: u8) -> Self {
702        debug_assert!(
703            integer < 14,
704            "Keviyah::from_integer() takes in a number between 0 and 13 inclusive"
705        );
706        match integer {
707            0 => Self::בחג,
708            1 => Self::בשה,
709            2 => Self::גכה,
710            3 => Self::הכז,
711            4 => Self::השא,
712            5 => Self::זחא,
713            6 => Self::זשג,
714            7 => Self::בחה,
715            8 => Self::בשז,
716            9 => Self::גכז,
717            10 => Self::החא,
718            11 => Self::השג,
719            12 => Self::זחג,
720            _ => Self::זשה,
721        }
722    }
723}
724
725// Four Gates Table
726// ======================
727//
728// The Four Gates table is a table that takes the time of week of the molad
729// and produces a Keviyah for the year
730/// "Metonic cycle" in general refers to any 19-year repeating pattern used by lunisolar
731/// calendars. The Hebrew calendar uses one where years 3, 6, 8, 11, 14, 17, 19
732/// are leap years.
733///
734/// The Hebrew calendar further categorizes regular years as whether they come before/after/or
735/// between leap years, and this is used when performing lookups.
736#[derive(Copy, Clone, Eq, PartialEq, Debug)]
737enum MetonicCycleType {
738    /// Before a leap year (2, 5, 10, 13, 16)
739    LMinusOne,
740    /// After a leap year (1, 4, 9, 12, 15)
741    LPlusOne,
742    /// Between leap years (7. 18)
743    LPlusMinusOne,
744    /// Leap year (3, 6, 8, 11, 14, 17, 19)
745    Leap,
746}
747
748impl MetonicCycleType {
749    fn for_h_year(h_year: i32) -> Self {
750        // h_year is 1-indexed, and our metonic cycle year numberings
751        // are 1-indexed, so we really need to do `(h_year - 1) % 19 + 1`
752        //
753        // However, that is equivalent to `h_year % 19` provided you handle the
754        // fact that that operation will produce 0 instead of 19.
755        // Both numbers end up in our wildcard leap year arm so that's fine.
756        let remainder = h_year.rem_euclid(19);
757        match remainder {
758            // These numbers are 1-indexed
759            2 | 5 | 10 | 13 | 16 => Self::LMinusOne,
760            1 | 4 | 9 | 12 | 15 => Self::LPlusOne,
761            7 | 18 => Self::LPlusMinusOne,
762            _ => {
763                debug_assert!(matches!(remainder, 3 | 6 | 8 | 11 | 14 | 17 | 0 | 19));
764                Self::Leap
765            }
766        }
767    }
768}
769
770// The actual Four Gates tables.
771//
772// Each entry is a range (ending at the next entry), and it corresponds to the equivalent discriminant value of the Keviyah type.
773// Leap and regular years map to different Keviyah values, however regular years all map to the same set of
774// seven values, with differing ḥalakim bounds for each. The first entry in the Four Gates table straddles the end of the previous week
775// and the beginning of this one.
776//
777// The regular-year tables only differ by their third and last entries (We may be able to write this as more compact code)
778//
779// You can reference these tables from https://en.wikipedia.org/wiki/Hebrew_calendar#The_four_gates
780// or from Adjler (Appendix 4). Be sure to look at the Adjler table referring the "modern calendar", older tables
781// use slightly different numbers.
782const FOUR_GATES_LMINUSONE: [i32; 7] = [
783    ḥal!(7 - 18 - 0),
784    ḥal!(1 - 9 - 204),
785    ḥal!(2 - 18 - 0),
786    ḥal!(3 - 9 - 204),
787    ḥal!(5 - 9 - 204),
788    ḥal!(5 - 18 - 0),
789    ḥal!(6 - 9 - 204),
790];
791const FOUR_GATES_LPLUSONE: [i32; 7] = [
792    ḥal!(7 - 18 - 0),
793    ḥal!(1 - 9 - 204),
794    ḥal!(2 - 15 - 589),
795    ḥal!(3 - 9 - 204),
796    ḥal!(5 - 9 - 204),
797    ḥal!(5 - 18 - 0),
798    ḥal!(6 - 0 - 408),
799];
800
801const FOUR_GATES_LPLUSMINUSONE: [i32; 7] = [
802    ḥal!(7 - 18 - 0),
803    ḥal!(1 - 9 - 204),
804    ḥal!(2 - 15 - 589),
805    ḥal!(3 - 9 - 204),
806    ḥal!(5 - 9 - 204),
807    ḥal!(5 - 18 - 0),
808    ḥal!(6 - 9 - 204),
809];
810
811const FOUR_GATES_LEAP: [i32; 7] = [
812    ḥal!(7 - 18 - 0),
813    ḥal!(1 - 20 - 491),
814    ḥal!(2 - 18 - 0),
815    ḥal!(3 - 18 - 0),
816    ḥal!(4 - 11 - 695),
817    ḥal!(5 - 18 - 0),
818    ḥal!(6 - 20 - 491),
819];
820
821/// Perform the four gates calculation, giving you the Keviyah for a given year type and
822/// the ḥalakim-since-beginning-of-week of its molad Tishri
823#[inline]
824fn keviyah_for(year_type: MetonicCycleType, ḥalakim: i32) -> Keviyah {
825    let gate = match year_type {
826        MetonicCycleType::LMinusOne => FOUR_GATES_LMINUSONE,
827        MetonicCycleType::LPlusOne => FOUR_GATES_LPLUSONE,
828        MetonicCycleType::LPlusMinusOne => FOUR_GATES_LPLUSMINUSONE,
829        MetonicCycleType::Leap => FOUR_GATES_LEAP,
830    };
831
832    // Calculate the non-leap and leap keviyot for this year
833    // This could potentially be made more efficient by just finding
834    // the right window on `gate` and transmuting, but this unrolled loop should be fine too.
835    let keviyot = if ḥalakim >= gate[0] || ḥalakim < gate[1] {
836        (Keviyah::בחג, Keviyah::בחה)
837    } else if ḥalakim < gate[2] {
838        (Keviyah::בשה, Keviyah::בשז)
839    } else if ḥalakim < gate[3] {
840        (Keviyah::גכה, Keviyah::גכז)
841    } else if ḥalakim < gate[4] {
842        (Keviyah::הכז, Keviyah::החא)
843    } else if ḥalakim < gate[5] {
844        (Keviyah::השא, Keviyah::השג)
845    } else if ḥalakim < gate[6] {
846        (Keviyah::זחא, Keviyah::זחג)
847    } else {
848        (Keviyah::זשג, Keviyah::זשה)
849    };
850
851    // We have conveniently set the discriminant value of Keviyah to match the four gates index
852    // Let's just assert to make sure the table above is correct.
853    debug_assert!(
854        keviyot.0 as u8 + 7 == keviyot.1 as u8,
855        "The table above should produce matching-indexed keviyot for the leap/non-leap year"
856    );
857    #[cfg(debug_assertions)]
858    #[allow(clippy::indexing_slicing)] // debug_assertion code
859    if keviyot.0 as u8 == 0 {
860        // The first entry in the gates table straddles the ends of the week
861        debug_assert!(
862            ḥalakim >= gate[keviyot.0 as usize] || ḥalakim < gate[(keviyot.0 as usize + 1) % 7],
863            "The table above should produce the right indexed keviyah, instead found {keviyot:?} for time {ḥalakim} (year type {year_type:?})"
864        );
865    } else {
866        // Other entries must properly bound the ḥalakim
867        debug_assert!(
868            ḥalakim >= gate[keviyot.0 as usize] && ḥalakim < gate[(keviyot.0 as usize + 1) % 7],
869            "The table above should produce the right indexed keviyah, instead found {keviyot:?} for time {ḥalakim} (year type {year_type:?})"
870        );
871    }
872
873    if year_type == MetonicCycleType::Leap {
874        keviyot.1
875    } else {
876        keviyot.0
877    }
878}
879
880#[cfg(test)]
881mod test {
882    use super::*;
883    use crate::hebrew::{self, BookHebrew};
884
885    #[test]
886    fn test_consts() {
887        assert_eq!(MOLAD_BEHERAD_OFFSET, 31524);
888        assert_eq!(ḤALAKIM_IN_WEEK, 181440);
889        // Adjler's printed value for this constant is incorrect (as confirmed by Adjler over email).
890        // Adjler is correct about the value being ḥal!(0-indexed 29-12-793).
891        // which matches the math used in `crate::hebrew::molad()` from Calendrical Calculations.
892        //
893        // The correct constant is seen in <https://en.wikibooks.org/wiki/Computer_Programming/Hebrew_calendar>
894        assert_eq!(HEBREW_LUNATION_TIME, 765433);
895
896        // Nicer to have the code be self-contained, but always worth asserting
897        assert_eq!(HEBREW_CALENDAR_EPOCH, hebrew::FIXED_HEBREW_EPOCH);
898    }
899
900    #[test]
901    fn test_roundtrip_days() {
902        for h_year in (1..10).chain(5775..5795).chain(10000..10010) {
903            let year_info = YearInfo::compute_for(h_year);
904            let ny = year_info.new_year();
905            for day in 1..=year_info.keviyah.year_length() {
906                let offset_date = ny + i64::from(day) - 1;
907                let (offset_yearinfo, offset_h_year) = YearInfo::year_containing_rd(offset_date);
908
909                assert_eq!(
910                    offset_h_year, h_year,
911                    "Backcomputed h_year should be same for day {day} in Hebrew Year {h_year}"
912                );
913                assert_eq!(
914                    offset_yearinfo, year_info,
915                    "Backcomputed YearInfo should be same for day {day} in Hebrew Year {h_year}"
916                );
917
918                let (month, day2) = year_info.keviyah.month_day_for(day);
919
920                let days_preceding = year_info.keviyah.days_preceding(month);
921
922                assert_eq!(
923                    days_preceding + u16::from(day2),
924                    day,
925                    "{h_year}-{month}-{day2} should round trip for day-of-year {day}"
926                )
927            }
928        }
929    }
930    #[test]
931    fn test_book_parity() {
932        let mut last_year = None;
933        for h_year in (1..100).chain(5600..5900).chain(10000..10100) {
934            let book_date = BookHebrew::from_civil_date(h_year, 1, 1);
935            let book_ny = BookHebrew::fixed_from_book_hebrew(book_date);
936            let kv_yearinfo = YearInfo::compute_for(h_year);
937            let kv_ny = kv_yearinfo.new_year();
938            assert_eq!(
939                book_ny,
940                kv_ny,
941                "Book and Keviyah-based years should match for Hebrew Year {h_year}. Got YearInfo {kv_yearinfo:?}"
942            );
943            let book_is_leap = BookHebrew::is_hebrew_leap_year(h_year);
944            assert_eq!(
945                book_is_leap,
946                kv_yearinfo.keviyah.is_leap(),
947                "Book and Keviyah-based years should match for Hebrew Year {h_year}. Got YearInfo {kv_yearinfo:?}"
948            );
949
950            let book_year_len = BookHebrew::days_in_book_hebrew_year(h_year);
951            let book_year_type = match book_year_len {
952                355 | 385 => YearType::Complete,
953                354 | 384 => YearType::Regular,
954                353 | 383 => YearType::Deficient,
955                _ => unreachable!("Found unexpected book year len {book_year_len}"),
956            };
957            assert_eq!(
958                book_year_type,
959                kv_yearinfo.keviyah.year_type(),
960                "Book and Keviyah-based years should match for Hebrew Year {h_year}. Got YearInfo {kv_yearinfo:?}"
961            );
962
963            let kv_recomputed_yearinfo = kv_yearinfo.keviyah.year_info(h_year);
964            assert_eq!(
965                kv_recomputed_yearinfo,
966                kv_yearinfo,
967                "Recomputed YearInfo should match for Hebrew Year {h_year}. Got YearInfo {kv_yearinfo:?}"
968            );
969
970            let year_len = kv_yearinfo.keviyah.year_length();
971
972            let month_range = if kv_yearinfo.keviyah.is_leap() {
973                1..14
974            } else {
975                1..13
976            };
977
978            let mut days_preceding = 0;
979
980            for month in month_range {
981                let kv_month_len = kv_yearinfo.keviyah.month_len(month);
982                let book_date = BookHebrew::from_civil_date(h_year, month, 1);
983                let book_month_len =
984                    BookHebrew::last_day_of_book_hebrew_month(book_date.year, book_date.month);
985                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:?}");
986
987                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:?}");
988                days_preceding += u16::from(kv_month_len);
989            }
990
991            for offset in [0, 1, 100, year_len - 100, year_len - 2, year_len - 1] {
992                let offset_date = kv_ny + offset.into();
993                let (offset_yearinfo, offset_h_year) = YearInfo::year_containing_rd(offset_date);
994
995                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})");
996                assert_eq!(offset_yearinfo, kv_yearinfo, "Backcomputed YearInfo should be same for date {offset_date:?} in Hebrew Year {h_year} (offset from ny {offset})");
997            }
998
999            if let Some((last_h_year, predicted_ny)) = last_year {
1000                if last_h_year + 1 == h_year {
1001                    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:?}");
1002                }
1003            }
1004
1005            last_year = Some((h_year, kv_ny + year_len.into()))
1006        }
1007    }
1008    #[test]
1009    fn test_minmax() {
1010        let min = YearInfo::compute_for(HEBREW_MIN_YEAR);
1011        let min_ny = min.new_year();
1012        assert_eq!(min_ny, HEBREW_MIN_RD);
1013
1014        let (recomputed_yi, recomputed_y) = YearInfo::year_containing_rd(min_ny);
1015        assert_eq!(recomputed_y, HEBREW_MIN_YEAR);
1016        assert_eq!(recomputed_yi, min);
1017
1018        let max = YearInfo::compute_for(HEBREW_MAX_YEAR);
1019        let max_ny = max.new_year();
1020        // -1 since max_ny is also a part of the year
1021        let max_last = max_ny + i64::from(max.keviyah.year_length()) - 1;
1022        assert_eq!(max_last, HEBREW_MAX_RD);
1023
1024        let (recomputed_yi, recomputed_y) = YearInfo::year_containing_rd(max_last);
1025        assert_eq!(recomputed_y, HEBREW_MAX_YEAR);
1026        assert_eq!(recomputed_yi, max);
1027    }
1028
1029    #[test]
1030    fn test_leap_agreement() {
1031        for year0 in -1000..1000 {
1032            let year1 = year0 + 1;
1033            let info0 = YearInfo::compute_for(year0);
1034            let info1 = YearInfo::compute_for(year1);
1035            let num_months = (info1.new_year() - info0.new_year()) / 29;
1036            if info0.keviyah.is_leap() {
1037                assert_eq!(num_months, 13, "{year0}");
1038            } else {
1039                assert_eq!(num_months, 12, "{year0}");
1040            }
1041        }
1042    }
1043    #[test]
1044    fn test_issue_6262() {
1045        // These are years where the molad ḥalakim is *exactly* ḥal!(7 - 18 - 0), we need
1046        // to ensure the Saturday wraparound logic works correctly
1047
1048        let rds = [
1049            // 72036-07-10
1050            (26310435, 75795),
1051            // 189394-12-06
1052            (69174713, 193152),
1053        ];
1054
1055        for (rd, expected_year) in rds {
1056            let rd = RataDie::new(rd);
1057            let (yi, year) = YearInfo::year_containing_rd(rd);
1058            assert_eq!(year, expected_year);
1059
1060            let yi_recomputed = yi.keviyah.year_info(year);
1061            assert_eq!(yi, yi_recomputed);
1062            // Double check that these testcases are on the boundary
1063            let (_weeks, ḥalakim) = molad_details(year);
1064            assert_eq!(ḥalakim, ḥal!(7 - 18 - 0));
1065        }
1066    }
1067}