icu_calendar/provider/
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
5//! 🚧 \[Unstable\] Data provider struct definitions for chinese-based calendars.
6//!
7//! <div class="stab unstable">
8//! 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
9//! including in SemVer minor releases. While the serde representation of data structs is guaranteed
10//! to be stable, their Rust representation might not be. Use with caution.
11//! </div>
12//!
13//! Read more about data providers: [`icu_provider`]
14
15use calendrical_calculations::rata_die::RataDie;
16use icu_provider::prelude::*;
17use zerovec::ule::AsULE;
18use zerovec::ZeroVec;
19
20icu_provider::data_marker!(
21    /// Precomputed data for the Hijri obsevational calendar
22    CalendarHijriSimulatedMeccaV1,
23    "calendar/hijri/simulated/mecca/v1",
24    HijriData<'static>,
25    is_singleton = true,
26);
27
28/// Cached/precompiled data for a certain range of years for a chinese-based
29/// calendar. Avoids the need to perform lunar calendar arithmetic for most calendrical
30/// operations.
31#[derive(Debug, PartialEq, Clone, Default, yoke::Yokeable, zerofrom::ZeroFrom)]
32#[cfg_attr(feature = "datagen", derive(serde::Serialize, databake::Bake))]
33#[cfg_attr(feature = "datagen", databake(path = icu_calendar::provider::hijri))]
34#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
35pub struct HijriData<'data> {
36    /// The extended year corresponding to the first data entry for this year
37    pub first_extended_year: i32,
38    /// A list of precomputed data for each year beginning with first_extended_year
39    #[cfg_attr(feature = "serde", serde(borrow))]
40    pub data: ZeroVec<'data, PackedHijriYearInfo>,
41}
42
43icu_provider::data_struct!(
44    HijriData<'_>,
45    #[cfg(feature = "datagen")]
46);
47
48/// The struct containing compiled Hijri YearInfo
49///
50/// Bit structure
51///
52/// ```text
53/// Bit:              F.........C  B.............0
54/// Value:           [ start day ][ month lengths ]
55/// ```
56///
57/// The start day is encoded as a signed offset from `Self::mean_synodic_start_day`. This number does not
58/// appear to be less than 2, however we use all remaining bits for it in case of drift in the math.
59/// The month lengths are stored as 1 = 30, 0 = 29 for each month including the leap month.
60///
61/// <div class="stab unstable">
62/// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
63/// including in SemVer minor releases. While the serde representation of data structs is guaranteed
64/// to be stable, their Rust representation might not be. Use with caution.
65/// </div>
66#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Debug)]
67#[cfg_attr(feature = "datagen", derive(serde::Serialize, databake::Bake))]
68#[cfg_attr(feature = "datagen", databake(path = icu_calendar::provider))]
69#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
70pub struct PackedHijriYearInfo(pub u16);
71
72impl PackedHijriYearInfo {
73    pub(crate) const fn new(
74        extended_year: i32,
75        month_lengths: [bool; 12],
76        start_day: RataDie,
77    ) -> Self {
78        let start_offset = start_day.until(Self::mean_synodic_start_day(extended_year));
79
80        debug_assert!(
81            -8 < start_offset && start_offset < 8,
82            "Year offset too big to store"
83        );
84        let start_offset = start_offset as i8;
85
86        let mut all = 0u16; // last byte unused
87
88        let mut i = 0;
89        while i < 12 {
90            #[allow(clippy::indexing_slicing)]
91            if month_lengths[i] {
92                all |= 1 << i;
93            }
94            i += 1;
95        }
96
97        if start_offset < 0 {
98            all |= 1 << 12;
99        }
100        all |= (start_offset.unsigned_abs() as u16) << 13;
101        Self(all)
102    }
103
104    pub(crate) fn unpack(self, extended_year: i32) -> ([bool; 12], RataDie) {
105        let month_lengths = core::array::from_fn(|i| self.0 & (1 << (i as u8) as u16) != 0);
106        let start_offset = if (self.0 & 0b1_0000_0000_0000) != 0 {
107            -((self.0 >> 13) as i64)
108        } else {
109            (self.0 >> 13) as i64
110        };
111        (
112            month_lengths,
113            Self::mean_synodic_start_day(extended_year) + start_offset,
114        )
115    }
116
117    const fn mean_synodic_start_day(extended_year: i32) -> RataDie {
118        // -1 because the epoch is new year of year 1
119        // truncating instead of flooring does not matter, as this is used for positive years only
120        calendrical_calculations::islamic::ISLAMIC_EPOCH_FRIDAY.add(
121            ((extended_year - 1) as f64 * calendrical_calculations::islamic::MEAN_YEAR_LENGTH)
122                as i64,
123        )
124    }
125}
126
127impl AsULE for PackedHijriYearInfo {
128    type ULE = <u16 as AsULE>::ULE;
129    fn from_unaligned(unaligned: Self::ULE) -> Self {
130        Self(<u16 as AsULE>::from_unaligned(unaligned))
131    }
132    fn to_unaligned(self) -> Self::ULE {
133        <u16 as AsULE>::to_unaligned(self.0)
134    }
135}
136
137#[test]
138fn test_hijri_packed_roundtrip() {
139    fn single_roundtrip(month_lengths: [bool; 12], year_start: RataDie) {
140        let packed = PackedHijriYearInfo::new(1600, month_lengths, year_start);
141        let (month_lengths2, year_start2) = packed.unpack(1600);
142        assert_eq!(month_lengths, month_lengths2, "Month lengths must match for testcase {month_lengths:?} / {year_start:?}, with packed repr: {packed:?}");
143        assert_eq!(year_start, year_start2, "Month lengths must match for testcase {month_lengths:?} / {year_start:?}, with packed repr: {packed:?}");
144    }
145
146    let l = true;
147    let s = false;
148    let all_short = [s; 12];
149    let all_long = [l; 12];
150    let mixed1 = [l, s, l, s, l, s, l, s, l, s, l, s];
151    let mixed2 = [s, s, l, l, l, s, l, s, s, s, l, l];
152
153    let start_1600 = PackedHijriYearInfo::mean_synodic_start_day(1600);
154    single_roundtrip(all_short, start_1600);
155    single_roundtrip(all_long, start_1600);
156    single_roundtrip(mixed1, start_1600);
157    single_roundtrip(mixed2, start_1600);
158
159    single_roundtrip(mixed1, start_1600 - 7);
160    single_roundtrip(mixed2, start_1600 + 7);
161    single_roundtrip(mixed2, start_1600 + 4);
162    single_roundtrip(mixed2, start_1600 + 1);
163    single_roundtrip(mixed2, start_1600 - 1);
164    single_roundtrip(mixed2, start_1600 - 4);
165}