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