1use 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#[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 pub first_extended_year: i32,
48 #[cfg_attr(feature = "serde", serde(borrow))]
50 pub data: ZeroVec<'data, PackedIslamicYearInfo>,
51}
52
53impl<'data> IslamicCacheV1<'data> {
54 #[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 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 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#[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; 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 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 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 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 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 pub(crate) fn last_day_of_month(self, month: u8) -> u16 {
232 let months = u16::from_le_bytes([self.0, self.1]);
233 let mut prev_month_lengths = 29 * month as u16;
235 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}