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}