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