use crate::islamic::IslamicYearInfo;
use calendrical_calculations::islamic::IslamicBasedMarker;
use calendrical_calculations::rata_die::RataDie;
use core::fmt;
use icu_provider::prelude::*;
use zerovec::ule::{AsULE, ULE};
use zerovec::ZeroVec;
#[icu_provider::data_struct(
marker(
IslamicObservationalCacheV1Marker,
"calendar/islamicobservationalcache@1",
singleton
),
marker(
IslamicUmmAlQuraCacheV1Marker,
"calendar/islamicummalquracache@1",
singleton
)
)]
#[derive(Debug, PartialEq, Clone, Default)]
#[cfg_attr(
feature = "datagen",
derive(serde::Serialize, databake::Bake),
databake(path = icu_calendar::provider::islamic),
)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
pub struct IslamicCacheV1<'data> {
pub first_extended_year: i32,
#[cfg_attr(feature = "serde", serde(borrow))]
pub data: ZeroVec<'data, PackedIslamicYearInfo>,
}
impl<'data> IslamicCacheV1<'data> {
#[cfg(feature = "datagen")]
pub fn compute_for<IB: IslamicBasedMarker>(extended_years: core::ops::Range<i32>) -> Self {
let data = extended_years
.clone()
.map(|year| PackedIslamicYearInfo::compute::<IB>(year))
.collect();
IslamicCacheV1 {
first_extended_year: extended_years.start,
data,
}
}
pub(crate) fn get_for_extended_year(&self, extended_year: i32) -> Option<IslamicYearInfo> {
let delta = extended_year - self.first_extended_year;
let delta = usize::try_from(delta).ok()?;
if delta == 0 {
return None;
}
let (Some(this_packed), Some(prev_packed)) =
(self.data.get(delta), self.data.get(delta - 1))
else {
return None;
};
Some(IslamicYearInfo::new(prev_packed, this_packed, extended_year).0)
}
pub(crate) fn get_for_fixed<IB: IslamicBasedMarker>(
&self,
fixed: RataDie,
) -> Option<(IslamicYearInfo, i32)> {
let extended_year = IB::approximate_islamic_from_fixed(fixed);
let delta = extended_year - self.first_extended_year;
let delta = usize::try_from(delta).ok()?;
if delta <= 1 {
return None;
}
let this_packed = self.data.get(delta)?;
let prev_packed = self.data.get(delta + 1)?;
let this_ny = this_packed.ny::<IB>(extended_year);
if fixed < this_ny {
let prev2_packed = self.data.get(delta - 2)?;
return Some(IslamicYearInfo::new(
prev2_packed,
prev_packed,
extended_year - 1,
));
}
let next_packed = self.data.get(delta + 1)?;
let next_ny = next_packed.ny::<IB>(extended_year + 1);
if fixed >= next_ny {
Some(IslamicYearInfo::new(
this_packed,
next_packed,
extended_year + 1,
))
} else {
Some(IslamicYearInfo::new(
prev_packed,
this_packed,
extended_year,
))
}
}
}
#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, ULE)]
#[cfg_attr(
feature = "datagen",
derive(serde::Serialize, databake::Bake),
databake(path = icu_calendar::provider),
)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
#[repr(C, packed)]
pub struct PackedIslamicYearInfo(pub u8, pub u8);
impl fmt::Debug for PackedIslamicYearInfo {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
fmt.debug_struct("PackedIslamicYearInfo")
.field("ny_offset", &self.ny_offset())
.field("month_lengths", &self.month_lengths())
.finish()
}
}
impl PackedIslamicYearInfo {
pub(crate) fn new(month_lengths: [bool; 12], ny_offset: i8) -> Self {
debug_assert!(
-8 < ny_offset && ny_offset < 8,
"Year offset too big to store"
);
let mut all = 0u16; for (month, length_30) in month_lengths.iter().enumerate() {
#[allow(clippy::indexing_slicing)]
if *length_30 {
all |= 1 << month as u16;
}
}
if ny_offset < 0 {
all |= 1 << 12;
}
all |= u16::from(ny_offset.unsigned_abs()) << 13;
let le = all.to_le_bytes();
Self(le[0], le[1])
}
fn month_lengths(self) -> [u8; 12] {
let months: [u8; 12] = core::array::from_fn(|i| 1 + i as u8);
months.map(|x| if self.month_has_30_days(x) { 30 } else { 29 })
}
pub(crate) fn ny_offset(self) -> i8 {
let masked = (self.1 >> 5) as i8;
if (self.1 & 0b10000) != 0 {
-masked
} else {
masked
}
}
pub(crate) fn ny<IB: IslamicBasedMarker>(self, extended_year: i32) -> RataDie {
let mean_synodic_ny = IB::mean_synodic_ny(extended_year);
mean_synodic_ny + i64::from(self.ny_offset())
}
pub(crate) fn month_has_30_days(self, month: u8) -> bool {
let months = u16::from_le_bytes([self.0, self.1]);
months & (1 << (month - 1) as u16) != 0
}
pub(crate) fn days_in_month(self, month: u8) -> u8 {
if self.month_has_30_days(month) {
30
} else {
29
}
}
pub(crate) fn last_day_of_month(self, month: u8) -> u16 {
let months = u16::from_le_bytes([self.0, self.1]);
let mut prev_month_lengths = 29 * month as u16;
let long_month_bits = months & ((1 << month as u16) - 1);
prev_month_lengths += long_month_bits.count_ones().try_into().unwrap_or(0);
prev_month_lengths
}
pub(crate) fn days_in_year(self) -> u16 {
self.last_day_of_month(12)
}
pub(crate) fn compute_with_ny<IB: IslamicBasedMarker>(extended_year: i32, ny: RataDie) -> Self {
let month_lengths = IB::month_lengths_for_year(extended_year, ny);
let ny_offset = ny - IB::mean_synodic_ny(extended_year);
let ny_offset = if !(-7..=7).contains(&ny_offset) {
0
} else {
ny_offset as i8
};
Self::new(month_lengths, ny_offset)
}
#[cfg(feature = "datagen")]
pub(crate) fn compute<IB: IslamicBasedMarker>(extended_year: i32) -> Self {
let ny = IB::fixed_from_islamic(extended_year, 1, 1);
Self::compute_with_ny::<IB>(extended_year, ny)
}
}
impl AsULE for PackedIslamicYearInfo {
type ULE = Self;
fn to_unaligned(self) -> Self {
self
}
fn from_unaligned(other: Self) -> Self {
other
}
}
#[cfg(test)]
mod tests {
use super::*;
fn single_roundtrip(month_lengths: [bool; 12], ny_offset: i8) {
let packed = PackedIslamicYearInfo::new(month_lengths, ny_offset);
for i in 0..12 {
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:?}");
}
assert_eq!(packed.ny_offset(), ny_offset, "Month lengths must match for testcase {month_lengths:?} / {ny_offset}, with packed repr: {packed:?}");
}
const ALL_FALSE: [bool; 12] = [false; 12];
const ALL_TRUE: [bool; 12] = [true; 12];
const MIXED1: [bool; 12] = [
true, false, true, false, true, false, true, false, true, false, true, false,
];
const MIXED2: [bool; 12] = [
false, false, true, true, true, false, true, false, false, false, true, true,
];
#[test]
fn test_islamic_packed_roundtrip() {
single_roundtrip(ALL_FALSE, 0);
single_roundtrip(ALL_TRUE, 0);
single_roundtrip(MIXED1, 0);
single_roundtrip(MIXED2, 0);
single_roundtrip(MIXED1, -7);
single_roundtrip(MIXED2, 7);
single_roundtrip(MIXED2, 4);
single_roundtrip(MIXED2, 1);
single_roundtrip(MIXED2, -1);
single_roundtrip(MIXED2, -4);
}
}