use crate::{
calendar_arithmetic::{ArithmeticDate, CalendarArithmetic, PrecomputedDataSource},
provider::chinese_based::{ChineseBasedCacheV1, PackedChineseBasedYearInfo},
types::{FormattableMonth, MonthCode},
Calendar, CalendarError, Iso,
};
use calendrical_calculations::chinese_based::{self, ChineseBased, YearBounds};
use calendrical_calculations::rata_die::RataDie;
use core::marker::PhantomData;
use core::num::NonZeroU8;
use tinystr::tinystr;
pub(crate) trait ChineseBasedWithDataLoading: Calendar {
type CB: ChineseBased;
fn get_precomputed_data(&self) -> ChineseBasedPrecomputedData<'_, Self::CB>;
}
#[derive(Debug, Eq, PartialEq, PartialOrd, Ord)]
pub(crate) struct ChineseBasedDateInner<C: CalendarArithmetic>(pub(crate) ArithmeticDate<C>);
impl<C: CalendarArithmetic> Copy for ChineseBasedDateInner<C> {}
impl<C: CalendarArithmetic> Clone for ChineseBasedDateInner<C> {
fn clone(&self) -> Self {
*self
}
}
#[derive(Default)]
pub(crate) struct ChineseBasedPrecomputedData<'a, CB: ChineseBased> {
data: Option<&'a ChineseBasedCacheV1<'a>>,
_cb: PhantomData<CB>,
}
fn compute_cache<CB: ChineseBased>(extended_year: i32) -> ChineseBasedYearInfo {
let mid_year = chinese_based::fixed_mid_year_from_year::<CB>(extended_year);
let year_bounds = YearBounds::compute::<CB>(mid_year);
compute_cache_with_yb::<CB>(extended_year, year_bounds)
}
fn compute_cache_with_yb<CB: ChineseBased>(
extended_year: i32,
year_bounds: YearBounds,
) -> ChineseBasedYearInfo {
let YearBounds { new_year, .. } = year_bounds;
let days_in_prev_year = chinese_based::days_in_prev_year::<CB>(new_year);
let packed_data = compute_packed_with_yb::<CB>(extended_year, year_bounds);
ChineseBasedYearInfo {
days_in_prev_year,
packed_data,
}
}
fn compute_packed_with_yb<CB: ChineseBased>(
extended_year: i32,
year_bounds: YearBounds,
) -> PackedChineseBasedYearInfo {
let YearBounds {
new_year,
next_new_year,
..
} = year_bounds;
let (month_lengths, leap_month) =
chinese_based::month_structure_for_year::<CB>(new_year, next_new_year);
let related_iso = CB::iso_from_extended(extended_year);
let iso_ny = calendrical_calculations::iso::fixed_from_iso(related_iso, 1, 1);
let ny_offset = new_year - iso_ny - i64::from(PackedChineseBasedYearInfo::FIRST_NY) + 1;
let ny_offset = if let Ok(ny_offset) = u8::try_from(ny_offset) {
ny_offset
} else {
debug_assert!(
false,
"Expected small new years offset, got {ny_offset} in ISO year {related_iso}"
);
0
};
PackedChineseBasedYearInfo::new(month_lengths, leap_month, ny_offset)
}
#[cfg(feature = "datagen")]
pub(crate) fn compute_many_packed<CB: ChineseBased>(
extended_years: core::ops::Range<i32>,
) -> alloc::vec::Vec<PackedChineseBasedYearInfo> {
extended_years
.map(|extended_year| {
let mid_year = chinese_based::fixed_mid_year_from_year::<CB>(extended_year);
let year_bounds = YearBounds::compute::<CB>(mid_year);
compute_packed_with_yb::<CB>(extended_year, year_bounds)
})
.collect()
}
impl<'b, CB: ChineseBased> PrecomputedDataSource<ChineseBasedYearInfo>
for ChineseBasedPrecomputedData<'b, CB>
{
fn load_or_compute_info(&self, extended_year: i32) -> ChineseBasedYearInfo {
self.data
.and_then(|d| d.get_for_extended_year(extended_year))
.unwrap_or_else(|| compute_cache::<CB>(extended_year))
}
}
impl<'b, CB: ChineseBased> ChineseBasedPrecomputedData<'b, CB> {
pub(crate) fn new(data: Option<&'b ChineseBasedCacheV1<'b>>) -> Self {
Self {
data,
_cb: PhantomData,
}
}
fn load_or_compute_info_for_iso(
&self,
fixed: RataDie,
iso: ArithmeticDate<Iso>,
) -> (ChineseBasedYearInfo, i32) {
let cached = self.data.and_then(|d| d.get_for_iso::<CB>(iso));
if let Some(cached) = cached {
return cached;
};
let extended_year = CB::extended_from_iso(iso.year);
let mid_year = chinese_based::fixed_mid_year_from_year::<CB>(extended_year);
let year_bounds = YearBounds::compute::<CB>(mid_year);
let YearBounds { new_year, .. } = year_bounds;
if fixed >= new_year {
(
compute_cache_with_yb::<CB>(extended_year, year_bounds),
extended_year,
)
} else {
let extended_year = extended_year - 1;
(compute_cache::<CB>(extended_year), extended_year)
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub(crate) struct ChineseBasedYearInfo {
days_in_prev_year: u16,
packed_data: PackedChineseBasedYearInfo,
}
impl ChineseBasedYearInfo {
pub(crate) fn new(days_in_prev_year: u16, packed_data: PackedChineseBasedYearInfo) -> Self {
Self {
days_in_prev_year,
packed_data,
}
}
pub(crate) fn new_year<CB: ChineseBased>(self, extended_year: i32) -> RataDie {
self.packed_data.ny_rd(CB::iso_from_extended(extended_year))
}
fn next_new_year<CB: ChineseBased>(self, extended_year: i32) -> RataDie {
self.new_year::<CB>(extended_year) + i64::from(self.packed_data.days_in_year())
}
pub(crate) fn leap_month(self) -> Option<NonZeroU8> {
self.packed_data.leap_month_idx()
}
fn last_day_of_previous_month(self, month: u8) -> u16 {
debug_assert!((1..=13).contains(&month), "Month out of bounds!");
if month == 1 {
0
} else {
self.packed_data.last_day_of_month(month - 1)
}
}
fn days_in_year(self) -> u16 {
self.packed_data.days_in_year()
}
fn days_in_prev_year(self) -> u16 {
self.days_in_prev_year
}
fn last_day_of_month(self, month: u8) -> u16 {
debug_assert!((1..=13).contains(&month), "Month out of bounds!");
self.packed_data.last_day_of_month(month)
}
fn days_in_month(self, month: u8) -> u8 {
let ret =
u8::try_from(self.last_day_of_month(month) - self.last_day_of_previous_month(month));
debug_assert!(ret.is_ok(), "Month too big!");
ret.unwrap_or(30)
}
}
impl<C: ChineseBasedWithDataLoading + CalendarArithmetic<YearInfo = ChineseBasedYearInfo>>
ChineseBasedDateInner<C>
{
fn chinese_based_date_from_info(
date: RataDie,
year_info: ChineseBasedYearInfo,
extended_year: i32,
) -> ChineseBasedDateInner<C> {
debug_assert!(
date < year_info.next_new_year::<C::CB>(extended_year),
"Stored date {date:?} out of bounds!"
);
let day_of_year = u16::try_from(date - year_info.new_year::<C::CB>(extended_year) + 1);
debug_assert!(day_of_year.is_ok(), "Somehow got a very large year in data");
let day_of_year = day_of_year.unwrap_or(1);
let mut month = 1;
for iter_month in 1..=13 {
month = iter_month;
if year_info.last_day_of_month(iter_month) >= day_of_year {
break;
}
}
debug_assert!((1..=13).contains(&month), "Month out of bounds!");
debug_assert!(
month < 13 || year_info.leap_month().is_some(),
"Cannot have 13 months in a non-leap year!"
);
let day_before_month_start = year_info.last_day_of_previous_month(month);
let day_of_month = day_of_year - day_before_month_start;
let day_of_month = u8::try_from(day_of_month);
debug_assert!(day_of_month.is_ok(), "Month too big!");
let day_of_month = day_of_month.unwrap_or(1);
ChineseBasedDateInner(ArithmeticDate::new_unchecked_with_info(
extended_year,
month,
day_of_month,
year_info,
))
}
pub(crate) fn chinese_based_date_from_fixed(
cal: &C,
fixed: RataDie,
iso: ArithmeticDate<Iso>,
) -> ChineseBasedDateInner<C> {
let data = cal.get_precomputed_data();
let (year_info, extended_year) = data.load_or_compute_info_for_iso(fixed, iso);
Self::chinese_based_date_from_info(fixed, year_info, extended_year)
}
pub(crate) fn new_year(self) -> RataDie {
self.0.year_info.new_year::<C::CB>(self.0.year)
}
pub(crate) fn fixed_from_chinese_based_date_inner(date: ChineseBasedDateInner<C>) -> RataDie {
let first_day_of_year = date.new_year();
let day_of_year = date.day_of_year(); first_day_of_year + i64::from(day_of_year) - 1
}
pub(crate) fn new_from_ordinals(
year: i32,
month: u8,
day: u8,
year_info: ChineseBasedYearInfo,
) -> Result<ArithmeticDate<C>, CalendarError> {
let max_month = Self::months_in_year_with_info(year_info);
if !(1..=max_month).contains(&month) {
return Err(CalendarError::Overflow {
field: "month",
max: max_month as usize,
});
}
let max_day = year_info.days_in_month(month);
if day > max_day {
return Err(CalendarError::Overflow {
field: "day",
max: max_day as usize,
});
}
Ok(ArithmeticDate::<C>::new_unchecked_with_info(
year, month, day, year_info,
))
}
pub(crate) fn months_in_year_inner(&self) -> u8 {
Self::months_in_year_with_info(self.0.year_info)
}
fn months_in_year_with_info(year_info: ChineseBasedYearInfo) -> u8 {
if year_info.leap_month().is_some() {
13
} else {
12
}
}
pub(crate) fn days_in_month_inner(&self) -> u8 {
self.0.year_info.days_in_month(self.0.month)
}
pub(crate) fn fixed_mid_year_from_year(year: i32) -> RataDie {
chinese_based::fixed_mid_year_from_year::<C::CB>(year)
}
pub(crate) fn days_in_year_inner(&self) -> u16 {
self.0.year_info.days_in_year()
}
pub(crate) fn days_in_prev_year(&self) -> u16 {
self.0.year_info.days_in_prev_year()
}
pub(crate) fn day_of_year(&self) -> u16 {
self.0.year_info.last_day_of_previous_month(self.0.month) + u16::from(self.0.day)
}
pub(crate) fn month(&self) -> FormattableMonth {
let ordinal = self.0.month;
let leap_month_option = self.0.year_info.leap_month();
let leap_month = if let Some(leap) = leap_month_option {
leap.get()
} else {
14
};
let code_inner = if leap_month == ordinal {
debug_assert!((2..=13).contains(&ordinal));
match ordinal {
2 => tinystr!(4, "M01L"),
3 => tinystr!(4, "M02L"),
4 => tinystr!(4, "M03L"),
5 => tinystr!(4, "M04L"),
6 => tinystr!(4, "M05L"),
7 => tinystr!(4, "M06L"),
8 => tinystr!(4, "M07L"),
9 => tinystr!(4, "M08L"),
10 => tinystr!(4, "M09L"),
11 => tinystr!(4, "M10L"),
12 => tinystr!(4, "M11L"),
13 => tinystr!(4, "M12L"),
_ => tinystr!(4, "und"),
}
} else {
let mut adjusted_ordinal = ordinal;
if ordinal > leap_month {
debug_assert!((2..=13).contains(&ordinal));
adjusted_ordinal -= 1;
}
debug_assert!((1..=12).contains(&adjusted_ordinal));
match adjusted_ordinal {
1 => tinystr!(4, "M01"),
2 => tinystr!(4, "M02"),
3 => tinystr!(4, "M03"),
4 => tinystr!(4, "M04"),
5 => tinystr!(4, "M05"),
6 => tinystr!(4, "M06"),
7 => tinystr!(4, "M07"),
8 => tinystr!(4, "M08"),
9 => tinystr!(4, "M09"),
10 => tinystr!(4, "M10"),
11 => tinystr!(4, "M11"),
12 => tinystr!(4, "M12"),
_ => tinystr!(4, "und"),
}
};
let code = MonthCode(code_inner);
FormattableMonth {
ordinal: ordinal as u32,
code,
}
}
}
impl<C: ChineseBasedWithDataLoading> CalendarArithmetic for C {
type YearInfo = ChineseBasedYearInfo;
fn month_days(_year: i32, month: u8, year_info: ChineseBasedYearInfo) -> u8 {
year_info.days_in_month(month)
}
fn months_for_every_year(_year: i32, year_info: ChineseBasedYearInfo) -> u8 {
if year_info.leap_month().is_some() {
13
} else {
12
}
}
fn is_leap_year(_year: i32, year_info: ChineseBasedYearInfo) -> bool {
year_info.leap_month().is_some()
}
fn last_month_day_in_year(_year: i32, year_info: ChineseBasedYearInfo) -> (u8, u8) {
if year_info.leap_month().is_some() {
(13, year_info.days_in_month(13))
} else {
(12, year_info.days_in_month(12))
}
}
fn days_in_provided_year(_year: i32, year_info: ChineseBasedYearInfo) -> u16 {
year_info.last_day_of_month(13)
}
}
pub(crate) fn chinese_based_ordinal_lunar_month_from_code(
code: MonthCode,
year_info: ChineseBasedYearInfo,
) -> Option<u8> {
let leap_month = if let Some(leap) = year_info.leap_month() {
leap.get()
} else {
14
};
if code.0.len() < 3 {
return None;
}
let bytes = code.0.all_bytes();
if bytes[0] != b'M' {
return None;
}
if code.0.len() == 4 && bytes[3] != b'L' {
return None;
}
let mut unadjusted = 0;
if bytes[1] == b'0' {
if bytes[2] >= b'1' && bytes[2] <= b'9' {
unadjusted = bytes[2] - b'0';
}
} else if bytes[1] == b'1' && bytes[2] >= b'0' && bytes[2] <= b'2' {
unadjusted = 10 + bytes[2] - b'0';
}
if bytes[3] == b'L' {
if unadjusted + 1 != leap_month {
return None;
} else {
return Some(unadjusted + 1);
}
}
if unadjusted != 0 {
if unadjusted + 1 > leap_month {
return Some(unadjusted + 1);
} else {
return Some(unadjusted);
}
}
None
}
#[cfg(test)]
mod test {
use super::*;
fn packed_roundtrip_single(
mut month_lengths: [bool; 13],
leap_month_idx: Option<NonZeroU8>,
ny_offset: u8,
) {
if leap_month_idx.is_none() {
month_lengths[12] = false;
}
let packed = PackedChineseBasedYearInfo::new(month_lengths, leap_month_idx, ny_offset);
assert_eq!(
ny_offset,
packed.ny_offset(),
"Roundtrip with {month_lengths:?}, {leap_month_idx:?}, {ny_offset}"
);
assert_eq!(
leap_month_idx,
packed.leap_month_idx(),
"Roundtrip with {month_lengths:?}, {leap_month_idx:?}, {ny_offset}"
);
let mut month_lengths_roundtrip = [false; 13];
for (i, len) in month_lengths_roundtrip.iter_mut().enumerate() {
*len = packed.month_has_30_days(i as u8 + 1);
}
assert_eq!(
month_lengths, month_lengths_roundtrip,
"Roundtrip with {month_lengths:?}, {leap_month_idx:?}, {ny_offset}"
);
}
#[test]
fn test_roundtrip_packed() {
const SHORT: [bool; 13] = [false; 13];
const LONG: [bool; 13] = [true; 13];
const ALTERNATING1: [bool; 13] = [
false, true, false, true, false, true, false, true, false, true, false, true, false,
];
const ALTERNATING2: [bool; 13] = [
true, false, true, false, true, false, true, false, true, false, true, false, true,
];
const RANDOM1: [bool; 13] = [
true, true, false, false, true, true, false, true, true, true, true, false, true,
];
const RANDOM2: [bool; 13] = [
false, true, true, true, true, false, true, true, true, false, false, true, false,
];
packed_roundtrip_single(SHORT, None, 5);
packed_roundtrip_single(SHORT, None, 10);
packed_roundtrip_single(SHORT, NonZeroU8::new(11), 15);
packed_roundtrip_single(LONG, NonZeroU8::new(12), 15);
packed_roundtrip_single(ALTERNATING1, None, 2);
packed_roundtrip_single(ALTERNATING1, NonZeroU8::new(3), 5);
packed_roundtrip_single(ALTERNATING2, None, 9);
packed_roundtrip_single(ALTERNATING2, NonZeroU8::new(7), 26);
packed_roundtrip_single(RANDOM1, None, 29);
packed_roundtrip_single(RANDOM1, NonZeroU8::new(12), 29);
packed_roundtrip_single(RANDOM1, NonZeroU8::new(2), 21);
packed_roundtrip_single(RANDOM2, None, 25);
packed_roundtrip_single(RANDOM2, NonZeroU8::new(2), 19);
packed_roundtrip_single(RANDOM2, NonZeroU8::new(5), 2);
packed_roundtrip_single(RANDOM2, NonZeroU8::new(12), 5);
}
}