1use 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#[derive(Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord, Default)]
49#[allow(clippy::exhaustive_structs)] pub struct Hebrew;
51
52#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
54pub struct HebrewDateInner(ArithmeticDate<Hebrew>);
55
56impl Hebrew {
57    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    #[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    #[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}
97impl 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                "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        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        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                    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    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    const ADARI: u8 = 13;
379
380    const LEAP_YEARS_IN_TESTS: [i32; 1] = [5782];
382    #[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        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        assert_eq!(hebr_year.year, -1240);
502    }
503
504    #[test]
505    fn test_weekdays() {
506        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        assert_eq!(6, dt.day_of_week() as usize);
515    }
516}