icu_calendar/provider/
islamic.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 crate::islamic::IslamicYearInfo;
16use calendrical_calculations::islamic::IslamicBasedMarker;
17use calendrical_calculations::rata_die::RataDie;
18use core::fmt;
19use icu_provider::prelude::*;
20use zerovec::ule::{AsULE, ULE};
21use zerovec::ZeroVec;
22
23/// Cached/precompiled data for a certain range of years for a chinese-based
24/// calendar. Avoids the need to perform lunar calendar arithmetic for most calendrical
25/// operations.
26#[icu_provider::data_struct(
27    marker(
28        IslamicObservationalCacheV1Marker,
29        "calendar/islamicobservationalcache@1",
30        singleton
31    ),
32    marker(
33        IslamicUmmAlQuraCacheV1Marker,
34        "calendar/islamicummalquracache@1",
35        singleton
36    )
37)]
38#[derive(Debug, PartialEq, Clone, Default)]
39#[cfg_attr(
40    feature = "datagen",
41    derive(serde::Serialize, databake::Bake),
42    databake(path = icu_calendar::provider::islamic),
43)]
44#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
45pub struct IslamicCacheV1<'data> {
46    /// The extended year corresponding to the first data entry for this year
47    pub first_extended_year: i32,
48    /// A list of precomputed data for each year beginning with first_extended_year
49    #[cfg_attr(feature = "serde", serde(borrow))]
50    pub data: ZeroVec<'data, PackedIslamicYearInfo>,
51}
52
53impl<'data> IslamicCacheV1<'data> {
54    /// Compute this data for a range of years
55    #[cfg(feature = "datagen")]
56    pub fn compute_for<IB: IslamicBasedMarker>(extended_years: core::ops::Range<i32>) -> Self {
57        let data = extended_years
58            .clone()
59            .map(|year| PackedIslamicYearInfo::compute::<IB>(year))
60            .collect();
61        IslamicCacheV1 {
62            first_extended_year: extended_years.start,
63            data,
64        }
65    }
66
67    /// Get the cached data for a given extended year
68    pub(crate) fn get_for_extended_year(&self, extended_year: i32) -> Option<IslamicYearInfo> {
69        let delta = extended_year - self.first_extended_year;
70        let delta = usize::try_from(delta).ok()?;
71
72        if delta == 0 {
73            return None;
74        }
75
76        let (Some(this_packed), Some(prev_packed)) =
77            (self.data.get(delta), self.data.get(delta - 1))
78        else {
79            return None;
80        };
81
82        Some(IslamicYearInfo::new(prev_packed, this_packed, extended_year).0)
83    }
84    /// Get the cached data for the Islamic Year corresponding to a given day.
85    ///
86    /// Also returns the corresponding extended year.
87    pub(crate) fn get_for_fixed<IB: IslamicBasedMarker>(
88        &self,
89        fixed: RataDie,
90    ) -> Option<(IslamicYearInfo, i32)> {
91        let extended_year = IB::approximate_islamic_from_fixed(fixed);
92
93        let delta = extended_year - self.first_extended_year;
94        let delta = usize::try_from(delta).ok()?;
95
96        if delta <= 1 {
97            return None;
98        }
99
100        let this_packed = self.data.get(delta)?;
101        let prev_packed = self.data.get(delta + 1)?;
102
103        let this_ny = this_packed.ny::<IB>(extended_year);
104
105        if fixed < this_ny {
106            let prev2_packed = self.data.get(delta - 2)?;
107            return Some(IslamicYearInfo::new(
108                prev2_packed,
109                prev_packed,
110                extended_year - 1,
111            ));
112        }
113        let next_packed = self.data.get(delta + 1)?;
114        let next_ny = next_packed.ny::<IB>(extended_year + 1);
115
116        if fixed >= next_ny {
117            Some(IslamicYearInfo::new(
118                this_packed,
119                next_packed,
120                extended_year + 1,
121            ))
122        } else {
123            Some(IslamicYearInfo::new(
124                prev_packed,
125                this_packed,
126                extended_year,
127            ))
128        }
129    }
130}
131
132/// The struct containing compiled Islamic YearInfo
133///
134/// Bit structure (little endian: note that shifts go in the opposite direction!)
135///
136/// ```text
137/// Bit:             0   1   2   3   4   5   6   7
138/// Byte 0:          [  month lengths .............
139/// Byte 1:         .. months    ] | [ ny offset    ]
140/// ```
141///
142/// Where the New Year Offset is a signed offset from `epoch + MEAN_SYNODIC_MONTH * year * 12` for the given
143/// calendar. This number does not appear to be less than 2, however we use all remaining bits for it in case of drift
144/// in the math.
145/// The month lengths are stored as 1 = 30, 0 = 29 for each month including the leap month.
146///
147/// <div class="stab unstable">
148/// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
149/// including in SemVer minor releases. While the serde representation of data structs is guaranteed
150/// to be stable, their Rust representation might not be. Use with caution.
151/// </div>
152#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, ULE)]
153#[cfg_attr(
154    feature = "datagen",
155    derive(serde::Serialize, databake::Bake),
156    databake(path = icu_calendar::provider),
157)]
158#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
159#[repr(C, packed)]
160pub struct PackedIslamicYearInfo(pub u8, pub u8);
161
162impl fmt::Debug for PackedIslamicYearInfo {
163    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
164        fmt.debug_struct("PackedIslamicYearInfo")
165            .field("ny_offset", &self.ny_offset())
166            .field("month_lengths", &self.month_lengths())
167            .finish()
168    }
169}
170
171impl PackedIslamicYearInfo {
172    pub(crate) fn new(month_lengths: [bool; 12], ny_offset: i8) -> Self {
173        debug_assert!(
174            -8 < ny_offset && ny_offset < 8,
175            "Year offset too big to store"
176        );
177
178        let mut all = 0u16; // last byte unused
179
180        for (month, length_30) in month_lengths.iter().enumerate() {
181            #[allow(clippy::indexing_slicing)]
182            if *length_30 {
183                all |= 1 << month as u16;
184            }
185        }
186
187        if ny_offset < 0 {
188            all |= 1 << 12;
189        }
190        all |= u16::from(ny_offset.unsigned_abs()) << 13;
191        let le = all.to_le_bytes();
192        Self(le[0], le[1])
193    }
194
195    fn month_lengths(self) -> [u8; 12] {
196        let months: [u8; 12] = core::array::from_fn(|i| 1 + i as u8);
197        months.map(|x| if self.month_has_30_days(x) { 30 } else { 29 })
198    }
199
200    // Get the new year offset from the mean synodic new year
201    pub(crate) fn ny_offset(self) -> i8 {
202        let masked = (self.1 >> 5) as i8;
203        if (self.1 & 0b10000) != 0 {
204            -masked
205        } else {
206            masked
207        }
208    }
209    // Get the new year offset from the mean synodic new year
210    pub(crate) fn ny<IB: IslamicBasedMarker>(self, extended_year: i32) -> RataDie {
211        let mean_synodic_ny = IB::mean_synodic_ny(extended_year);
212        mean_synodic_ny + i64::from(self.ny_offset())
213    }
214
215    // Whether a particular month has 30 days (month is 1-indexed)
216    pub(crate) fn month_has_30_days(self, month: u8) -> bool {
217        let months = u16::from_le_bytes([self.0, self.1]);
218        months & (1 << (month - 1) as u16) != 0
219    }
220
221    /// The number of days in a given 1-indexed month
222    pub(crate) fn days_in_month(self, month: u8) -> u8 {
223        if self.month_has_30_days(month) {
224            30
225        } else {
226            29
227        }
228    }
229
230    // Which day of year is the last day of a month (month is 1-indexed)
231    pub(crate) fn last_day_of_month(self, month: u8) -> u16 {
232        let months = u16::from_le_bytes([self.0, self.1]);
233        // month is 1-indexed, so `29 * month` includes the current month
234        let mut prev_month_lengths = 29 * month as u16;
235        // month is 1-indexed, so `1 << month` is a mask with all zeroes except
236        // for a 1 at the bit index at the next month. Subtracting 1 from it gets us
237        // a bitmask for all months up to now
238        let long_month_bits = months & ((1 << month as u16) - 1);
239        prev_month_lengths += long_month_bits.count_ones().try_into().unwrap_or(0);
240        prev_month_lengths
241    }
242
243    pub(crate) fn days_in_year(self) -> u16 {
244        self.last_day_of_month(12)
245    }
246
247    pub(crate) fn compute_with_ny<IB: IslamicBasedMarker>(extended_year: i32, ny: RataDie) -> Self {
248        let month_lengths = IB::month_lengths_for_year(extended_year, ny);
249        let ny_offset = ny - IB::mean_synodic_ny(extended_year);
250        let ny_offset = if !(-7..=7).contains(&ny_offset) {
251            0
252        } else {
253            ny_offset as i8
254        };
255        Self::new(month_lengths, ny_offset)
256    }
257    #[cfg(feature = "datagen")]
258    pub(crate) fn compute<IB: IslamicBasedMarker>(extended_year: i32) -> Self {
259        let ny = IB::fixed_from_islamic(extended_year, 1, 1);
260        Self::compute_with_ny::<IB>(extended_year, ny)
261    }
262}
263
264impl AsULE for PackedIslamicYearInfo {
265    type ULE = Self;
266    fn to_unaligned(self) -> Self {
267        self
268    }
269    fn from_unaligned(other: Self) -> Self {
270        other
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    fn single_roundtrip(month_lengths: [bool; 12], ny_offset: i8) {
279        let packed = PackedIslamicYearInfo::new(month_lengths, ny_offset);
280        for i in 0..12 {
281            assert_eq!(packed.month_has_30_days(i + 1), month_lengths[i as usize], "Month lengths must match for testcase {month_lengths:?} / {ny_offset}, with packed repr: {packed:?}");
282        }
283        assert_eq!(packed.ny_offset(), ny_offset, "Month lengths must match for testcase {month_lengths:?} / {ny_offset}, with packed repr: {packed:?}");
284    }
285    const ALL_FALSE: [bool; 12] = [false; 12];
286    const ALL_TRUE: [bool; 12] = [true; 12];
287    const MIXED1: [bool; 12] = [
288        true, false, true, false, true, false, true, false, true, false, true, false,
289    ];
290    const MIXED2: [bool; 12] = [
291        false, false, true, true, true, false, true, false, false, false, true, true,
292    ];
293    #[test]
294    fn test_islamic_packed_roundtrip() {
295        single_roundtrip(ALL_FALSE, 0);
296        single_roundtrip(ALL_TRUE, 0);
297        single_roundtrip(MIXED1, 0);
298        single_roundtrip(MIXED2, 0);
299
300        single_roundtrip(MIXED1, -7);
301        single_roundtrip(MIXED2, 7);
302        single_roundtrip(MIXED2, 4);
303        single_roundtrip(MIXED2, 1);
304        single_roundtrip(MIXED2, -1);
305        single_roundtrip(MIXED2, -4);
306    }
307}