icu_calendar/provider/
chinese_based.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::calendar_arithmetic::ArithmeticDate;
16use crate::chinese_based::ChineseBasedYearInfo;
17use crate::Iso;
18use calendrical_calculations::chinese_based::ChineseBased;
19use calendrical_calculations::rata_die::RataDie;
20use core::num::NonZeroU8;
21use icu_provider::prelude::*;
22use zerovec::ule::{AsULE, ULE};
23use zerovec::ZeroVec;
24
25/// Cached/precompiled data for a certain range of years for a chinese-based
26/// calendar. Avoids the need to perform lunar calendar arithmetic for most calendrical
27/// operations.
28#[icu_provider::data_struct(
29    marker(ChineseCacheV1Marker, "calendar/chinesecache@1", singleton),
30    marker(DangiCacheV1Marker, "calendar/dangicache@1", singleton)
31)]
32#[derive(Debug, PartialEq, Clone, Default)]
33#[cfg_attr(
34    feature = "datagen",
35    derive(serde::Serialize, databake::Bake),
36    databake(path = icu_calendar::provider::chinese_based),
37)]
38#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
39pub struct ChineseBasedCacheV1<'data> {
40    /// The extended year corresponding to the first data entry for this year
41    pub first_extended_year: i32,
42    /// A list of precomputed data for each year beginning with first_extended_year
43    #[cfg_attr(feature = "serde", serde(borrow))]
44    pub data: ZeroVec<'data, PackedChineseBasedYearInfo>,
45}
46
47impl<'data> ChineseBasedCacheV1<'data> {
48    /// Compute this data for a range of years
49    #[cfg(feature = "datagen")]
50    pub fn compute_for<CB: ChineseBased>(extended_years: core::ops::Range<i32>) -> Self {
51        let data = crate::chinese_based::compute_many_packed::<CB>(extended_years.clone());
52        ChineseBasedCacheV1 {
53            first_extended_year: extended_years.start,
54            data: data.into(),
55        }
56    }
57
58    /// Get the cached data for a given extended year
59    pub(crate) fn get_for_extended_year(&self, extended_year: i32) -> Option<ChineseBasedYearInfo> {
60        let delta = extended_year - self.first_extended_year;
61        let delta = usize::try_from(delta).ok()?;
62
63        if delta == 0 {
64            return None;
65        }
66
67        let (Some(this_packed), Some(prev_packed)) =
68            (self.data.get(delta), self.data.get(delta - 1))
69        else {
70            return None;
71        };
72
73        let days_in_prev_year = prev_packed.days_in_year();
74
75        Some(ChineseBasedYearInfo::new(days_in_prev_year, this_packed))
76    }
77    /// Get the cached data for the Chinese Year corresponding to a given day.
78    ///
79    /// Also returns the corresponding extended year.
80    pub(crate) fn get_for_iso<CB: ChineseBased>(
81        &self,
82        iso: ArithmeticDate<Iso>,
83    ) -> Option<(ChineseBasedYearInfo, i32)> {
84        let extended_year = CB::extended_from_iso(iso.year);
85        let delta = extended_year - self.first_extended_year;
86        let delta = usize::try_from(delta).ok()?;
87        if delta <= 1 {
88            return None;
89        }
90
91        let this_packed = self.data.get(delta)?;
92        let prev_packed = self.data.get(delta - 1)?;
93
94        let iso_in_year = iso.day_of_year();
95        let fetched_data_ny_in_iso = u16::from(this_packed.ny_day_of_iso_year());
96
97        if iso_in_year >= fetched_data_ny_in_iso {
98            Some((
99                ChineseBasedYearInfo::new(prev_packed.days_in_year(), this_packed),
100                extended_year,
101            ))
102        } else {
103            // We're dealing with an ISO day in the beginning of the year, before Chinese New Year.
104            // Return data for the previous Chinese year instead.
105            if delta <= 2 {
106                return None;
107            }
108            let prev2_packed = self.data.get(delta - 2)?;
109
110            let days_in_prev_year = prev2_packed.days_in_year();
111
112            Some((
113                ChineseBasedYearInfo::new(days_in_prev_year, prev_packed),
114                extended_year - 1,
115            ))
116        }
117    }
118}
119
120/// The struct containing compiled ChineseData
121///
122/// Bit structure (little endian: note that shifts go in the opposite direction!)
123///
124/// ```text
125/// Bit:             0   1   2   3   4   5   6   7
126/// Byte 0:          [  month lengths .............
127/// Byte 1:         .. month lengths ] | [ leap month index ..
128/// Byte 2:          ] | [   NY offset       ] | unused
129/// ```
130///
131/// Where the New Year Offset is the offset from ISO Jan 21 of that year for Chinese New Year,
132/// the month lengths are stored as 1 = 30, 0 = 29 for each month including the leap month.
133/// The largest possible offset is 33, which requires 6 bits of storage.
134///
135/// <div class="stab unstable">
136/// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
137/// including in SemVer minor releases. While the serde representation of data structs is guaranteed
138/// to be stable, their Rust representation might not be. Use with caution.
139/// </div>
140#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ULE)]
141#[cfg_attr(
142    feature = "datagen",
143    derive(databake::Bake),
144    databake(path = icu_calendar::provider),
145)]
146#[repr(C, packed)]
147pub struct PackedChineseBasedYearInfo(pub u8, pub u8, pub u8);
148
149impl PackedChineseBasedYearInfo {
150    /// The first day of the ISO year on which Chinese New Year may occur
151    ///
152    /// According to Reingold & Dershowitz, ch 19.6, Chinese New Year occurs on Jan 21 - Feb 21 inclusive.
153    ///
154    /// Chinese New Year in the year 30 AD is January 20 (30-01-20).
155    ///
156    /// We allow it to occur as early as January 19 which is the earliest the second new moon
157    /// could occur after the Winter Solstice if the solstice is pinned to December 20.
158    pub(crate) const FIRST_NY: u8 = 19;
159
160    pub(crate) fn new(
161        month_lengths: [bool; 13],
162        leap_month_idx: Option<NonZeroU8>,
163        ny_offset: u8,
164    ) -> Self {
165        debug_assert!(
166            !month_lengths[12] || leap_month_idx.is_some(),
167            "Last month length should not be set for non-leap years"
168        );
169        debug_assert!(ny_offset < 34, "Year offset too big to store");
170        debug_assert!(
171            leap_month_idx.map(|l| l.get() <= 13).unwrap_or(true),
172            "Leap month indices must be 1 <= i <= 13"
173        );
174        let mut all = 0u32; // last byte unused
175
176        for (month, length_30) in month_lengths.iter().enumerate() {
177            #[allow(clippy::indexing_slicing)]
178            if *length_30 {
179                all |= 1 << month as u32;
180            }
181        }
182        let leap_month_idx = leap_month_idx.map(|x| x.get()).unwrap_or(0);
183        all |= (leap_month_idx as u32) << (8 + 5);
184        all |= (ny_offset as u32) << (16 + 1);
185        let le = all.to_le_bytes();
186        Self(le[0], le[1], le[2])
187    }
188
189    // Get the new year offset from January 21
190    pub(crate) fn ny_offset(self) -> u8 {
191        self.2 >> 1
192    }
193
194    /// The day of the year (1-indexed) that this is in the ISO year
195    fn ny_day_of_iso_year(self) -> u8 {
196        let ny_offset = self.ny_offset();
197        // FIRST_NY is one-indexed, offset is an offset, we can just add
198        Self::FIRST_NY + ny_offset
199    }
200
201    pub(crate) fn ny_rd(self, related_iso: i32) -> RataDie {
202        let iso_ny = calendrical_calculations::iso::fixed_from_iso(related_iso, 1, 1);
203        // -1 because `iso_ny` is itself in the year, and ny_day_of_iso_year
204        iso_ny + i64::from(self.ny_day_of_iso_year()) - 1
205    }
206
207    pub(crate) fn leap_month_idx(self) -> Option<NonZeroU8> {
208        let low_bits = self.1 >> 5;
209        let high_bits = (self.2 & 0b1) << 3;
210
211        NonZeroU8::new(low_bits + high_bits)
212    }
213
214    // Whether a particular month has 30 days (month is 1-indexed)
215    #[cfg(any(test, feature = "datagen"))]
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    // Which day of year is the last day of a month (month is 1-indexed)
222    pub(crate) fn last_day_of_month(self, month: u8) -> u16 {
223        let months = u16::from_le_bytes([self.0, self.1]);
224        // month is 1-indexed, so `29 * month` includes the current month
225        let mut prev_month_lengths = 29 * month as u16;
226        // month is 1-indexed, so `1 << month` is a mask with all zeroes except
227        // for a 1 at the bit index at the next month. Subtracting 1 from it gets us
228        // a bitmask for all months up to now
229        let long_month_bits = months & ((1 << month as u16) - 1);
230        prev_month_lengths += long_month_bits.count_ones().try_into().unwrap_or(0);
231        prev_month_lengths
232    }
233
234    pub(crate) fn days_in_year(self) -> u16 {
235        if self.leap_month_idx().is_some() {
236            self.last_day_of_month(13)
237        } else {
238            self.last_day_of_month(12)
239        }
240    }
241}
242
243impl AsULE for PackedChineseBasedYearInfo {
244    type ULE = Self;
245    fn to_unaligned(self) -> Self {
246        self
247    }
248    fn from_unaligned(other: Self) -> Self {
249        other
250    }
251}
252
253#[cfg(feature = "serde")]
254mod serialization {
255    use super::*;
256
257    #[cfg(feature = "datagen")]
258    use serde::{ser, Serialize};
259    use serde::{Deserialize, Deserializer};
260
261    #[derive(Deserialize)]
262    #[cfg_attr(feature = "datagen", derive(Serialize))]
263    struct SerdePackedChineseBasedYearInfo {
264        ny_offset: u8,
265        month_has_30_days: [bool; 13],
266        leap_month_idx: Option<NonZeroU8>,
267    }
268
269    impl<'de> Deserialize<'de> for PackedChineseBasedYearInfo {
270        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
271        where
272            D: Deserializer<'de>,
273        {
274            if deserializer.is_human_readable() {
275                SerdePackedChineseBasedYearInfo::deserialize(deserializer).map(Into::into)
276            } else {
277                let data = <(u8, u8, u8)>::deserialize(deserializer)?;
278                Ok(PackedChineseBasedYearInfo(data.0, data.1, data.2))
279            }
280        }
281    }
282
283    #[cfg(feature = "datagen")]
284    impl Serialize for PackedChineseBasedYearInfo {
285        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
286        where
287            S: ser::Serializer,
288        {
289            if serializer.is_human_readable() {
290                SerdePackedChineseBasedYearInfo::from(*self).serialize(serializer)
291            } else {
292                (self.0, self.1, self.2).serialize(serializer)
293            }
294        }
295    }
296
297    #[cfg(feature = "datagen")]
298    impl From<PackedChineseBasedYearInfo> for SerdePackedChineseBasedYearInfo {
299        fn from(other: PackedChineseBasedYearInfo) -> Self {
300            let mut month_has_30_days = [false; 13];
301            for (i, month) in month_has_30_days.iter_mut().enumerate() {
302                *month = other.month_has_30_days(i as u8 + 1)
303            }
304            Self {
305                ny_offset: other.ny_offset(),
306                month_has_30_days,
307                leap_month_idx: other.leap_month_idx(),
308            }
309        }
310    }
311
312    impl From<SerdePackedChineseBasedYearInfo> for PackedChineseBasedYearInfo {
313        fn from(other: SerdePackedChineseBasedYearInfo) -> Self {
314            Self::new(
315                other.month_has_30_days,
316                other.leap_month_idx,
317                other.ny_offset,
318            )
319        }
320    }
321}