icu_calendar/cal/
hebrew.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5use crate::calendar_arithmetic::{ArithmeticDate, DateFieldsResolver, ToExtendedYear};
6use crate::error::{
7    DateError, DateFromFieldsError, EcmaReferenceYearError, MonthCodeError, UnknownEraError,
8};
9use crate::options::{DateAddOptions, DateDifferenceOptions};
10use crate::options::{DateFromFieldsOptions, Overflow};
11use crate::types::{DateFields, MonthInfo, ValidMonthCode};
12use crate::RangeError;
13use crate::{types, Calendar, Date};
14use ::tinystr::tinystr;
15use calendrical_calculations::hebrew_keviyah::{Keviyah, YearInfo};
16use calendrical_calculations::rata_die::RataDie;
17
18/// The [Hebrew Calendar](https://en.wikipedia.org/wiki/Hebrew_calendar)
19///
20/// The Hebrew calendar is a lunisolar calendar used as the Jewish liturgical calendar
21/// as well as an official calendar in Israel.
22///
23/// This implementation uses civil month numbering, where Tishrei is the first month of the year.
24///
25/// The precise algorithm used to calculate the Hebrew Calendar has [changed over time], with
26/// the modern one being in place since about 4536 AM (776 CE). This implementation extends
27/// proleptically for dates before that.
28///
29/// [changed over time]: https://hakirah.org/vol20AjdlerAppendices.pdf
30///
31/// This corresponds to the `"hebrew"` [CLDR calendar](https://unicode.org/reports/tr35/#UnicodeCalendarIdentifier).
32///
33/// # Era codes
34///
35/// This calendar uses a single era code `am`, Anno Mundi. Dates before this era use negative years.
36///
37/// # Months and days
38///
39/// The 12 months are called Tishrei (`M01`, 30 days), Ḥešvan (`M02`, 29/30 days),
40/// Kīslev (`M03`, 30/29 days), Ṭevet (`M04`, 29 days), Šəvaṭ (`M05`, 30 days), ʾĂdār (`M06`, 29 days),
41/// Nīsān (`M07`, 30 days), ʾĪyyar (`M08`, 29 days), Sivan (`M09`, 30 days), Tammūz (`M10`, 29 days),
42/// ʾAv (`M11`, 30 days), ʾElūl (`M12`, 29 days).
43///
44/// Due to Rosh Hashanah postponement rules, Ḥešvan and Kislev vary in length.
45///  
46/// In leap years (years 3, 6, 8, 11, 17, 19 in a 19-year cycle), the leap month Adar I (`M05L`, 30 days)
47/// is inserted before Adar, and Adar is called Adar II (the `formatting_code` returned by [`MonthInfo`]
48/// will be `M06L` to mark this, while the `standard_code` remains `M06`).
49///
50/// Standard years thus have 353-355 days, and leap years 383-385.
51#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord, Default)]
52#[allow(clippy::exhaustive_structs)] // unit struct
53pub struct Hebrew;
54
55/// The inner date type used for representing [`Date`]s of [`Hebrew`]. See [`Date`] and [`Hebrew`] for more details.
56#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
57pub struct HebrewDateInner(ArithmeticDate<Hebrew>);
58
59impl Hebrew {
60    /// Construct a new [`Hebrew`]
61    pub fn new() -> Self {
62        Hebrew
63    }
64}
65
66#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
67pub(crate) struct HebrewYearInfo {
68    keviyah: Keviyah,
69    value: i32,
70}
71
72impl ToExtendedYear for HebrewYearInfo {
73    fn to_extended_year(&self) -> i32 {
74        self.value
75    }
76}
77
78impl HebrewYearInfo {
79    /// Convenience method to compute for a given year. Don't use this if you actually need
80    /// a YearInfo that you want to call .new_year() on.
81    #[inline]
82    fn compute(value: i32) -> Self {
83        Self {
84            keviyah: YearInfo::compute_for(value).keviyah,
85            value,
86        }
87    }
88}
89
90impl DateFieldsResolver for Hebrew {
91    type YearInfo = HebrewYearInfo;
92    fn days_in_provided_month(info: HebrewYearInfo, ordinal_month: u8) -> u8 {
93        info.keviyah.month_len(ordinal_month)
94    }
95
96    fn months_in_provided_year(info: HebrewYearInfo) -> u8 {
97        if info.keviyah.is_leap() {
98            13
99        } else {
100            12
101        }
102    }
103
104    #[inline]
105    fn year_info_from_era(
106        &self,
107        era: &[u8],
108        era_year: i32,
109    ) -> Result<Self::YearInfo, UnknownEraError> {
110        match era {
111            b"am" => Ok(HebrewYearInfo::compute(era_year)),
112            _ => Err(UnknownEraError),
113        }
114    }
115
116    #[inline]
117    fn year_info_from_extended(&self, extended_year: i32) -> Self::YearInfo {
118        HebrewYearInfo::compute(extended_year)
119    }
120
121    fn reference_year_from_month_day(
122        &self,
123        month_code: types::ValidMonthCode,
124        day: u8,
125    ) -> Result<Self::YearInfo, EcmaReferenceYearError> {
126        // December 31, 1972 occurs on 4th month, 26th day, 5733 AM
127        let hebrew_year = match month_code.to_tuple() {
128            (1, false) => 5733,
129            (2, false) => match day {
130                // There is no day 30 in 5733 (there is in 5732)
131                ..=29 => 5733,
132                // Note (here and below): this must be > 29, not just == 30,
133                // since we have not yet applied a potential Overflow::Constrain.
134                _ => 5732,
135            },
136            (3, false) => match day {
137                // There is no day 30 in 5733 (there is in 5732)
138                ..=29 => 5733,
139                _ => 5732,
140            },
141            (4, false) => match day {
142                ..=26 => 5733,
143                _ => 5732,
144            },
145            (5..=12, false) => 5732,
146            // Neither 5731 nor 5732 is a leap year
147            (5, true) => 5730,
148            _ => {
149                return Err(EcmaReferenceYearError::MonthCodeNotInCalendar);
150            }
151        };
152        Ok(HebrewYearInfo::compute(hebrew_year))
153    }
154
155    fn ordinal_month_from_code(
156        &self,
157        year: &Self::YearInfo,
158        month_code: types::ValidMonthCode,
159        options: DateFromFieldsOptions,
160    ) -> Result<u8, MonthCodeError> {
161        let is_leap_year = year.keviyah.is_leap();
162        let ordinal_month = match month_code.to_tuple() {
163            (n @ 1..=12, false) => n + (n >= 6 && is_leap_year) as u8,
164            (5, true) => {
165                if is_leap_year {
166                    6
167                } else if matches!(options.overflow, Some(Overflow::Constrain)) {
168                    // M05L maps to M06 in a common year
169                    6
170                } else {
171                    return Err(MonthCodeError::NotInYear);
172                }
173            }
174            _ => return Err(MonthCodeError::NotInCalendar),
175        };
176        Ok(ordinal_month)
177    }
178
179    fn month_code_from_ordinal(
180        &self,
181        year: &Self::YearInfo,
182        ordinal_month: u8,
183    ) -> types::ValidMonthCode {
184        let is_leap = year.keviyah.is_leap();
185        ValidMonthCode::new_unchecked(
186            ordinal_month - (is_leap && ordinal_month >= 6) as u8,
187            ordinal_month == 6 && is_leap,
188        )
189    }
190}
191
192impl crate::cal::scaffold::UnstableSealed for Hebrew {}
193impl Calendar for Hebrew {
194    type DateInner = HebrewDateInner;
195    type Year = types::EraYear;
196    type DifferenceError = core::convert::Infallible;
197
198    fn from_codes(
199        &self,
200        era: Option<&str>,
201        year: i32,
202        month_code: types::MonthCode,
203        day: u8,
204    ) -> Result<Self::DateInner, DateError> {
205        ArithmeticDate::from_codes(era, year, month_code, day, self).map(HebrewDateInner)
206    }
207
208    #[cfg(feature = "unstable")]
209    fn from_fields(
210        &self,
211        fields: DateFields,
212        options: DateFromFieldsOptions,
213    ) -> Result<Self::DateInner, DateFromFieldsError> {
214        ArithmeticDate::from_fields(fields, options, self).map(HebrewDateInner)
215    }
216
217    fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
218        let (year_info, year) = YearInfo::year_containing_rd(rd);
219        let keviyah = year_info.keviyah;
220
221        // Obtaining a 1-indexed day-in-year value
222        let day_in_year = u16::try_from(rd - year_info.new_year() + 1).unwrap_or(u16::MAX);
223        let (month, day) = keviyah.month_day_for(day_in_year);
224
225        HebrewDateInner(ArithmeticDate::new_unchecked(
226            HebrewYearInfo {
227                keviyah,
228                value: year,
229            },
230            month,
231            day,
232        ))
233    }
234
235    fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
236        let ny = date.0.year.keviyah.year_info(date.0.year.value).new_year();
237        let days_preceding = date.0.year.keviyah.days_preceding(date.0.month);
238
239        // Need to subtract 1 since the new year is itself in this year
240        ny + i64::from(days_preceding) + i64::from(date.0.day) - 1
241    }
242
243    fn has_cheap_iso_conversion(&self) -> bool {
244        false
245    }
246
247    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
248        Self::months_in_provided_year(date.0.year)
249    }
250
251    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
252        date.0.year.keviyah.year_length()
253    }
254
255    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
256        Self::days_in_provided_month(date.0.year, date.0.month)
257    }
258
259    #[cfg(feature = "unstable")]
260    fn add(
261        &self,
262        date: &Self::DateInner,
263        duration: types::DateDuration,
264        options: DateAddOptions,
265    ) -> Result<Self::DateInner, DateError> {
266        date.0.added(duration, self, options).map(HebrewDateInner)
267    }
268
269    #[cfg(feature = "unstable")]
270    fn until(
271        &self,
272        date1: &Self::DateInner,
273        date2: &Self::DateInner,
274        options: DateDifferenceOptions,
275    ) -> Result<types::DateDuration, Self::DifferenceError> {
276        Ok(date1.0.until(&date2.0, self, options))
277    }
278
279    fn debug_name(&self) -> &'static str {
280        "Hebrew"
281    }
282
283    fn year_info(&self, date: &Self::DateInner) -> Self::Year {
284        let extended_year = date.0.year.value;
285        types::EraYear {
286            era_index: Some(0),
287            era: tinystr!(16, "am"),
288            year: extended_year,
289            extended_year,
290            ambiguity: types::YearAmbiguity::CenturyRequired,
291        }
292    }
293
294    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
295        date.0.year.keviyah.is_leap()
296    }
297
298    fn month(&self, date: &Self::DateInner) -> MonthInfo {
299        let valid_standard_code = self.month_code_from_ordinal(&date.0.year, date.0.month);
300
301        let valid_formatting_code = if valid_standard_code.number() == 6 && date.0.month == 7 {
302            ValidMonthCode::new_unchecked(6, true) // M06L
303        } else {
304            valid_standard_code
305        };
306
307        types::MonthInfo {
308            ordinal: date.0.month,
309            standard_code: valid_standard_code.to_month_code(),
310            valid_standard_code,
311            formatting_code: valid_formatting_code.to_month_code(),
312            valid_formatting_code,
313        }
314    }
315
316    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
317        types::DayOfMonth(date.0.day)
318    }
319
320    fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
321        types::DayOfYear(date.0.year.keviyah.days_preceding(date.0.month) + date.0.day as u16)
322    }
323
324    fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
325        Some(crate::preferences::CalendarAlgorithm::Hebrew)
326    }
327}
328
329impl Date<Hebrew> {
330    /// This method uses an ordinal month, which is probably not what you want.
331    ///
332    /// Use [`Date::try_new_from_codes`]
333    #[deprecated(since = "2.1.0", note = "use `Date::try_new_from_codes`")]
334    pub fn try_new_hebrew(
335        year: i32,
336        ordinal_month: u8,
337        day: u8,
338    ) -> Result<Date<Hebrew>, RangeError> {
339        let year = HebrewYearInfo::compute(year);
340
341        ArithmeticDate::try_from_ymd(year, ordinal_month, day)
342            .map(HebrewDateInner)
343            .map(|inner| Date::from_raw(inner, Hebrew))
344    }
345}
346
347#[cfg(test)]
348mod tests {
349
350    use super::*;
351    use crate::types::MonthCode;
352
353    pub const TISHREI: ValidMonthCode = ValidMonthCode::new_unchecked(1, false);
354    pub const ḤESHVAN: ValidMonthCode = ValidMonthCode::new_unchecked(2, false);
355    pub const KISLEV: ValidMonthCode = ValidMonthCode::new_unchecked(3, false);
356    pub const TEVET: ValidMonthCode = ValidMonthCode::new_unchecked(4, false);
357    pub const SHEVAT: ValidMonthCode = ValidMonthCode::new_unchecked(5, false);
358    pub const ADARI: ValidMonthCode = ValidMonthCode::new_unchecked(5, true);
359    pub const ADAR: ValidMonthCode = ValidMonthCode::new_unchecked(6, false);
360    pub const NISAN: ValidMonthCode = ValidMonthCode::new_unchecked(7, false);
361    pub const IYYAR: ValidMonthCode = ValidMonthCode::new_unchecked(8, false);
362    pub const SIVAN: ValidMonthCode = ValidMonthCode::new_unchecked(9, false);
363    pub const TAMMUZ: ValidMonthCode = ValidMonthCode::new_unchecked(10, false);
364    pub const AV: ValidMonthCode = ValidMonthCode::new_unchecked(11, false);
365    pub const ELUL: ValidMonthCode = ValidMonthCode::new_unchecked(12, false);
366
367    /// The leap years used in the tests below
368    const LEAP_YEARS_IN_TESTS: [i32; 1] = [5782];
369    /// (iso, hebrew) pairs of testcases. If any of the years here
370    /// are leap years please add them to LEAP_YEARS_IN_TESTS (we have this manually
371    /// so we don't end up exercising potentially buggy codepaths to test this)
372    #[expect(clippy::type_complexity)]
373    const ISO_HEBREW_DATE_PAIRS: [((i32, u8, u8), (i32, ValidMonthCode, u8)); 48] = [
374        ((2021, 1, 10), (5781, TEVET, 26)),
375        ((2021, 1, 25), (5781, SHEVAT, 12)),
376        ((2021, 2, 10), (5781, SHEVAT, 28)),
377        ((2021, 2, 25), (5781, ADAR, 13)),
378        ((2021, 3, 10), (5781, ADAR, 26)),
379        ((2021, 3, 25), (5781, NISAN, 12)),
380        ((2021, 4, 10), (5781, NISAN, 28)),
381        ((2021, 4, 25), (5781, IYYAR, 13)),
382        ((2021, 5, 10), (5781, IYYAR, 28)),
383        ((2021, 5, 25), (5781, SIVAN, 14)),
384        ((2021, 6, 10), (5781, SIVAN, 30)),
385        ((2021, 6, 25), (5781, TAMMUZ, 15)),
386        ((2021, 7, 10), (5781, AV, 1)),
387        ((2021, 7, 25), (5781, AV, 16)),
388        ((2021, 8, 10), (5781, ELUL, 2)),
389        ((2021, 8, 25), (5781, ELUL, 17)),
390        ((2021, 9, 10), (5782, TISHREI, 4)),
391        ((2021, 9, 25), (5782, TISHREI, 19)),
392        ((2021, 10, 10), (5782, ḤESHVAN, 4)),
393        ((2021, 10, 25), (5782, ḤESHVAN, 19)),
394        ((2021, 11, 10), (5782, KISLEV, 6)),
395        ((2021, 11, 25), (5782, KISLEV, 21)),
396        ((2021, 12, 10), (5782, TEVET, 6)),
397        ((2021, 12, 25), (5782, TEVET, 21)),
398        ((2022, 1, 10), (5782, SHEVAT, 8)),
399        ((2022, 1, 25), (5782, SHEVAT, 23)),
400        ((2022, 2, 10), (5782, ADARI, 9)),
401        ((2022, 2, 25), (5782, ADARI, 24)),
402        ((2022, 3, 10), (5782, ADAR, 7)),
403        ((2022, 3, 25), (5782, ADAR, 22)),
404        ((2022, 4, 10), (5782, NISAN, 9)),
405        ((2022, 4, 25), (5782, NISAN, 24)),
406        ((2022, 5, 10), (5782, IYYAR, 9)),
407        ((2022, 5, 25), (5782, IYYAR, 24)),
408        ((2022, 6, 10), (5782, SIVAN, 11)),
409        ((2022, 6, 25), (5782, SIVAN, 26)),
410        ((2022, 7, 10), (5782, TAMMUZ, 11)),
411        ((2022, 7, 25), (5782, TAMMUZ, 26)),
412        ((2022, 8, 10), (5782, AV, 13)),
413        ((2022, 8, 25), (5782, AV, 28)),
414        ((2022, 9, 10), (5782, ELUL, 14)),
415        ((2022, 9, 25), (5782, ELUL, 29)),
416        ((2022, 10, 10), (5783, TISHREI, 15)),
417        ((2022, 10, 25), (5783, TISHREI, 30)),
418        ((2022, 11, 10), (5783, ḤESHVAN, 16)),
419        ((2022, 11, 25), (5783, KISLEV, 1)),
420        ((2022, 12, 10), (5783, KISLEV, 16)),
421        ((2022, 12, 25), (5783, TEVET, 1)),
422    ];
423
424    #[test]
425    fn test_conversions() {
426        for ((iso_y, iso_m, iso_d), (y, m, d)) in ISO_HEBREW_DATE_PAIRS.into_iter() {
427            let iso_date = Date::try_new_iso(iso_y, iso_m, iso_d).unwrap();
428            let hebrew_date = Date::try_new_from_codes(Some("am"), y, m.to_month_code(), d, Hebrew)
429                .expect("Date should parse");
430
431            let iso_to_hebrew = iso_date.to_calendar(Hebrew);
432
433            let hebrew_to_iso = hebrew_date.to_iso();
434
435            assert_eq!(
436                hebrew_to_iso, iso_date,
437                "Failed comparing to-ISO value for {hebrew_date:?} => {iso_date:?}"
438            );
439            assert_eq!(
440                iso_to_hebrew, hebrew_date,
441                "Failed comparing to-hebrew value for {iso_date:?} => {hebrew_date:?}"
442            );
443
444            let ordinal_month = if (m == ADARI || m.number() >= ADAR.number())
445                && LEAP_YEARS_IN_TESTS.contains(&y)
446            {
447                m.number() + 1
448            } else {
449                assert!(m != ADARI);
450                m.number()
451            };
452
453            #[allow(deprecated)] // should still test
454            let ordinal_hebrew_date = Date::try_new_hebrew(y, ordinal_month, d)
455                .expect("Construction of date must succeed");
456
457            assert_eq!(ordinal_hebrew_date, hebrew_date, "Hebrew date construction from codes and ordinals should work the same for {hebrew_date:?}");
458        }
459    }
460
461    #[test]
462    fn test_icu_bug_22441() {
463        let yi = YearInfo::compute_for(88369);
464        assert_eq!(yi.keviyah.year_length(), 383);
465    }
466
467    #[test]
468    fn test_negative_era_years() {
469        let greg_date = Date::try_new_gregorian(-5000, 1, 1).unwrap();
470        let greg_year = greg_date.era_year();
471        assert_eq!(greg_date.inner.0.year, -5000);
472        assert_eq!(greg_year.era, "bce");
473        // In Gregorian, era year is 1 - extended year
474        assert_eq!(greg_year.year, 5001);
475        let hebr_date = greg_date.to_calendar(Hebrew);
476        let hebr_year = hebr_date.era_year();
477        assert_eq!(hebr_date.inner.0.year.value, -1240);
478        assert_eq!(hebr_year.era, "am");
479        // In Hebrew, there is no inverse era, so negative extended years are negative era years
480        assert_eq!(hebr_year.year, -1240);
481    }
482
483    #[test]
484    fn test_weekdays() {
485        // https://github.com/unicode-org/icu4x/issues/4893
486        let cal = Hebrew::new();
487        let era = "am";
488        let month_code = MonthCode::new_normal(1).unwrap();
489        let dt = Date::try_new_from_codes(Some(era), 3760, month_code, 1, cal).unwrap();
490
491        // Should be Saturday per:
492        // https://www.hebcal.com/converter?hd=1&hm=Tishrei&hy=3760&h2g=1
493        assert_eq!(6, dt.day_of_week() as usize);
494    }
495}