Skip to main content

icu_calendar/cal/
hijri.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;
6use crate::calendar_arithmetic::DateFieldsResolver;
7use crate::calendar_arithmetic::PackWithMD;
8use crate::error::{
9    DateAddError, DateFromFieldsError, DateNewError, EcmaReferenceYearError, UnknownEraError,
10};
11use crate::options::DateFromFieldsOptions;
12use crate::options::{DateAddOptions, DateDifferenceOptions};
13use crate::types::DateFields;
14use crate::types::Month;
15use crate::{AsCalendar, RangeError};
16use crate::{Calendar, Date, types};
17use calendrical_calculations::rata_die::RataDie;
18use core::cmp::Ordering;
19use core::fmt::Debug;
20use icu_locale_core::preferences::extensions::unicode::keywords::{
21    CalendarAlgorithm, HijriCalendarAlgorithm,
22};
23use icu_provider::prelude::*;
24use tinystr::tinystr;
25
26#[path = "hijri/ummalqura_data.rs"]
27mod ummalqura_data;
28
29/// The [Hijri Calendar](https://en.wikipedia.org/wiki/Islamic_calendar)
30///
31/// There are many variants of this calendar, using different lunar observations or calculations
32/// (see [`Rules`]). Currently, [`Rules`] is an unstable trait, but some of its implementors
33/// are stable, and can be constructed via the various `Hijri::new_*` constructors. Please comment
34/// on [this issue](https://github.com/unicode-org/icu4x/issues/6962)
35/// if you would like to see this the ability to implement custom [`Rules`] stabilized.
36///
37/// This implementation supports only variants where months are either 29 or 30 days.
38///
39/// This corresponds to various `"islamic-*"` [CLDR calendars](https://unicode.org/reports/tr35/#UnicodeCalendarIdentifier),
40/// see the individual implementors of [`Rules`] ([`TabularAlgorithm`], [`UmmAlQura`], [`AstronomicalSimulation`]) for more information.
41///
42/// # Era codes
43///
44/// This calendar uses two era codes: `ah`, and `bh`, corresponding to the Anno Hegirae and Before Hijrah eras
45///
46/// # Months and days
47///
48/// The 12 months are called al-Muḥarram (`M01`), Ṣafar (`M02`), Rabīʿ al-ʾAwwal (`M03`),
49/// Rabīʿ ath-Thānī or Rabīʿ al-ʾĀkhir (`M04`), Jumādā al-ʾŪlā (`M05`), Jumādā ath-Thāniyah
50/// or Jumādā al-ʾĀkhirah (`M06`), Rajab (`M07`), Shaʿbān (`M08`), Ramaḍān (`M09`), Shawwāl (`M10`),
51/// Ḏū al-Qaʿdah (`M11`), Ḏū al-Ḥijjah (`M12`).
52///
53/// As a true lunar calendar, the lengths of the months depend on the lunar cycle (a month starts on the day
54/// where the waxing crescent is first observed), and will be either 29 or 30 days.
55///
56/// The lengths of the months are determined by the concrete [`Rules`] implementation.
57///
58/// There are either 6 or 7 30-day months, so the length of the year is 354 or 355 days.
59///
60/// # Crescent moon visibility
61///
62/// According to Islam, a new month begins the evening when the crescent moon is visible at sunset.
63///
64/// Currently, most groups that use the Hijri calendar rely on human observations of the
65/// crescent moon, which are impacted by atmospheric phenomena. As a result, it is not
66/// possible to predict the month start dates ahead of time.
67///
68/// However, some groups use criteria that are not impacted by these phenomena and are
69/// therefore suitable for future prediction. This crate ships two:
70///
71/// 1. [`UmmAlQura`], used in Saudi Arabia, is based on official predictions of crescent
72///    timings published by the KACST.
73/// 2. [`TabularAlgorithm`] is based on a proleptic approximation of the length of a lunar year.
74///    See the docs for information on the branches of Islam using it.
75///
76/// To support other Hijri variants, use the [`Rules`] trait.
77///
78/// # Calendar drift
79///
80/// As a lunar calendar, this calendar does not intend to follow the solar year, and drifts more
81/// than 10 days per year with respect to the seasons.
82#[derive(Clone, Debug, Default, Copy)]
83#[allow(clippy::exhaustive_structs)] // newtype
84pub struct Hijri<S>(pub S);
85
86/// Defines a variant of the [`Hijri`] calendar.
87///
88/// This crate includes the [`UmmAlQura`] and [`TabularAlgorithm`] rules.
89///
90/// To support other Hijri variants, provide your own rules by implementing this trait.
91/// You may find the simulations in the [`calendrical_calculations`] crate to be useful,
92/// supplemented with data from human observations.
93///
94///
95/// <div class="stab unstable">
96/// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
97/// including in SemVer minor releases. Do not use this type unless you are prepared for things to occasionally break.
98///
99/// Graduation tracking issue: [issue #6962](https://github.com/unicode-org/icu4x/issues/6962).
100/// </div>
101///
102/// <div class="stab unstable">
103/// 🚫 This trait is sealed; it should not be implemented by user code. If an API requests an item that implements this
104/// trait, please consider using a type from the implementors listed below.
105///
106/// It is still possible to implement this trait in userland (since `UnstableSealed` is public),
107/// do not do so unless you are prepared for things to occasionally break.
108/// </div>
109pub trait Rules: Clone + Debug + crate::cal::scaffold::UnstableSealed {
110    /// Returns data about the given year.
111    fn year(&self, extended_year: i32) -> HijriYear;
112
113    /// Returns data for the year containing the given [`RataDie`].
114    fn year_containing_rd(&self, rd: RataDie) -> HijriYear {
115        let extended_year = calendrical_calculations::islamic::tabular_year_from_fixed(
116            // give an allowance of 5 days before the tabular new year
117            rd - 5,
118            calendrical_calculations::islamic::ISLAMIC_EPOCH_FRIDAY,
119        );
120
121        let mut year = self.year(extended_year);
122
123        // we might need to use the next year
124        if rd >= year.new_year() + year.packed.days_in_year() as i64 && extended_year < i32::MAX {
125            year = self.year(year.extended_year + 1)
126        }
127
128        year
129    }
130
131    /// Returns an ECMA reference year (represented as an extended year)
132    /// that contains the given month-day combination.
133    ///
134    /// If the day is out of range, it will return a year that contains the given month
135    /// and the maximum day possible for that month. See [the spec][spec] for the
136    /// precise algorithm used.
137    ///
138    /// This API only matters when using [`MissingFieldsStrategy::Ecma`] to compute
139    /// a date without providing a year in [`Date::try_from_fields()`]. The default impl
140    /// will just error, and custom calendars who do not care about ECMA/Temporal
141    /// reference years do not need to override this.
142    ///
143    /// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-nonisomonthdaytoisoreferencedate
144    /// [`MissingFieldsStrategy::Ecma`]: crate::options::MissingFieldsStrategy::Ecma
145    fn ecma_reference_year(&self, _month: Month, _day: u8) -> Result<i32, EcmaReferenceYearError> {
146        Err(EcmaReferenceYearError::Unimplemented)
147    }
148
149    /// The error that is returned by [`Self::check_date_compatibility`].
150    ///
151    /// Set this to [`core::convert::Infallible`] if the type is a singleton or
152    /// the parameterization does not affect calendar semantics.
153    type DateCompatibilityError: Debug;
154
155    /// Checks whether two [`Rules`] values are equal for the purpose of [`Date`] interaction.
156    fn check_date_compatibility(&self, other: &Self) -> Result<(), Self::DateCompatibilityError>;
157
158    /// The BCP-47 [`CalendarAlgorithm`] for the Hijri calendar using these rules, if defined.
159    fn calendar_algorithm(&self) -> Option<CalendarAlgorithm> {
160        None
161    }
162
163    /// The debug name for these rules.
164    fn debug_name(&self) -> &'static str {
165        "Hijri (custom rules)"
166    }
167}
168
169/// [`Hijri`] [`Rules`] based on astronomical simulations.
170///
171/// Currently, this uses simulation results published by the KACST,
172/// making it identical to [`UmmAlQura`].
173///
174/// In previous versions, this type used arithmetic published by E. M. Reingold, S. K. Shaukat,
175/// et al.[^1]. Since it now uses the KACST simulations, it is deprecated in favor of [`UmmAlQura`].
176/// If you wish to use the previous behavior, use the [`calendrical_calculations`] crate directly;
177/// for an example, see [`tests/reingold.rs`](https://github.com/unicode-org/icu4x/blob/main/components/calendar/tests/reingold.rs).
178///
179/// The precise behavior of this calendar may change in the future if:
180/// - We decide to tweak the precise astronomical simulation used
181/// - We decide to expand or reduce the range where we are using the astronomical simulation.
182///
183/// [^1]: See [`calendrical_calculations::islamic::observational_islamic_from_fixed`]
184#[derive(Copy, Clone, Debug, PartialEq, Eq)]
185#[non_exhaustive]
186#[deprecated(since = "2.2.0", note = "use `UmmAlQura`")]
187pub struct AstronomicalSimulation;
188
189#[allow(deprecated)]
190impl crate::cal::scaffold::UnstableSealed for AstronomicalSimulation {}
191#[allow(deprecated)]
192impl Rules for AstronomicalSimulation {
193    fn debug_name(&self) -> &'static str {
194        "Hijri (simulated, Mecca)"
195    }
196
197    fn calendar_algorithm(&self) -> Option<CalendarAlgorithm> {
198        None
199    }
200
201    fn year(&self, extended_year: i32) -> HijriYear {
202        UmmAlQura.year(extended_year)
203    }
204
205    fn ecma_reference_year(&self, month: Month, day: u8) -> Result<i32, EcmaReferenceYearError> {
206        UmmAlQura.ecma_reference_year(month, day)
207    }
208
209    fn year_containing_rd(&self, rd: RataDie) -> HijriYear {
210        UmmAlQura.year_containing_rd(rd)
211    }
212
213    type DateCompatibilityError = core::convert::Infallible;
214
215    fn check_date_compatibility(&self, &Self: &Self) -> Result<(), Self::DateCompatibilityError> {
216        Ok(())
217    }
218}
219
220/// [`Hijri`] [`Rules`] for the [Umm al-Qura](https://en.wikipedia.org/wiki/Islamic_calendar#Saudi_Arabia's_Umm_al-Qura_calendar) calendar.
221///
222/// From the start of 1300 AH (1882-11-12 ISO) to the end of 1600 AH (2174-11-25 ISO), this
223/// `Rules` implementation uses Umm al-Qura month lengths obtained from
224/// [KACST](https://kacst.gov.sa/). Outside this range, this implementation falls back to
225/// [`TabularAlgorithm`] with [`TabularAlgorithmLeapYears::TypeII`] and [`TabularAlgorithmEpoch::Friday`].
226///
227/// The precise behavior of this calendar may change in the future if:
228/// - New ground truth is established by published government sources
229/// - We decide to use a different algorithm outside the KACST range
230/// - We decide to expand or reduce the range where we are correctly handling past dates.
231///
232/// This corresponds to the `"islamic-umalqura"` [CLDR calendar](https://unicode.org/reports/tr35/#UnicodeCalendarIdentifier).
233#[derive(Copy, Clone, Debug, Default)]
234#[non_exhaustive]
235pub struct UmmAlQura;
236
237impl crate::cal::scaffold::UnstableSealed for UmmAlQura {}
238impl Rules for UmmAlQura {
239    fn calendar_algorithm(&self) -> Option<CalendarAlgorithm> {
240        Some(CalendarAlgorithm::Hijri(Some(
241            HijriCalendarAlgorithm::Umalqura,
242        )))
243    }
244
245    fn ecma_reference_year(&self, month: Month, day: u8) -> Result<i32, EcmaReferenceYearError> {
246        if month.is_leap() {
247            return Err(EcmaReferenceYearError::MonthNotInCalendar);
248        }
249
250        let extended_year = match (month.number(), day) {
251            (1, _) => 1392,
252            (2, 30..) => 1390,
253            (2, _) => 1392,
254            (3, 30..) => 1391,
255            (3, _) => 1392,
256            (4, _) => 1392,
257            (5, 30..) => 1391,
258            (5, _) => 1392,
259            (6, _) => 1392,
260            (7, 30..) => 1389,
261            (7, _) => 1392,
262            (8, _) => 1392,
263            (9, _) => 1392,
264            (10, 30..) => 1390,
265            (10, _) => 1392,
266            (11, ..26) => 1392,
267            (11, _) => 1391,
268            (12, 30..) => 1390,
269            (12, _) => 1391,
270            _ => return Err(EcmaReferenceYearError::MonthNotInCalendar),
271        };
272        Ok(extended_year)
273    }
274
275    fn debug_name(&self) -> &'static str {
276        "Hijri (Umm al-Qura)"
277    }
278
279    fn year(&self, extended_year: i32) -> HijriYear {
280        if let Some(data) = HijriYear::lookup(
281            extended_year,
282            ummalqura_data::STARTING_YEAR,
283            ummalqura_data::DATA,
284        ) {
285            data
286        } else {
287            TabularAlgorithm {
288                leap_years: TabularAlgorithmLeapYears::TypeII,
289                epoch: TabularAlgorithmEpoch::Friday,
290            }
291            .year(extended_year)
292        }
293    }
294
295    type DateCompatibilityError = core::convert::Infallible;
296
297    fn check_date_compatibility(&self, &Self: &Self) -> Result<(), Self::DateCompatibilityError> {
298        Ok(())
299    }
300}
301
302/// [`Hijri`] [`Rules`] for the [Tabular Hijri Algorithm](https://en.wikipedia.org/wiki/Tabular_Islamic_calendar).
303///
304/// See [`TabularAlgorithmEpoch`] and [`TabularAlgorithmLeapYears`] for customization.
305///
306/// The most common version of these rules uses [`TabularAlgorithmEpoch::Friday`] and [`TabularAlgorithmLeapYears::TypeII`].
307///
308/// Tabular Islamic rules are used in denominations such as Dawoodi Bohra and other branches of Ismailism.
309/// Be sure to select the correct leap year and epoch parameters according to your use case. If this type does
310/// not implement the parameters you need, please file an issue.
311///
312/// When constructed with [`TabularAlgorithmLeapYears::TypeII`], and either [`TabularAlgorithmEpoch::Friday`] or [`TabularAlgorithmEpoch::Thursday`],
313/// this corresponds to the `"islamic-civil"` and `"islamic-tbla"` [CLDR calendars](https://unicode.org/reports/tr35/#UnicodeCalendarIdentifier) respectively.
314#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
315pub struct TabularAlgorithm {
316    pub(crate) leap_years: TabularAlgorithmLeapYears,
317    pub(crate) epoch: TabularAlgorithmEpoch,
318}
319
320impl TabularAlgorithm {
321    /// Construct a new [`TabularAlgorithm`] with the given leap year rule and epoch.
322    pub const fn new(leap_years: TabularAlgorithmLeapYears, epoch: TabularAlgorithmEpoch) -> Self {
323        Self { epoch, leap_years }
324    }
325}
326
327impl crate::cal::scaffold::UnstableSealed for TabularAlgorithm {}
328impl Rules for TabularAlgorithm {
329    fn calendar_algorithm(&self) -> Option<CalendarAlgorithm> {
330        Some(match (self.epoch, self.leap_years) {
331            (TabularAlgorithmEpoch::Friday, TabularAlgorithmLeapYears::TypeII) => {
332                CalendarAlgorithm::Hijri(Some(HijriCalendarAlgorithm::Civil))
333            }
334            (TabularAlgorithmEpoch::Thursday, TabularAlgorithmLeapYears::TypeII) => {
335                CalendarAlgorithm::Hijri(Some(HijriCalendarAlgorithm::Tbla))
336            }
337        })
338    }
339
340    fn ecma_reference_year(&self, month: Month, day: u8) -> Result<i32, EcmaReferenceYearError> {
341        if month.is_leap() {
342            return Err(EcmaReferenceYearError::MonthNotInCalendar);
343        }
344
345        Ok(match (month.number(), day) {
346            (1, _) => 1392,
347            (2, _) => 1392,
348            (3, _) => 1392,
349            (4, _) => 1392,
350            (5, _) => 1392,
351            (6, _) => 1392,
352            (7, _) => 1392,
353            (8, _) => 1392,
354            (9, _) => 1392,
355            (10, _) => 1392,
356            (11, ..26) if self.epoch == TabularAlgorithmEpoch::Friday => 1392,
357            (11, ..27) if self.epoch == TabularAlgorithmEpoch::Thursday => 1392,
358            (11, _) => 1391,
359            (12, 30..) => 1390,
360            (12, _) => 1391,
361            _ => return Err(EcmaReferenceYearError::MonthNotInCalendar),
362        })
363    }
364
365    fn debug_name(&self) -> &'static str {
366        match self.epoch {
367            TabularAlgorithmEpoch::Friday => "Hijri (civil)",
368            TabularAlgorithmEpoch::Thursday => "Hijri (astronomical)",
369        }
370    }
371
372    fn year(&self, extended_year: i32) -> HijriYear {
373        let start_day = calendrical_calculations::islamic::fixed_from_tabular_islamic(
374            extended_year,
375            1,
376            1,
377            self.epoch.rata_die(),
378        );
379        let month_lengths = core::array::from_fn(|m| {
380            m % 2 == 0
381                || m == 11
382                    && match self.leap_years {
383                        TabularAlgorithmLeapYears::TypeII => {
384                            (14 + 11 * extended_year as i64).rem_euclid(30) < 11
385                        }
386                    }
387        });
388        HijriYear {
389            // start_day is within 5 days of the tabular start day (trivial), and month lengths
390            // has either 6 or 7 long months.
391            packed: PackedHijriYearData::new_unchecked(extended_year, month_lengths, start_day),
392            extended_year,
393        }
394    }
395
396    type DateCompatibilityError = crate::error::MismatchedCalendarError;
397
398    fn check_date_compatibility(&self, other: &Self) -> Result<(), Self::DateCompatibilityError> {
399        if self != other {
400            return Err(crate::error::MismatchedCalendarError);
401        }
402        Ok(())
403    }
404}
405
406#[allow(deprecated)]
407impl Hijri<AstronomicalSimulation> {
408    /// Use [`Self::new_simulated_mecca`].
409    #[cfg(feature = "compiled_data")]
410    #[deprecated(since = "2.1.0", note = "use `Hijri::new_umm_al_qura`")]
411    pub const fn new_mecca() -> Self {
412        Self::new_simulated_mecca()
413    }
414
415    /// Creates a [`Hijri`] calendar using simulated sightings at Mecca.
416    ///
417    /// These simulations are unofficial and are known to not necessarily match sightings
418    /// on the ground. Unless you know otherwise for sure, instead of this variant, use
419    /// [`Hijri::new_umm_al_qura`], which uses the results of KACST's Mecca-based calculations.
420    #[deprecated(since = "2.2.0", note = "use `Hijri::new_umm_al_qura`")]
421    pub const fn new_simulated_mecca() -> Self {
422        Self(AstronomicalSimulation)
423    }
424
425    #[cfg(feature = "serde")]
426    #[doc = icu_provider::gen_buffer_unstable_docs!(BUFFER,Self::new)]
427    #[deprecated(since = "2.1.0", note = "use `Hijri::new_umm_al_qura`")]
428    pub fn try_new_mecca_with_buffer_provider(
429        _provider: &(impl BufferProvider + ?Sized),
430    ) -> Result<Self, DataError> {
431        Ok(Self::new_simulated_mecca())
432    }
433
434    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new_mecca)]
435    #[deprecated(since = "2.1.0", note = "use `Hijri::new_umm_al_qura`")]
436    pub fn try_new_mecca_unstable<D: ?Sized>(_provider: &D) -> Result<Self, DataError> {
437        Ok(Self::new_simulated_mecca())
438    }
439
440    /// Use [`Self::new_simulated_mecca`].
441    #[deprecated(since = "2.1.0", note = "use `Hijri::new_umm_al_qura`")]
442    pub const fn new_mecca_always_calculating() -> Self {
443        Self::new_simulated_mecca()
444    }
445}
446
447impl Hijri<UmmAlQura> {
448    /// Use [`Self::new_umm_al_qura`]
449    #[deprecated(since = "2.1.0", note = "use `Self::new_umm_al_qura`")]
450    pub const fn new() -> Self {
451        Self(UmmAlQura)
452    }
453
454    /// Creates a [`Hijri`] calendar using [`UmmAlQura`] rules.
455    pub const fn new_umm_al_qura() -> Self {
456        Self(UmmAlQura)
457    }
458}
459
460/// The epoch for the [`TabularAlgorithm`] rules.
461#[non_exhaustive]
462#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
463pub enum TabularAlgorithmEpoch {
464    /// Thusday July 15, 622 AD Julian (0622-07-18 ISO)
465    Thursday,
466    /// Friday July 16, 622 AD Julian (0622-07-19 ISO)
467    Friday,
468}
469
470impl TabularAlgorithmEpoch {
471    fn rata_die(self) -> RataDie {
472        match self {
473            Self::Thursday => calendrical_calculations::islamic::ISLAMIC_EPOCH_THURSDAY,
474            Self::Friday => calendrical_calculations::islamic::ISLAMIC_EPOCH_FRIDAY,
475        }
476    }
477}
478
479/// The leap year rule for the [`TabularAlgorithm`] rules.
480///
481/// This specifies which years of a 30-year cycle have an additional day at
482/// the end of the year.
483#[non_exhaustive]
484#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
485pub enum TabularAlgorithmLeapYears {
486    /// Leap years 2, 5, 7, 10, 13, 16, 18, 21, 24, 26, 29
487    TypeII,
488}
489
490impl Hijri<TabularAlgorithm> {
491    /// Use [`Self::new_tabular`]
492    #[deprecated(since = "2.1.0", note = "use `Hijri::new_tabular`")]
493    pub const fn new(leap_years: TabularAlgorithmLeapYears, epoch: TabularAlgorithmEpoch) -> Self {
494        Hijri::new_tabular(leap_years, epoch)
495    }
496
497    /// Creates a [`Hijri`] calendar with tabular rules and the given leap year rule and epoch.
498    pub const fn new_tabular(
499        leap_years: TabularAlgorithmLeapYears,
500        epoch: TabularAlgorithmEpoch,
501    ) -> Self {
502        Self(TabularAlgorithm::new(leap_years, epoch))
503    }
504}
505
506/// Information about a Hijri year.
507///
508/// <div class="stab unstable">
509/// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
510/// including in SemVer minor releases. Do not use this type unless you are prepared for things to occasionally break.
511///
512/// Graduation tracking issue: [issue #6962](https://github.com/unicode-org/icu4x/issues/6962).
513/// </div>
514#[derive(Copy, Clone, Debug)]
515pub struct HijriYear {
516    packed: PackedHijriYearData,
517    extended_year: i32,
518}
519
520impl PartialEq for HijriYear {
521    fn eq(&self, other: &Self) -> bool {
522        self.extended_year == other.extended_year
523    }
524}
525impl Eq for HijriYear {}
526impl core::hash::Hash for HijriYear {
527    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
528        self.extended_year.hash(state);
529    }
530}
531impl PartialOrd for HijriYear {
532    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
533        Some(self.cmp(other))
534    }
535}
536impl Ord for HijriYear {
537    fn cmp(&self, other: &Self) -> Ordering {
538        self.extended_year.cmp(&other.extended_year)
539    }
540}
541
542impl core::ops::Sub<HijriYear> for HijriYear {
543    type Output = i32;
544    #[inline]
545    fn sub(self, rhs: HijriYear) -> Self::Output {
546        self.extended_year - rhs.extended_year
547    }
548}
549
550impl HijriYear {
551    /// Creates [`HijriYear`] from the given parts.
552    ///
553    /// `month_starts` contains the first day of the 13 months from al-Muḥarram of the
554    /// given year to al-Muḥarram of the following year (inclusive). See [`Date::to_rata_die`]
555    /// to obtain a [`RataDie`] from a [`Date`] in an arbitrary calendar.
556    ///
557    /// The start of al-Muḥarram has to be within 5 days of the start of al-Muḥarram using
558    /// the [`TabularAlgorithm`].
559    ///
560    /// Months need to have either 29 or 30 days.
561    pub const fn try_new(extended_year: i32, month_starts: [RataDie; 13]) -> Option<Self> {
562        let mut month_lengths = [false; 12];
563
564        let mut i = 0;
565        #[allow(clippy::indexing_slicing)]
566        while i < 12 {
567            match month_starts[i + 1].to_i64_date() - month_starts[i].to_i64_date() {
568                29 => month_lengths[i] = false,
569                30 => month_lengths[i] = true,
570                _ => return None,
571            }
572            i += 1;
573        }
574
575        let Some(packed) =
576            PackedHijriYearData::try_new(extended_year, month_lengths, month_starts[0])
577        else {
578            return None;
579        };
580
581        Some(Self {
582            packed,
583            extended_year,
584        })
585    }
586
587    fn lookup(
588        extended_year: i32,
589        starting_year: i32,
590        data: &[PackedHijriYearData],
591    ) -> Option<Self> {
592        Some(extended_year)
593            .and_then(|e| usize::try_from(e.checked_sub(starting_year)?).ok())
594            .and_then(|i| data.get(i))
595            .map(|&packed| Self {
596                extended_year,
597                packed,
598            })
599    }
600
601    // 1-based day of year
602    fn month_day_for(self, day_of_year: u16) -> (u8, u8) {
603        // We divide by 30, not 29, to account for the case where all months before this
604        // were length 30 (possible near the beginning of the year)
605        let mut month = ((day_of_year - 1) / 30) as u8 + 1;
606        let mut days_before_month = self.packed.days_before_month(month);
607        let mut last_day_of_month = self.packed.days_before_month(month + 1);
608
609        while day_of_year > last_day_of_month {
610            month += 1;
611            days_before_month = last_day_of_month;
612            last_day_of_month = self.packed.days_before_month(month + 1);
613        }
614
615        (month, (day_of_year - days_before_month) as u8)
616    }
617
618    fn new_year(self) -> RataDie {
619        self.packed.new_year(self.extended_year)
620    }
621}
622
623/// A packed [`HijriYear`]
624///
625/// * `start_day` has to be within 5 days of the start of the year of the [`TabularAlgorithm`].
626/// * `month_lengths[n - 1]` has either 6 or 7 long months.
627///
628/// Bit structure
629///
630/// ```text
631/// Bit:              F.........C  B.............0
632/// Value:           [ start day ][ month lengths ]
633/// ```
634///
635/// The start day is encoded as a signed offset from `Self::mean_tabular_start_day`. This number does not
636/// appear to be less than 2, however we use all remaining bits for it in case of drift in the math.
637/// The month lengths are stored as 1 = 30, 0 = 29 for each month including the leap month.
638///
639/// <div class="stab unstable">
640/// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
641/// including in SemVer minor releases. While the serde representation of data structs is guaranteed
642/// to be stable, their Rust representation might not be. Use with caution.
643///
644/// Graduation tracking issue: [issue #6962](https://github.com/unicode-org/icu4x/issues/6962).
645/// </div>
646#[derive(Copy, Clone, Hash, Debug)]
647#[cfg_attr(test, derive(PartialEq))]
648struct PackedHijriYearData(u16);
649
650impl PackedHijriYearData {
651    const fn try_new(
652        extended_year: i32,
653        month_lengths: [bool; 12],
654        start_day: RataDie,
655    ) -> Option<Self> {
656        let start_offset @ -5..=5 = start_day.since(Self::mean_tabular_start_day(extended_year))
657        else {
658            return None;
659        };
660
661        let start_offset = start_offset as i8 & 0b1000_0111u8 as i8;
662
663        let mut all = 0u16;
664
665        let mut i = 0;
666        while i < 12 {
667            #[expect(clippy::indexing_slicing)]
668            if month_lengths[i] {
669                all |= 1 << i;
670            }
671            i += 1;
672        }
673
674        if start_offset < 0 {
675            all |= 1 << 12;
676        }
677        all |= (start_offset.unsigned_abs() as u16) << 13;
678        Some(Self(all))
679    }
680
681    const fn new_unchecked(
682        extended_year: i32,
683        month_lengths: [bool; 12],
684        start_day: RataDie,
685    ) -> Self {
686        let start_offset = start_day.since(Self::mean_tabular_start_day(extended_year));
687
688        let start_offset = start_offset as i8 & 0b1000_0111u8 as i8;
689
690        let mut all = 0u16;
691
692        let mut i = 0;
693        while i < 12 {
694            #[expect(clippy::indexing_slicing)]
695            if month_lengths[i] {
696                all |= 1 << i;
697            }
698            i += 1;
699        }
700
701        if start_offset < 0 {
702            all |= 1 << 12;
703        }
704        all |= (start_offset.unsigned_abs() as u16) << 13;
705        Self(all)
706    }
707
708    fn new_year(self, extended_year: i32) -> RataDie {
709        let start_offset = if (self.0 & 0b1_0000_0000_0000) != 0 {
710            -((self.0 >> 13) as i64)
711        } else {
712            (self.0 >> 13) as i64
713        };
714        Self::mean_tabular_start_day(extended_year) + start_offset
715    }
716
717    fn month_len(self, month: u8) -> u8 {
718        29 + (self.0 >> (month - 1) & 1) as u8
719    }
720
721    fn is_leap(self) -> bool {
722        (self.0 & ((1 << 12) - 1)).count_ones() == 7
723    }
724
725    // month is 1-indexed
726    fn days_before_month(self, month: u8) -> u16 {
727        // month is 1-indexed, so `29 * month` includes the current month
728        let mut prev_month_lengths = 29 * (month - 1) as u16;
729        // month is 1-indexed, so `1 << month` is a mask with all zeroes except
730        // for a 1 at the bit index at the next month. Subtracting 1 from it gets us
731        // a bitmask for all months up to now
732        let long_month_bits = self.0 & ((1 << (month - 1) as u16) - 1);
733        prev_month_lengths += long_month_bits.count_ones().try_into().unwrap_or(0);
734        prev_month_lengths
735    }
736
737    fn days_in_year(self) -> u16 {
738        self.days_before_month(13)
739    }
740
741    const fn mean_tabular_start_day(extended_year: i32) -> RataDie {
742        // -1 because the epoch is new year of year 1
743        calendrical_calculations::islamic::ISLAMIC_EPOCH_FRIDAY
744            .add((extended_year as i64 - 1) * (354 * 30 + 11) / 30)
745    }
746}
747
748impl PackWithMD for HijriYear {
749    /// The first two bytes are the [`PackedHijriYearData`], the remaining four the YMD as encoded by [`i32::pack`].
750    type Packed = [u8; 6];
751
752    fn pack(self, month: u8, day: u8) -> Self::Packed {
753        let [a, b] = self.packed.0.to_le_bytes();
754        let [c, d, e, f] = self.extended_year.pack(month, day);
755        [a, b, c, d, e, f]
756    }
757
758    fn unpack_year([a, b, c, d, e, f]: Self::Packed) -> Self {
759        let packed = PackedHijriYearData(u16::from_le_bytes([a, b]));
760        let extended_year = i32::unpack_year([c, d, e, f]);
761        Self {
762            extended_year,
763            packed,
764        }
765    }
766
767    fn unpack_month([_, _, c, d, e, f]: Self::Packed) -> u8 {
768        i32::unpack_month([c, d, e, f])
769    }
770
771    fn unpack_day([_, _, c, d, e, f]: Self::Packed) -> u8 {
772        i32::unpack_day([c, d, e, f])
773    }
774}
775
776#[allow(deprecated)]
777impl<A: AsCalendar<Calendar = Hijri<AstronomicalSimulation>>> Date<A> {
778    /// Deprecated
779    #[deprecated(since = "2.1.0", note = "use `Date::try_new_hijri_with_calendar`")]
780    pub fn try_new_simulated_hijri_with_calendar(
781        year: i32,
782        month: u8,
783        day: u8,
784        calendar: A,
785    ) -> Result<Date<A>, RangeError> {
786        Date::try_new_hijri_with_calendar(year, month, day, calendar)
787    }
788}
789
790#[test]
791fn computer_reference_years() {
792    fn compute_hijri_reference_year<R>(ordinal_month: u8, day: u8, rules: R) -> Option<HijriYear>
793    where
794        R: Rules + Copy,
795    {
796        let cal = Hijri(rules);
797        let dec_31 = Date::from_rata_die(
798            crate::cal::abstract_gregorian::LAST_DAY_OF_REFERENCE_YEAR,
799            cal,
800        );
801        // December 31, 1972 occurs in the 11th month, 1392 AH, but the day could vary
802        debug_assert_eq!(dec_31.month().ordinal, 11);
803        let (y0, y1, y2, y3) =
804            if ordinal_month < 11 || (ordinal_month == 11 && day <= dec_31.day_of_month().0) {
805                (1389, 1390, 1391, 1392)
806            } else {
807                (1388, 1389, 1390, 1391)
808            };
809        let year_info = cal.0.year(y3);
810        if day <= Hijri::<R>::days_in_provided_month(year_info, ordinal_month) {
811            return Some(year_info);
812        }
813        let year_info = cal.0.year(y2);
814        if day <= Hijri::<R>::days_in_provided_month(year_info, ordinal_month) {
815            return Some(year_info);
816        }
817        let year_info = cal.0.year(y1);
818        if day <= Hijri::<R>::days_in_provided_month(year_info, ordinal_month) {
819            return Some(year_info);
820        }
821        let year_info = cal.0.year(y0);
822        if day <= Hijri::<R>::days_in_provided_month(year_info, ordinal_month) {
823            return Some(year_info);
824        }
825        None
826    }
827
828    fn compute_all_reference_years<R>(rules: R)
829    where
830        R: Rules + Copy + Debug,
831    {
832        println!("Reference years for {:?}", rules);
833        for month in 1..=12 {
834            let y_29 = compute_hijri_reference_year(month, 29, rules)
835                .expect("All Hijri calendars have d = 29")
836                .extended_year;
837            let y_30 = compute_hijri_reference_year(month, 30, rules).map(|y| y.extended_year);
838
839            if let Some(y_30) = y_30
840                && y_29 != y_30
841            {
842                println!("({month}, 30..) => {y_30},")
843            }
844            if month == 11 {
845                let y_1 = compute_hijri_reference_year(month, 1, rules)
846                    .expect("All Hijri calendars have d = 1")
847                    .extended_year;
848                for day in 1..29 {
849                    let y_n = compute_hijri_reference_year(month, day, rules)
850                        .expect("All Hijri calendars have d < 29")
851                        .extended_year;
852                    if y_n != y_1 {
853                        println!("({month}, ..{day}) => {y_1},");
854                        break;
855                    }
856                }
857            }
858            println!("({month}, _) => {y_29},");
859        }
860    }
861
862    compute_all_reference_years(UmmAlQura);
863    compute_all_reference_years(TabularAlgorithm {
864        leap_years: TabularAlgorithmLeapYears::TypeII,
865        epoch: TabularAlgorithmEpoch::Friday,
866    });
867    compute_all_reference_years(TabularAlgorithm {
868        leap_years: TabularAlgorithmLeapYears::TypeII,
869        epoch: TabularAlgorithmEpoch::Thursday,
870    });
871}
872
873#[allow(clippy::derived_hash_with_manual_eq)] // bounds
874#[derive(Clone, Debug, Hash)]
875/// The inner date type used for representing [`Date`]s of [`Hijri`]. See [`Date`] and [`Hijri`] for more details.
876pub struct HijriDateInner<R: Rules>(ArithmeticDate<Hijri<R>>);
877
878impl<R: Rules> Copy for HijriDateInner<R> {}
879impl<R: Rules> PartialEq for HijriDateInner<R> {
880    fn eq(&self, other: &Self) -> bool {
881        self.0 == other.0
882    }
883}
884impl<R: Rules> Eq for HijriDateInner<R> {}
885impl<R: Rules> PartialOrd for HijriDateInner<R> {
886    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
887        Some(self.cmp(other))
888    }
889}
890impl<R: Rules> Ord for HijriDateInner<R> {
891    fn cmp(&self, other: &Self) -> Ordering {
892        self.0.cmp(&other.0)
893    }
894}
895
896impl<R: Rules> DateFieldsResolver for Hijri<R> {
897    type YearInfo = HijriYear;
898
899    fn days_in_provided_month(year: Self::YearInfo, month: u8) -> u8 {
900        year.packed.month_len(month)
901    }
902
903    #[inline]
904    fn extended_year_from_era_year_unchecked(
905        &self,
906        era: &[u8],
907        era_year: i32,
908    ) -> Result<i32, UnknownEraError> {
909        let extended_year = match era {
910            b"ah" => era_year,
911            b"bh" => 1 - era_year,
912            _ => return Err(UnknownEraError),
913        };
914        Ok(extended_year)
915    }
916
917    #[inline]
918    fn year_info_from_extended(&self, extended_year: i32) -> Self::YearInfo {
919        debug_assert!(crate::calendar_arithmetic::SAFE_YEAR_RANGE.contains(&extended_year));
920        self.0.year(extended_year)
921    }
922
923    #[inline]
924    fn extended_from_year_info(&self, year_info: Self::YearInfo) -> i32 {
925        year_info.extended_year
926    }
927
928    #[inline]
929    fn reference_year_from_month_day(
930        &self,
931        month: Month,
932        day: u8,
933    ) -> Result<Self::YearInfo, EcmaReferenceYearError> {
934        self.0
935            .ecma_reference_year(month, day)
936            .map(|y| self.0.year(y))
937    }
938
939    fn to_rata_die_inner(year: Self::YearInfo, month: u8, day: u8) -> RataDie {
940        year.new_year() + year.packed.days_before_month(month) as i64 + (day - 1) as i64
941    }
942}
943
944impl<R: Rules> crate::cal::scaffold::UnstableSealed for Hijri<R> {}
945impl<R: Rules> Calendar for Hijri<R> {
946    type DateInner = HijriDateInner<R>;
947    type Year = types::EraYear;
948    type DateCompatibilityError = R::DateCompatibilityError;
949
950    fn new_date(
951        &self,
952        year: types::YearInput,
953        month: Month,
954        day: u8,
955    ) -> Result<Self::DateInner, DateNewError> {
956        ArithmeticDate::from_input_year_month_code_day(year, month, day, self).map(HijriDateInner)
957    }
958
959    fn from_fields(
960        &self,
961        fields: DateFields,
962        options: DateFromFieldsOptions,
963    ) -> Result<Self::DateInner, DateFromFieldsError> {
964        ArithmeticDate::from_fields(fields, options, self).map(HijriDateInner)
965    }
966
967    fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
968        let year = self.0.year_containing_rd(rd);
969
970        let (month, day) = year.month_day_for((rd - year.new_year()) as u16 + 1);
971
972        // date is in the valid RD range
973        HijriDateInner(ArithmeticDate::new_unchecked(year, month, day))
974    }
975
976    fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
977        date.0.to_rata_die()
978    }
979
980    fn has_cheap_iso_conversion(&self) -> bool {
981        false
982    }
983
984    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
985        Self::months_in_provided_year(date.0.year())
986    }
987
988    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
989        date.0.year().packed.days_in_year()
990    }
991
992    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
993        Self::days_in_provided_month(date.0.year(), date.0.month())
994    }
995
996    fn add(
997        &self,
998        date: &Self::DateInner,
999        duration: types::DateDuration,
1000        options: DateAddOptions,
1001    ) -> Result<Self::DateInner, DateAddError> {
1002        date.0.added(duration, self, options).map(HijriDateInner)
1003    }
1004
1005    fn until(
1006        &self,
1007        date1: &Self::DateInner,
1008        date2: &Self::DateInner,
1009        options: DateDifferenceOptions,
1010    ) -> types::DateDuration {
1011        date1.0.until(&date2.0, self, options)
1012    }
1013
1014    fn check_date_compatibility(&self, other: &Self) -> Result<(), Self::DateCompatibilityError> {
1015        self.0.check_date_compatibility(&other.0)
1016    }
1017
1018    fn debug_name(&self) -> &'static str {
1019        self.0.debug_name()
1020    }
1021
1022    fn year_info(&self, date: &Self::DateInner) -> Self::Year {
1023        let extended_year = date.0.year().extended_year;
1024        if extended_year > 0 {
1025            types::EraYear {
1026                era: tinystr!(16, "ah"),
1027                era_index: Some(0),
1028                year: extended_year,
1029                extended_year,
1030                ambiguity: types::YearAmbiguity::CenturyRequired,
1031            }
1032        } else {
1033            types::EraYear {
1034                era: tinystr!(16, "bh"),
1035                era_index: Some(1),
1036                year: 1 - extended_year,
1037                extended_year,
1038                ambiguity: types::YearAmbiguity::CenturyRequired,
1039            }
1040        }
1041    }
1042
1043    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
1044        date.0.year().packed.is_leap()
1045    }
1046
1047    fn month(&self, date: &Self::DateInner) -> types::MonthInfo {
1048        types::MonthInfo::new(self, date.0)
1049    }
1050
1051    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
1052        types::DayOfMonth(date.0.day())
1053    }
1054
1055    fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
1056        types::DayOfYear(
1057            date.0.year().packed.days_before_month(date.0.month()) + date.0.day() as u16,
1058        )
1059    }
1060
1061    fn calendar_algorithm(&self) -> Option<CalendarAlgorithm> {
1062        self.0.calendar_algorithm()
1063    }
1064}
1065
1066impl<A: AsCalendar<Calendar = Hijri<R>>, R: Rules> Date<A> {
1067    /// Construct new Hijri [`Date`].
1068    ///
1069    /// Years are arithmetic, meaning there is a year 0 preceded by negative years, with a
1070    /// valid range of `-9999..=9999`.
1071    ///
1072    /// ```rust
1073    /// use icu::calendar::Date;
1074    /// use icu::calendar::cal::Hijri;
1075    ///
1076    /// let hijri = Hijri::new_umm_al_qura();
1077    ///
1078    /// let date_hijri = Date::try_new_hijri_with_calendar(1392, 4, 25, hijri)
1079    ///     .expect("Failed to initialize Hijri Date instance.");
1080    ///
1081    /// assert_eq!(date_hijri.era_year().year, 1392);
1082    /// assert_eq!(date_hijri.month().ordinal, 4);
1083    /// assert_eq!(date_hijri.day_of_month().0, 25);
1084    /// ```
1085    pub fn try_new_hijri_with_calendar(
1086        year: i32,
1087        month: u8,
1088        day: u8,
1089        calendar: A,
1090    ) -> Result<Self, RangeError> {
1091        ArithmeticDate::from_year_month_day(year, month, day, calendar.as_calendar())
1092            .map(HijriDateInner)
1093            .map(|inner| Date::from_raw(inner, calendar))
1094    }
1095}
1096
1097impl Date<Hijri<UmmAlQura>> {
1098    /// Deprecated
1099    #[deprecated(since = "2.1.0", note = "use `Date::try_new_hijri_with_calendar`")]
1100    pub fn try_new_ummalqura(year: i32, month: u8, day: u8) -> Result<Self, RangeError> {
1101        Date::try_new_hijri_with_calendar(year, month, day, Hijri::new_umm_al_qura())
1102    }
1103}
1104
1105impl<A: AsCalendar<Calendar = Hijri<TabularAlgorithm>>> Date<A> {
1106    /// Deprecated
1107    #[deprecated(since = "2.1.0", note = "use `Date::try_new_hijri_with_calendar`")]
1108    pub fn try_new_hijri_tabular_with_calendar(
1109        year: i32,
1110        month: u8,
1111        day: u8,
1112        calendar: A,
1113    ) -> Result<Date<A>, RangeError> {
1114        Date::try_new_hijri_with_calendar(year, month, day, calendar)
1115    }
1116}
1117
1118#[cfg(test)]
1119mod test {
1120    use super::*;
1121
1122    const START_YEAR: i32 = -1245;
1123    const END_YEAR: i32 = 1518;
1124
1125    #[derive(Debug)]
1126    struct DateCase {
1127        year: i32,
1128        month: u8,
1129        day: u8,
1130    }
1131
1132    static TEST_RD: [i64; 33] = [
1133        -214193, -61387, 25469, 49217, 171307, 210155, 253427, 369740, 400085, 434355, 452605,
1134        470160, 473837, 507850, 524156, 544676, 567118, 569477, 601716, 613424, 626596, 645554,
1135        664224, 671401, 694799, 704424, 708842, 709409, 709580, 727274, 728714, 744313, 764652,
1136    ];
1137
1138    static UMMALQURA_CASES: [DateCase; 33] = [
1139        DateCase {
1140            year: -1245,
1141            month: 12,
1142            day: 9,
1143        },
1144        DateCase {
1145            year: -813,
1146            month: 2,
1147            day: 23,
1148        },
1149        DateCase {
1150            year: -568,
1151            month: 4,
1152            day: 1,
1153        },
1154        DateCase {
1155            year: -501,
1156            month: 4,
1157            day: 6,
1158        },
1159        DateCase {
1160            year: -157,
1161            month: 10,
1162            day: 17,
1163        },
1164        DateCase {
1165            year: -47,
1166            month: 6,
1167            day: 3,
1168        },
1169        DateCase {
1170            year: 75,
1171            month: 7,
1172            day: 13,
1173        },
1174        DateCase {
1175            year: 403,
1176            month: 10,
1177            day: 5,
1178        },
1179        DateCase {
1180            year: 489,
1181            month: 5,
1182            day: 22,
1183        },
1184        DateCase {
1185            year: 586,
1186            month: 2,
1187            day: 7,
1188        },
1189        DateCase {
1190            year: 637,
1191            month: 8,
1192            day: 7,
1193        },
1194        DateCase {
1195            year: 687,
1196            month: 2,
1197            day: 20,
1198        },
1199        DateCase {
1200            year: 697,
1201            month: 7,
1202            day: 7,
1203        },
1204        DateCase {
1205            year: 793,
1206            month: 7,
1207            day: 1,
1208        },
1209        DateCase {
1210            year: 839,
1211            month: 7,
1212            day: 6,
1213        },
1214        DateCase {
1215            year: 897,
1216            month: 6,
1217            day: 1,
1218        },
1219        DateCase {
1220            year: 960,
1221            month: 9,
1222            day: 30,
1223        },
1224        DateCase {
1225            year: 967,
1226            month: 5,
1227            day: 27,
1228        },
1229        DateCase {
1230            year: 1058,
1231            month: 5,
1232            day: 18,
1233        },
1234        DateCase {
1235            year: 1091,
1236            month: 6,
1237            day: 2,
1238        },
1239        DateCase {
1240            year: 1128,
1241            month: 8,
1242            day: 4,
1243        },
1244        DateCase {
1245            year: 1182,
1246            month: 2,
1247            day: 3,
1248        },
1249        DateCase {
1250            year: 1234,
1251            month: 10,
1252            day: 10,
1253        },
1254        DateCase {
1255            year: 1255,
1256            month: 1,
1257            day: 11,
1258        },
1259        DateCase {
1260            year: 1321,
1261            month: 1,
1262            day: 21,
1263        },
1264        DateCase {
1265            year: 1348,
1266            month: 3,
1267            day: 20,
1268        },
1269        DateCase {
1270            year: 1360,
1271            month: 9,
1272            day: 7,
1273        },
1274        DateCase {
1275            year: 1362,
1276            month: 4,
1277            day: 14,
1278        },
1279        DateCase {
1280            year: 1362,
1281            month: 10,
1282            day: 7,
1283        },
1284        DateCase {
1285            year: 1412,
1286            month: 9,
1287            day: 12,
1288        },
1289        DateCase {
1290            year: 1416,
1291            month: 10,
1292            day: 6,
1293        },
1294        DateCase {
1295            year: 1460,
1296            month: 10,
1297            day: 13,
1298        },
1299        DateCase {
1300            year: 1518,
1301            month: 3,
1302            day: 5,
1303        },
1304    ];
1305
1306    static ARITHMETIC_CASES: [DateCase; 33] = [
1307        DateCase {
1308            year: -1245,
1309            month: 12,
1310            day: 9,
1311        },
1312        DateCase {
1313            year: -813,
1314            month: 2,
1315            day: 23,
1316        },
1317        DateCase {
1318            year: -568,
1319            month: 4,
1320            day: 1,
1321        },
1322        DateCase {
1323            year: -501,
1324            month: 4,
1325            day: 6,
1326        },
1327        DateCase {
1328            year: -157,
1329            month: 10,
1330            day: 17,
1331        },
1332        DateCase {
1333            year: -47,
1334            month: 6,
1335            day: 3,
1336        },
1337        DateCase {
1338            year: 75,
1339            month: 7,
1340            day: 13,
1341        },
1342        DateCase {
1343            year: 403,
1344            month: 10,
1345            day: 5,
1346        },
1347        DateCase {
1348            year: 489,
1349            month: 5,
1350            day: 22,
1351        },
1352        DateCase {
1353            year: 586,
1354            month: 2,
1355            day: 7,
1356        },
1357        DateCase {
1358            year: 637,
1359            month: 8,
1360            day: 7,
1361        },
1362        DateCase {
1363            year: 687,
1364            month: 2,
1365            day: 20,
1366        },
1367        DateCase {
1368            year: 697,
1369            month: 7,
1370            day: 7,
1371        },
1372        DateCase {
1373            year: 793,
1374            month: 7,
1375            day: 1,
1376        },
1377        DateCase {
1378            year: 839,
1379            month: 7,
1380            day: 6,
1381        },
1382        DateCase {
1383            year: 897,
1384            month: 6,
1385            day: 1,
1386        },
1387        DateCase {
1388            year: 960,
1389            month: 9,
1390            day: 30,
1391        },
1392        DateCase {
1393            year: 967,
1394            month: 5,
1395            day: 27,
1396        },
1397        DateCase {
1398            year: 1058,
1399            month: 5,
1400            day: 18,
1401        },
1402        DateCase {
1403            year: 1091,
1404            month: 6,
1405            day: 2,
1406        },
1407        DateCase {
1408            year: 1128,
1409            month: 8,
1410            day: 4,
1411        },
1412        DateCase {
1413            year: 1182,
1414            month: 2,
1415            day: 3,
1416        },
1417        DateCase {
1418            year: 1234,
1419            month: 10,
1420            day: 10,
1421        },
1422        DateCase {
1423            year: 1255,
1424            month: 1,
1425            day: 11,
1426        },
1427        DateCase {
1428            year: 1321,
1429            month: 1,
1430            day: 21,
1431        },
1432        DateCase {
1433            year: 1348,
1434            month: 3,
1435            day: 19,
1436        },
1437        DateCase {
1438            year: 1360,
1439            month: 9,
1440            day: 8,
1441        },
1442        DateCase {
1443            year: 1362,
1444            month: 4,
1445            day: 13,
1446        },
1447        DateCase {
1448            year: 1362,
1449            month: 10,
1450            day: 7,
1451        },
1452        DateCase {
1453            year: 1412,
1454            month: 9,
1455            day: 13,
1456        },
1457        DateCase {
1458            year: 1416,
1459            month: 10,
1460            day: 5,
1461        },
1462        DateCase {
1463            year: 1460,
1464            month: 10,
1465            day: 12,
1466        },
1467        DateCase {
1468            year: 1518,
1469            month: 3,
1470            day: 5,
1471        },
1472    ];
1473
1474    static ASTRONOMICAL_CASES: [DateCase; 33] = [
1475        DateCase {
1476            year: -1245,
1477            month: 12,
1478            day: 10,
1479        },
1480        DateCase {
1481            year: -813,
1482            month: 2,
1483            day: 24,
1484        },
1485        DateCase {
1486            year: -568,
1487            month: 4,
1488            day: 2,
1489        },
1490        DateCase {
1491            year: -501,
1492            month: 4,
1493            day: 7,
1494        },
1495        DateCase {
1496            year: -157,
1497            month: 10,
1498            day: 18,
1499        },
1500        DateCase {
1501            year: -47,
1502            month: 6,
1503            day: 4,
1504        },
1505        DateCase {
1506            year: 75,
1507            month: 7,
1508            day: 14,
1509        },
1510        DateCase {
1511            year: 403,
1512            month: 10,
1513            day: 6,
1514        },
1515        DateCase {
1516            year: 489,
1517            month: 5,
1518            day: 23,
1519        },
1520        DateCase {
1521            year: 586,
1522            month: 2,
1523            day: 8,
1524        },
1525        DateCase {
1526            year: 637,
1527            month: 8,
1528            day: 8,
1529        },
1530        DateCase {
1531            year: 687,
1532            month: 2,
1533            day: 21,
1534        },
1535        DateCase {
1536            year: 697,
1537            month: 7,
1538            day: 8,
1539        },
1540        DateCase {
1541            year: 793,
1542            month: 7,
1543            day: 2,
1544        },
1545        DateCase {
1546            year: 839,
1547            month: 7,
1548            day: 7,
1549        },
1550        DateCase {
1551            year: 897,
1552            month: 6,
1553            day: 2,
1554        },
1555        DateCase {
1556            year: 960,
1557            month: 10,
1558            day: 1,
1559        },
1560        DateCase {
1561            year: 967,
1562            month: 5,
1563            day: 28,
1564        },
1565        DateCase {
1566            year: 1058,
1567            month: 5,
1568            day: 19,
1569        },
1570        DateCase {
1571            year: 1091,
1572            month: 6,
1573            day: 3,
1574        },
1575        DateCase {
1576            year: 1128,
1577            month: 8,
1578            day: 5,
1579        },
1580        DateCase {
1581            year: 1182,
1582            month: 2,
1583            day: 4,
1584        },
1585        DateCase {
1586            year: 1234,
1587            month: 10,
1588            day: 11,
1589        },
1590        DateCase {
1591            year: 1255,
1592            month: 1,
1593            day: 12,
1594        },
1595        DateCase {
1596            year: 1321,
1597            month: 1,
1598            day: 22,
1599        },
1600        DateCase {
1601            year: 1348,
1602            month: 3,
1603            day: 20,
1604        },
1605        DateCase {
1606            year: 1360,
1607            month: 9,
1608            day: 9,
1609        },
1610        DateCase {
1611            year: 1362,
1612            month: 4,
1613            day: 14,
1614        },
1615        DateCase {
1616            year: 1362,
1617            month: 10,
1618            day: 8,
1619        },
1620        DateCase {
1621            year: 1412,
1622            month: 9,
1623            day: 14,
1624        },
1625        DateCase {
1626            year: 1416,
1627            month: 10,
1628            day: 6,
1629        },
1630        DateCase {
1631            year: 1460,
1632            month: 10,
1633            day: 13,
1634        },
1635        DateCase {
1636            year: 1518,
1637            month: 3,
1638            day: 6,
1639        },
1640    ];
1641
1642    #[test]
1643    fn test_rd_from_hijri() {
1644        let calendar = Hijri::new_tabular(
1645            TabularAlgorithmLeapYears::TypeII,
1646            TabularAlgorithmEpoch::Friday,
1647        );
1648        for (case, f_date) in ARITHMETIC_CASES.iter().zip(TEST_RD.iter()) {
1649            let date = Date::try_new_hijri_with_calendar(case.year, case.month, case.day, calendar)
1650                .unwrap();
1651            assert_eq!(date.to_rata_die(), RataDie::new(*f_date), "{case:?}");
1652        }
1653    }
1654
1655    #[test]
1656    fn test_hijri_from_rd() {
1657        let calendar = Hijri::new_tabular(
1658            TabularAlgorithmLeapYears::TypeII,
1659            TabularAlgorithmEpoch::Friday,
1660        );
1661        for (case, f_date) in ARITHMETIC_CASES.iter().zip(TEST_RD.iter()) {
1662            let date = Date::try_new_hijri_with_calendar(case.year, case.month, case.day, calendar)
1663                .unwrap();
1664            let date_rd = Date::from_rata_die(RataDie::new(*f_date), calendar);
1665
1666            assert_eq!(date, date_rd, "{case:?}");
1667        }
1668    }
1669
1670    #[test]
1671    fn test_rd_from_hijri_tbla() {
1672        let calendar = Hijri::new_tabular(
1673            TabularAlgorithmLeapYears::TypeII,
1674            TabularAlgorithmEpoch::Thursday,
1675        );
1676        for (case, f_date) in ASTRONOMICAL_CASES.iter().zip(TEST_RD.iter()) {
1677            let date = Date::try_new_hijri_with_calendar(case.year, case.month, case.day, calendar)
1678                .unwrap();
1679            assert_eq!(date.to_rata_die(), RataDie::new(*f_date), "{case:?}");
1680        }
1681    }
1682
1683    #[test]
1684    fn test_hijri_tbla_from_rd() {
1685        let calendar = Hijri::new_tabular(
1686            TabularAlgorithmLeapYears::TypeII,
1687            TabularAlgorithmEpoch::Thursday,
1688        );
1689        for (case, f_date) in ASTRONOMICAL_CASES.iter().zip(TEST_RD.iter()) {
1690            let date = Date::try_new_hijri_with_calendar(case.year, case.month, case.day, calendar)
1691                .unwrap();
1692            let date_rd = Date::from_rata_die(RataDie::new(*f_date), calendar);
1693
1694            assert_eq!(date, date_rd, "{case:?}");
1695        }
1696    }
1697
1698    #[test]
1699    fn test_saudi_hijri_from_rd() {
1700        let calendar = Hijri::new_umm_al_qura();
1701        for (case, f_date) in UMMALQURA_CASES.iter().zip(TEST_RD.iter()) {
1702            let date = Date::try_new_hijri_with_calendar(case.year, case.month, case.day, calendar)
1703                .unwrap();
1704            let date_rd = Date::from_rata_die(RataDie::new(*f_date), calendar);
1705
1706            assert_eq!(date, date_rd, "{case:?}");
1707        }
1708    }
1709
1710    #[test]
1711    fn test_rd_from_saudi_hijri() {
1712        let calendar = Hijri::new_umm_al_qura();
1713        for (case, f_date) in UMMALQURA_CASES.iter().zip(TEST_RD.iter()) {
1714            let date = Date::try_new_hijri_with_calendar(case.year, case.month, case.day, calendar)
1715                .unwrap();
1716            assert_eq!(date.to_rata_die(), RataDie::new(*f_date), "{case:?}");
1717        }
1718    }
1719
1720    #[ignore] // slow
1721    #[test]
1722    fn test_days_in_provided_year_ummalqura() {
1723        let calendar = Hijri::new_umm_al_qura();
1724        // -1245 1 1 = -214528 (R.D Date)
1725        // 1518 1 1 = 764588 (R.D Date)
1726        let sum_days_in_year: i64 = (START_YEAR..END_YEAR)
1727            .map(|year| calendar.0.year(year).packed.days_in_year() as i64)
1728            .sum();
1729        let expected_number_of_days = Date::try_new_hijri_with_calendar(END_YEAR, 1, 1, calendar)
1730            .unwrap()
1731            .to_rata_die()
1732            - (Date::try_new_hijri_with_calendar(START_YEAR, 1, 1, calendar).unwrap())
1733                .to_rata_die(); // The number of days between Umm al-Qura Hijri years -1245 and 1518
1734
1735        assert_eq!(sum_days_in_year, expected_number_of_days);
1736    }
1737
1738    #[test]
1739    fn test_regression_3868() {
1740        // This date used to panic on creation
1741        let date = Date::try_new_iso(2011, 4, 4)
1742            .unwrap()
1743            .to_calendar(Hijri::new_umm_al_qura());
1744        // Data from https://www.ummulqura.org.sa/Index.aspx
1745        assert_eq!(date.day_of_month().0, 30);
1746        assert_eq!(date.month().ordinal, 4);
1747        assert_eq!(date.era_year().year, 1432);
1748    }
1749
1750    #[test]
1751    fn test_regression_4914() {
1752        // https://github.com/unicode-org/icu4x/issues/4914
1753        let dt = Hijri::new_umm_al_qura()
1754            .new_date(types::YearInput::EraYear("bh", 6824), Month::new(1), 1)
1755            .unwrap();
1756        assert_eq!(dt.0.day(), 1);
1757        assert_eq!(dt.0.month(), 1);
1758        assert_eq!(dt.0.year().extended_year, -6823);
1759    }
1760
1761    #[test]
1762    fn test_regression_7056() {
1763        // https://github.com/unicode-org/icu4x/issues/7056
1764        let calendar = Hijri::new_tabular(
1765            TabularAlgorithmLeapYears::TypeII,
1766            TabularAlgorithmEpoch::Friday,
1767        );
1768        let _dt = Date::from_rata_die(
1769            calendrical_calculations::gregorian::fixed_from_gregorian(-62971, 3, 19),
1770            calendar,
1771        );
1772    }
1773
1774    #[test]
1775    fn test_regression_5069_uaq() {
1776        let calendar = Hijri::new_umm_al_qura();
1777
1778        let dt = Date::try_new_hijri_with_calendar(1391, 1, 29, calendar).unwrap();
1779
1780        assert_eq!(Date::from_rata_die(dt.to_rata_die(), calendar), dt);
1781    }
1782
1783    #[test]
1784    fn test_regression_6197() {
1785        let calendar = Hijri::new_umm_al_qura();
1786
1787        let date = Date::try_new_iso(2025, 2, 26)
1788            .unwrap()
1789            .to_calendar(calendar);
1790
1791        // Data from https://www.ummulqura.org.sa/
1792        assert_eq!(
1793            (
1794                date.day_of_month().0,
1795                date.month().ordinal,
1796                date.era_year().year
1797            ),
1798            (27, 8, 1446)
1799        );
1800    }
1801
1802    #[test]
1803    fn test_hijri_packed_roundtrip() {
1804        #[track_caller]
1805        fn single_roundtrip(month_lengths: [bool; 12], start_day: RataDie) {
1806            let packed = PackedHijriYearData::try_new(1600, month_lengths, start_day).unwrap();
1807            for i in 0..12 {
1808                assert_eq!(packed.month_len(i + 1) == 30, month_lengths[i as usize]);
1809            }
1810            assert_eq!(packed.new_year(1600), start_day);
1811        }
1812
1813        let l = true;
1814        let s = false;
1815        let all_short = [s; 12];
1816        let all_long = [l; 12];
1817        let mixed1 = [l, s, l, s, l, s, l, s, l, s, l, s];
1818        let mixed2 = [s, s, l, l, l, s, l, s, s, s, l, l];
1819
1820        let start_1600 = PackedHijriYearData::mean_tabular_start_day(1600);
1821        single_roundtrip(all_short, start_1600);
1822        single_roundtrip(all_long, start_1600);
1823        single_roundtrip(mixed1, start_1600);
1824        single_roundtrip(mixed2, start_1600);
1825
1826        single_roundtrip(mixed1, start_1600 - 5);
1827        single_roundtrip(mixed2, start_1600 + 5);
1828        single_roundtrip(mixed2, start_1600 + 4);
1829        single_roundtrip(mixed2, start_1600 + 1);
1830        single_roundtrip(mixed2, start_1600 - 1);
1831        single_roundtrip(mixed2, start_1600 - 4);
1832    }
1833}