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
5//! This module contains types and implementations for the Hebrew calendar.
6//!
7//! ```rust
8//! use icu::calendar::Date;
9//!
10//! let hebrew_date = Date::try_new_hebrew(3425, 10, 11)
11//!     .expect("Failed to initialize hebrew Date instance.");
12//!
13//! assert_eq!(hebrew_date.era_year().year, 3425);
14//! assert_eq!(hebrew_date.month().ordinal, 10);
15//! assert_eq!(hebrew_date.day_of_month().0, 11);
16//! ```
17
18use crate::cal::iso::{Iso, IsoDateInner};
19use crate::calendar_arithmetic::PrecomputedDataSource;
20use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
21use crate::error::DateError;
22use crate::types::MonthInfo;
23use crate::RangeError;
24use crate::{types, Calendar, Date, DateDuration, DateDurationUnit};
25use ::tinystr::tinystr;
26use calendrical_calculations::hebrew_keviyah::{Keviyah, YearInfo};
27use calendrical_calculations::rata_die::RataDie;
28
29/// The [Hebrew Calendar](https://en.wikipedia.org/wiki/Hebrew_calendar)
30///
31/// The Hebrew calendar is a lunisolar calendar used as the Jewish liturgical calendar
32/// as well as an official calendar in Israel.
33///
34/// This calendar is the _civil_ Hebrew calendar, with the year starting at in the month of Tishrei.
35///
36/// # Era codes
37///
38/// This calendar uses a single era code `am`, Anno Mundi. Dates before this era use negative years.
39///
40/// # Month codes
41///
42/// This calendar is a lunisolar calendar and thus has a leap month. It supports codes `"M01"-"M12"`
43/// for regular months, and the leap month Adar I being coded as `"M05L"`.
44///
45/// [`MonthInfo`] has slightly divergent behavior: because the regular month Adar is formatted
46/// as "Adar II" in a leap year, this calendar will produce the special code `"M06L"` in any [`MonthInfo`]
47/// objects it creates.
48#[derive(Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord, Default)]
49#[allow(clippy::exhaustive_structs)] // unit struct
50pub struct Hebrew;
51
52/// The inner date type used for representing [`Date`]s of [`Hebrew`]. See [`Date`] and [`Hebrew`] for more details.
53#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
54pub struct HebrewDateInner(ArithmeticDate<Hebrew>);
55
56impl Hebrew {
57    /// Construct a new [`Hebrew`]
58    pub fn new() -> Self {
59        Hebrew
60    }
61}
62
63#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
64pub(crate) struct HebrewYearInfo {
65    keviyah: Keviyah,
66    prev_keviyah: Keviyah,
67    value: i32,
68}
69
70impl From<HebrewYearInfo> for i32 {
71    fn from(value: HebrewYearInfo) -> Self {
72        value.value
73    }
74}
75
76impl HebrewYearInfo {
77    /// Convenience method to compute for a given year. Don't use this if you actually need
78    /// a YearInfo that you want to call .new_year() on.
79    ///
80    /// This can potentially be optimized with adjacent-year knowledge, but it's complex
81    #[inline]
82    fn compute(h_year: i32) -> Self {
83        let keviyah = YearInfo::compute_for(h_year).keviyah;
84        Self::compute_with_keviyah(keviyah, h_year)
85    }
86    /// Compute for a given year when the keviyah is already known
87    #[inline]
88    fn compute_with_keviyah(keviyah: Keviyah, h_year: i32) -> Self {
89        let prev_keviyah = YearInfo::compute_for(h_year - 1).keviyah;
90        Self {
91            keviyah,
92            prev_keviyah,
93            value: h_year,
94        }
95    }
96}
97//  HEBREW CALENDAR
98
99impl CalendarArithmetic for Hebrew {
100    type YearInfo = HebrewYearInfo;
101
102    fn days_in_provided_month(info: HebrewYearInfo, ordinal_month: u8) -> u8 {
103        info.keviyah.month_len(ordinal_month)
104    }
105
106    fn months_in_provided_year(info: HebrewYearInfo) -> u8 {
107        if info.keviyah.is_leap() {
108            13
109        } else {
110            12
111        }
112    }
113
114    fn days_in_provided_year(info: HebrewYearInfo) -> u16 {
115        info.keviyah.year_length()
116    }
117
118    fn provided_year_is_leap(info: HebrewYearInfo) -> bool {
119        info.keviyah.is_leap()
120    }
121
122    fn last_month_day_in_provided_year(info: HebrewYearInfo) -> (u8, u8) {
123        info.keviyah.last_month_day_in_year()
124    }
125}
126
127impl PrecomputedDataSource<HebrewYearInfo> for () {
128    fn load_or_compute_info(&self, h_year: i32) -> HebrewYearInfo {
129        HebrewYearInfo::compute(h_year)
130    }
131}
132
133impl crate::cal::scaffold::UnstableSealed for Hebrew {}
134impl Calendar for Hebrew {
135    type DateInner = HebrewDateInner;
136    type Year = types::EraYear;
137
138    fn from_codes(
139        &self,
140        era: Option<&str>,
141        year: i32,
142        month_code: types::MonthCode,
143        day: u8,
144    ) -> Result<Self::DateInner, DateError> {
145        match era {
146            Some("am") | None => {}
147            _ => return Err(DateError::UnknownEra),
148        }
149
150        let year = HebrewYearInfo::compute(year);
151
152        let is_leap_year = year.keviyah.is_leap();
153
154        let month_code_str = month_code.0.as_str();
155
156        let month_ordinal = if is_leap_year {
157            match month_code_str {
158                "M01" => 1,
159                "M02" => 2,
160                "M03" => 3,
161                "M04" => 4,
162                "M05" => 5,
163                "M05L" => 6,
164                // M06L is the formatting era code used for Adar II
165                "M06" | "M06L" => 7,
166                "M07" => 8,
167                "M08" => 9,
168                "M09" => 10,
169                "M10" => 11,
170                "M11" => 12,
171                "M12" => 13,
172                _ => {
173                    return Err(DateError::UnknownMonthCode(month_code));
174                }
175            }
176        } else {
177            match month_code_str {
178                "M01" => 1,
179                "M02" => 2,
180                "M03" => 3,
181                "M04" => 4,
182                "M05" => 5,
183                "M06" => 6,
184                "M07" => 7,
185                "M08" => 8,
186                "M09" => 9,
187                "M10" => 10,
188                "M11" => 11,
189                "M12" => 12,
190                _ => {
191                    return Err(DateError::UnknownMonthCode(month_code));
192                }
193            }
194        };
195
196        Ok(HebrewDateInner(ArithmeticDate::new_from_ordinals(
197            year,
198            month_ordinal,
199            day,
200        )?))
201    }
202
203    fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
204        let (year, h_year) = YearInfo::year_containing_rd(rd);
205        // Obtaining a 1-indexed day-in-year value
206        let day = rd - year.new_year() + 1;
207        let day = u16::try_from(day).unwrap_or(u16::MAX);
208
209        let year = HebrewYearInfo::compute_with_keviyah(year.keviyah, h_year);
210        let (month, day) = year.keviyah.month_day_for(day);
211        HebrewDateInner(ArithmeticDate::new_unchecked(year, month, day))
212    }
213
214    fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
215        let year = date.0.year.keviyah.year_info(date.0.year.value);
216
217        let ny = year.new_year();
218        let days_preceding = year.keviyah.days_preceding(date.0.month);
219
220        // Need to subtract 1 since the new year is itself in this year
221        ny + i64::from(days_preceding) + i64::from(date.0.day) - 1
222    }
223
224    fn from_iso(&self, iso: IsoDateInner) -> Self::DateInner {
225        self.from_rata_die(Iso.to_rata_die(&iso))
226    }
227
228    fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
229        Iso.from_rata_die(self.to_rata_die(date))
230    }
231
232    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
233        date.0.months_in_year()
234    }
235
236    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
237        date.0.days_in_year()
238    }
239
240    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
241        date.0.days_in_month()
242    }
243
244    fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
245        date.0.offset_date(offset, &())
246    }
247
248    fn until(
249        &self,
250        date1: &Self::DateInner,
251        date2: &Self::DateInner,
252        _calendar2: &Self,
253        _largest_unit: DateDurationUnit,
254        _smallest_unit: DateDurationUnit,
255    ) -> DateDuration<Self> {
256        date1.0.until(date2.0, _largest_unit, _smallest_unit)
257    }
258
259    fn debug_name(&self) -> &'static str {
260        "Hebrew"
261    }
262
263    fn year_info(&self, date: &Self::DateInner) -> Self::Year {
264        types::EraYear {
265            era_index: Some(0),
266            era: tinystr!(16, "am"),
267            year: self.extended_year(date),
268            ambiguity: types::YearAmbiguity::CenturyRequired,
269        }
270    }
271
272    fn extended_year(&self, date: &Self::DateInner) -> i32 {
273        date.0.extended_year()
274    }
275
276    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
277        Self::provided_year_is_leap(date.0.year)
278    }
279
280    fn month(&self, date: &Self::DateInner) -> MonthInfo {
281        let mut ordinal = date.0.month;
282        let is_leap_year = Self::provided_year_is_leap(date.0.year);
283
284        if is_leap_year {
285            if ordinal == 6 {
286                return types::MonthInfo {
287                    ordinal,
288                    standard_code: types::MonthCode(tinystr!(4, "M05L")),
289                    formatting_code: types::MonthCode(tinystr!(4, "M05L")),
290                };
291            } else if ordinal == 7 {
292                return types::MonthInfo {
293                    ordinal,
294                    // Adar II is the same as Adar and has the same code
295                    standard_code: types::MonthCode(tinystr!(4, "M06")),
296                    formatting_code: types::MonthCode(tinystr!(4, "M06L")),
297                };
298            }
299        }
300
301        if is_leap_year && ordinal > 6 {
302            ordinal -= 1;
303        }
304
305        let code = match ordinal {
306            1 => tinystr!(4, "M01"),
307            2 => tinystr!(4, "M02"),
308            3 => tinystr!(4, "M03"),
309            4 => tinystr!(4, "M04"),
310            5 => tinystr!(4, "M05"),
311            6 => tinystr!(4, "M06"),
312            7 => tinystr!(4, "M07"),
313            8 => tinystr!(4, "M08"),
314            9 => tinystr!(4, "M09"),
315            10 => tinystr!(4, "M10"),
316            11 => tinystr!(4, "M11"),
317            12 => tinystr!(4, "M12"),
318            _ => tinystr!(4, "und"),
319        };
320
321        types::MonthInfo {
322            ordinal: date.0.month,
323            standard_code: types::MonthCode(code),
324            formatting_code: types::MonthCode(code),
325        }
326    }
327
328    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
329        date.0.day_of_month()
330    }
331
332    fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
333        date.0.day_of_year()
334    }
335
336    fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
337        Some(crate::preferences::CalendarAlgorithm::Hebrew)
338    }
339}
340
341impl Date<Hebrew> {
342    /// Construct new Hebrew Date.
343    ///
344    /// This date will not use any precomputed calendrical calculations,
345    /// one that loads such data from a provider will be added in the future (#3933)
346    ///
347    ///
348    /// ```rust
349    /// use icu::calendar::Date;
350    ///
351    /// let date_hebrew = Date::try_new_hebrew(3425, 4, 25)
352    ///     .expect("Failed to initialize Hebrew Date instance.");
353    ///
354    /// assert_eq!(date_hebrew.era_year().year, 3425);
355    /// assert_eq!(date_hebrew.month().ordinal, 4);
356    /// assert_eq!(date_hebrew.day_of_month().0, 25);
357    /// ```
358    pub fn try_new_hebrew(year: i32, month: u8, day: u8) -> Result<Date<Hebrew>, RangeError> {
359        let year = HebrewYearInfo::compute(year);
360
361        ArithmeticDate::new_from_ordinals(year, month, day)
362            .map(HebrewDateInner)
363            .map(|inner| Date::from_raw(inner, Hebrew))
364    }
365}
366
367#[cfg(test)]
368mod tests {
369
370    use super::*;
371    use crate::types::MonthCode;
372    use calendrical_calculations::hebrew_keviyah::*;
373
374    // Sentinel value for Adar I
375    // We're using normalized month values here so that we can use constants. These do not
376    // distinguish between the different Adars. We add an out-of-range sentinel value of 13 to
377    // specifically talk about Adar I in a leap year
378    const ADARI: u8 = 13;
379
380    /// The leap years used in the tests below
381    const LEAP_YEARS_IN_TESTS: [i32; 1] = [5782];
382    /// (iso, hebrew) pairs of testcases. If any of the years here
383    /// are leap years please add them to LEAP_YEARS_IN_TESTS (we have this manually
384    /// so we don't end up exercising potentially buggy codepaths to test this)
385    #[allow(clippy::type_complexity)]
386    const ISO_HEBREW_DATE_PAIRS: [((i32, u8, u8), (i32, u8, u8)); 48] = [
387        ((2021, 1, 10), (5781, TEVET, 26)),
388        ((2021, 1, 25), (5781, SHEVAT, 12)),
389        ((2021, 2, 10), (5781, SHEVAT, 28)),
390        ((2021, 2, 25), (5781, ADAR, 13)),
391        ((2021, 3, 10), (5781, ADAR, 26)),
392        ((2021, 3, 25), (5781, NISAN, 12)),
393        ((2021, 4, 10), (5781, NISAN, 28)),
394        ((2021, 4, 25), (5781, IYYAR, 13)),
395        ((2021, 5, 10), (5781, IYYAR, 28)),
396        ((2021, 5, 25), (5781, SIVAN, 14)),
397        ((2021, 6, 10), (5781, SIVAN, 30)),
398        ((2021, 6, 25), (5781, TAMMUZ, 15)),
399        ((2021, 7, 10), (5781, AV, 1)),
400        ((2021, 7, 25), (5781, AV, 16)),
401        ((2021, 8, 10), (5781, ELUL, 2)),
402        ((2021, 8, 25), (5781, ELUL, 17)),
403        ((2021, 9, 10), (5782, TISHREI, 4)),
404        ((2021, 9, 25), (5782, TISHREI, 19)),
405        ((2021, 10, 10), (5782, ḤESHVAN, 4)),
406        ((2021, 10, 25), (5782, ḤESHVAN, 19)),
407        ((2021, 11, 10), (5782, KISLEV, 6)),
408        ((2021, 11, 25), (5782, KISLEV, 21)),
409        ((2021, 12, 10), (5782, TEVET, 6)),
410        ((2021, 12, 25), (5782, TEVET, 21)),
411        ((2022, 1, 10), (5782, SHEVAT, 8)),
412        ((2022, 1, 25), (5782, SHEVAT, 23)),
413        ((2022, 2, 10), (5782, ADARI, 9)),
414        ((2022, 2, 25), (5782, ADARI, 24)),
415        ((2022, 3, 10), (5782, ADAR, 7)),
416        ((2022, 3, 25), (5782, ADAR, 22)),
417        ((2022, 4, 10), (5782, NISAN, 9)),
418        ((2022, 4, 25), (5782, NISAN, 24)),
419        ((2022, 5, 10), (5782, IYYAR, 9)),
420        ((2022, 5, 25), (5782, IYYAR, 24)),
421        ((2022, 6, 10), (5782, SIVAN, 11)),
422        ((2022, 6, 25), (5782, SIVAN, 26)),
423        ((2022, 7, 10), (5782, TAMMUZ, 11)),
424        ((2022, 7, 25), (5782, TAMMUZ, 26)),
425        ((2022, 8, 10), (5782, AV, 13)),
426        ((2022, 8, 25), (5782, AV, 28)),
427        ((2022, 9, 10), (5782, ELUL, 14)),
428        ((2022, 9, 25), (5782, ELUL, 29)),
429        ((2022, 10, 10), (5783, TISHREI, 15)),
430        ((2022, 10, 25), (5783, TISHREI, 30)),
431        ((2022, 11, 10), (5783, ḤESHVAN, 16)),
432        ((2022, 11, 25), (5783, KISLEV, 1)),
433        ((2022, 12, 10), (5783, KISLEV, 16)),
434        ((2022, 12, 25), (5783, TEVET, 1)),
435    ];
436
437    #[test]
438    fn test_conversions() {
439        for ((iso_y, iso_m, iso_d), (y, m, d)) in ISO_HEBREW_DATE_PAIRS.into_iter() {
440            let iso_date = Date::try_new_iso(iso_y, iso_m, iso_d).unwrap();
441            let month_code = if m == ADARI {
442                MonthCode(tinystr!(4, "M05L"))
443            } else {
444                MonthCode::new_normal(m).unwrap()
445            };
446            let hebrew_date = Date::try_new_from_codes(Some("am"), y, month_code, d, Hebrew)
447                .expect("Date should parse");
448
449            let iso_to_hebrew = iso_date.to_calendar(Hebrew);
450
451            let hebrew_to_iso = hebrew_date.to_calendar(Iso);
452
453            assert_eq!(
454                hebrew_to_iso, iso_date,
455                "Failed comparing to-ISO value for {hebrew_date:?} => {iso_date:?}"
456            );
457            assert_eq!(
458                iso_to_hebrew, hebrew_date,
459                "Failed comparing to-hebrew value for {iso_date:?} => {hebrew_date:?}"
460            );
461
462            let ordinal_month = if LEAP_YEARS_IN_TESTS.contains(&y) {
463                if m == ADARI {
464                    ADAR
465                } else if m >= ADAR {
466                    m + 1
467                } else {
468                    m
469                }
470            } else {
471                assert!(m != ADARI);
472                m
473            };
474
475            let ordinal_hebrew_date = Date::try_new_hebrew(y, ordinal_month, d)
476                .expect("Construction of date must succeed");
477
478            assert_eq!(ordinal_hebrew_date, hebrew_date, "Hebrew date construction from codes and ordinals should work the same for {hebrew_date:?}");
479        }
480    }
481
482    #[test]
483    fn test_icu_bug_22441() {
484        let yi = YearInfo::compute_for(88369);
485        assert_eq!(yi.keviyah.year_length(), 383);
486    }
487
488    #[test]
489    fn test_negative_era_years() {
490        let greg_date = Date::try_new_gregorian(-5000, 1, 1).unwrap();
491        let greg_year = greg_date.era_year();
492        assert_eq!(greg_date.inner.0 .0.year, -5000);
493        assert_eq!(greg_year.era, "bce");
494        // In Gregorian, era year is 1 - extended year
495        assert_eq!(greg_year.year, 5001);
496        let hebr_date = greg_date.to_calendar(Hebrew);
497        let hebr_year = hebr_date.era_year();
498        assert_eq!(hebr_date.inner.0.year.value, -1240);
499        assert_eq!(hebr_year.era, "am");
500        // In Hebrew, there is no inverse era, so negative extended years are negative era years
501        assert_eq!(hebr_year.year, -1240);
502    }
503
504    #[test]
505    fn test_weekdays() {
506        // https://github.com/unicode-org/icu4x/issues/4893
507        let cal = Hebrew::new();
508        let era = "am";
509        let month_code = MonthCode(tinystr!(4, "M01"));
510        let dt = Date::try_new_from_codes(Some(era), 3760, month_code, 1, cal).unwrap();
511
512        // Should be Saturday per:
513        // https://www.hebcal.com/converter?hd=1&hm=Tishrei&hy=3760&h2g=1
514        assert_eq!(6, dt.day_of_week() as usize);
515    }
516}