diff --git a/components/calendar/src/chinese.rs b/components/calendar/src/chinese.rs index a61c1a3e6c1..df14c248304 100644 --- a/components/calendar/src/chinese.rs +++ b/components/calendar/src/chinese.rs @@ -343,8 +343,9 @@ impl> DateTime { } } +type ChineseCB = calendrical_calculations::chinese_based::Chinese; impl ChineseBasedWithDataLoading for Chinese { - type CB = calendrical_calculations::chinese_based::Chinese; + type CB = ChineseCB; fn get_precomputed_data(&self) -> ChineseBasedPrecomputedData { Default::default() } @@ -367,7 +368,7 @@ impl Chinese { let cyclic = (number - 1).rem_euclid(60) as u8; let cyclic = NonZeroU8::new(cyclic + 1); // 1-indexed let rata_die_in_year = if let Some(info) = year_info_option { - info.new_year(year) + info.new_year::(year) } else { Inner::fixed_mid_year_from_year(number) }; @@ -633,7 +634,7 @@ mod test { let iso = Date::try_new_iso_date(year, 6, 1).unwrap(); let chinese_date = iso.to_calendar(Chinese); assert!(chinese_date.is_in_leap_year()); - let new_year = chinese_date.inner.0 .0.year_info.new_year(); + let new_year = chinese_date.inner.0.new_year(); assert_eq!( expected_month, calendrical_calculations::chinese_based::get_leap_month_from_new_year::< diff --git a/components/calendar/src/chinese_based.rs b/components/calendar/src/chinese_based.rs index e61edc6ee62..ea2e60ff681 100644 --- a/components/calendar/src/chinese_based.rs +++ b/components/calendar/src/chinese_based.rs @@ -61,6 +61,13 @@ pub(crate) struct ChineseBasedPrecomputedData { _cb: CB, // this is zero-sized } +/// The first day of the ISO year on which Chinese New Year may occur +/// +/// According to Reingold & Dershowitz, ch 19.6, Chinese New Year occurs on Jan 21 - Feb 21 inclusive. +/// +/// Chinese New Year in the year 30 AD is January 20 (30-01-20) +const FIRST_NY: u8 = 20; + fn compute_cache(extended_year: i32) -> ChineseBasedYearInfo { let mid_year = chinese_based::fixed_mid_year_from_year::(extended_year); let year_bounds = YearBounds::compute::(mid_year); @@ -69,18 +76,27 @@ fn compute_cache(extended_year: i32) -> ChineseBasedYearInfo { next_new_year, .. } = year_bounds; - let (last_day_of_month, leap_month) = + let (month_lengths, leap_month) = chinese_based::month_structure_for_year::(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); + + // +1 because `new_year - iso_ny` is zero-indexed, but `FIRST_NY` is 1-indexed + let ny_offset = new_year - iso_ny - i64::from(FIRST_NY) + 1; + let ny_offset = if let Some(ny_offset) = u8::try_from(ny_offset).ok() { + ny_offset + } else { + debug_assert!(false, "Expected small new years offset, got {ny_offset}"); + 0 + }; let days_in_prev_year = chinese_based::days_in_prev_year::(new_year); + + let packed_data = PackedChineseBasedYearInfo::new(month_lengths, leap_month, ny_offset); + ChineseBasedYearInfo { - new_year, days_in_prev_year, - // TODO(#3933): switch ChineseBasedYearInfo to packed info so we don't need to store as bloaty u16s - last_day_of_month: last_day_of_month - // +1 since new_year is in the current month - .map(|rd| (rd.to_i64_date() - new_year.to_i64_date() + 1) as u16), - leap_month, + packed_data, } } @@ -109,7 +125,7 @@ impl PrecomputedDataSource /// the month lengths are stored as 1 = 30, 0 = 29 for each month including the leap month. /// /// Should not -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub(crate) struct PackedChineseBasedYearInfo(pub(crate) u8, pub(crate) u8, pub(crate) u8); impl PackedChineseBasedYearInfo { @@ -145,7 +161,8 @@ impl PackedChineseBasedYearInfo { fn ny_rd(self, related_iso: i32) -> RataDie { let ny_offset = self.ny_offset(); let iso_ny = calendrical_calculations::iso::fixed_from_iso(related_iso, 1, 1); - iso_ny + 21 + i64::from(ny_offset) + // -1 because `iso_ny` is itself in the year, and `FIRST_NY` is 1-indexed + iso_ny + i64::from(FIRST_NY) + i64::from(ny_offset) - 1 } fn leap_month_idx(self) -> Option { @@ -156,6 +173,7 @@ impl PackedChineseBasedYearInfo { } // Whether a particular month has 30 days (month is 1-indexed) + #[cfg(test)] 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 @@ -167,14 +185,14 @@ impl PackedChineseBasedYearInfo { // month is 1-indexed, so `29 * month` includes the current month let mut prev_month_lengths = 29 * month as u16; // month is 1-indexed, so `1 << month` is a mask with all zeroes except - // for a 1 at the bit index at the next month. Subtracting it from 1 gets us + // for a 1 at the bit index at the next month. Subtracting 1 from it gets us // a bitmask for all months up to now - let long_month_bits = months & (1 - (1 << 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 } - fn year_length(self) -> u16 { + fn days_in_year(self) -> u16 { if self.leap_month_idx().is_some() { self.last_day_of_month(13) } else { @@ -187,23 +205,24 @@ impl PackedChineseBasedYearInfo { #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] // TODO(#3933): potentially make this smaller pub(crate) struct ChineseBasedYearInfo { - new_year: RataDie, days_in_prev_year: u16, - /// last_day_of_month[12] = last_day_of_month[11] in non-leap years - /// These days are 1-indexed: so the last day of month for a 30-day 一月 is 30 - /// The array itself is zero-indexed, be careful passing it self.0.month! - last_day_of_month: [u16; 13], - /// - leap_month: Option, + /// Contains: + /// - length of each month in the year + /// - whether or not there is a leap month, and which month it is + /// - the date of Chinese New Year in the related ISO year + packed_data: PackedChineseBasedYearInfo, } impl ChineseBasedYearInfo { - pub(crate) fn new_year(self, _extended_year: i32) -> RataDie { - self.new_year + /// Get the new year R.D. given the extended year that this yearinfo is for + pub(crate) fn new_year(self, extended_year: i32) -> RataDie { + self.packed_data.ny_rd(CB::iso_from_extended(extended_year)) } - fn next_new_year(self, _extended_year: i32) -> RataDie { - self.new_year + i64::from(self.last_day_of_month[12]) + /// Get the next new year R.D. given the extended year that this yearinfo is for + /// (i.e, this year, not next year) + fn next_new_year(self, extended_year: i32) -> RataDie { + self.new_year::(extended_year) + i64::from(self.packed_data.days_in_year()) } /// Get which month is the leap month. This produces the month *number* @@ -211,7 +230,7 @@ impl ChineseBasedYearInfo { /// a year with an M05L, this will return Some(5). Note that the regular month precedes /// the leap month. pub(crate) fn leap_month(self) -> Option { - self.leap_month + self.packed_data.leap_month_idx() } /// The last day of year in the previous month. @@ -223,19 +242,16 @@ impl ChineseBasedYearInfo { fn last_day_of_previous_month(self, month: u8) -> u16 { debug_assert!((1..=13).contains(&month), "Month out of bounds!"); // Get the last day of the previous month. - // Since `month` is 1-indexed, this needs to subtract *two* to get to the right index of the array - if month < 2 { + // Since `month` is 1-indexed, this needs to check if the month is 1 for the zero case + if month == 1 { 0 } else { - self.last_day_of_month - .get(usize::from(month - 2)) - .copied() - .unwrap_or(0) + self.packed_data.last_day_of_month(month - 1) } } fn days_in_year(self) -> u16 { - self.last_day_of_month[12] + self.packed_data.days_in_year() } fn days_in_prev_year(self) -> u16 { @@ -250,12 +266,7 @@ impl ChineseBasedYearInfo { /// is not in this year fn last_day_of_month(self, month: u8) -> u16 { debug_assert!((1..=13).contains(&month), "Month out of bounds!"); - // Get the last day of the previous month. - // Since `month` is 1-indexed, this needs to subtract one - self.last_day_of_month - .get(usize::from(month - 1)) - .copied() - .unwrap_or(0) + self.packed_data.last_day_of_month(month) } fn days_in_month(self, month: u8) -> u8 { @@ -279,11 +290,11 @@ impl ChineseBasedYearInfo { let data = cal.get_precomputed_data(); let year_info = data.load_or_compute_info(*getter_year); - if date < year_info.new_year(*getter_year) { + if date < year_info.new_year::(*getter_year) { *getter_year -= 1; data.load_or_compute_info(*getter_year) // FIXME (manishearth) try collapsing these new year calculations into one - } else if date >= year_info.next_new_year(*getter_year) { + } else if date >= year_info.next_new_year::(*getter_year) { *getter_year += 1; data.load_or_compute_info(*getter_year) } else { @@ -298,11 +309,11 @@ impl ChineseBasedDateInner { debug_assert!( - date < year_info.next_new_year(extended_year), + date < year_info.next_new_year::(extended_year), "Stored date {date:?} out of bounds!" ); // 1-indexed day of year - let day_of_year = u16::try_from(date - year_info.new_year(extended_year) + 1); + let day_of_year = u16::try_from(date - year_info.new_year::(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; @@ -317,7 +328,7 @@ impl RataDie { + self.0.year_info.new_year::(self.0.year) + } + /// Get a RataDie from a ChineseBasedDateInner /// /// This finds the RataDie of the new year of the year given, then finds the RataDie of the new moon /// (beginning of the month) of the month given, then adds the necessary number of days. pub(crate) fn fixed_from_chinese_based_date_inner(date: ChineseBasedDateInner) -> RataDie { - let first_day_of_year = date.0.year_info.new_year(date.0.year); + let first_day_of_year = date.new_year(); let day_of_year = date.day_of_year(); // 1 indexed first_day_of_year + i64::from(day_of_year) - 1 } @@ -401,7 +416,7 @@ impl u8 { - if year_info.leap_month.is_some() { + if year_info.leap_month().is_some() { 13 } else { 12 @@ -512,7 +527,7 @@ impl CalendarArithmetic for C { /// Returns the number of months in a given year, which is 13 in a leap year, and 12 in a common year. fn months_for_every_year(_year: i32, year_info: ChineseBasedYearInfo) -> u8 { - if year_info.leap_month.is_some() { + if year_info.leap_month().is_some() { 13 } else { 12 @@ -521,7 +536,7 @@ impl CalendarArithmetic for C { /// Returns true if the given year is a leap year, and false if not. fn is_leap_year(_year: i32, year_info: ChineseBasedYearInfo) -> bool { - year_info.leap_month.is_some() + year_info.leap_month().is_some() } /// Returns the (month, day) of the last day in a Chinese year (the day before Chinese New Year). @@ -529,7 +544,7 @@ impl CalendarArithmetic for C { /// determined by finding the day immediately before the next new year and calculating the number /// of days since the last new moon (beginning of the last month in the year). fn last_month_day_in_year(_year: i32, year_info: ChineseBasedYearInfo) -> (u8, u8) { - if year_info.leap_month.is_some() { + if year_info.leap_month().is_some() { (13, year_info.days_in_month(13)) } else { (12, year_info.days_in_month(12)) @@ -546,7 +561,7 @@ pub(crate) fn chinese_based_ordinal_lunar_month_from_code( code: MonthCode, year_info: ChineseBasedYearInfo, ) -> Option { - let leap_month = if let Some(leap) = year_info.leap_month { + let leap_month = if let Some(leap) = year_info.leap_month() { leap.get() } else { // 14 is a sentinel value, greater than all other months, for the purpose of computation only; diff --git a/components/calendar/src/dangi.rs b/components/calendar/src/dangi.rs index 258148a5cd2..2eeee247200 100644 --- a/components/calendar/src/dangi.rs +++ b/components/calendar/src/dangi.rs @@ -318,8 +318,9 @@ impl> DateTime { } } +type DangiCB = calendrical_calculations::chinese_based::Dangi; impl ChineseBasedWithDataLoading for Dangi { - type CB = calendrical_calculations::chinese_based::Dangi; + type CB = DangiCB; fn get_precomputed_data(&self) -> ChineseBasedPrecomputedData { Default::default() } @@ -338,7 +339,7 @@ impl Dangi { let cyclic = (number as i64 - 1 + 364).rem_euclid(60) as u8; let cyclic = NonZeroU8::new(cyclic + 1); // 1-indexed let rata_die_in_year = if let Some(info) = year_info_option { - info.new_year(year) + info.new_year::(year) } else { Inner::fixed_mid_year_from_year(number) }; diff --git a/utils/calendrical_calculations/src/chinese_based.rs b/utils/calendrical_calculations/src/chinese_based.rs index e684a9133a3..bf377dcb2ad 100644 --- a/utils/calendrical_calculations/src/chinese_based.rs +++ b/utils/calendrical_calculations/src/chinese_based.rs @@ -25,10 +25,23 @@ pub trait ChineseBased { /// reflect traditional methods of year-tracking or eras, since Chinese-based calendars /// may not track years ordinally in the same way many western calendars do. const EPOCH: RataDie; + + /// The ISO year that corresponds to year 1 + const EPOCH_ISO: i32; + /// Given an ISO year, return the extended year + + fn extended_from_iso(iso_year: i32) -> i32 { + iso_year - Self::EPOCH_ISO + 1 + } + /// Given an extended year, return the ISO year + fn iso_from_extended(extended_year: i32) -> i32 { + extended_year - 1 + Self::EPOCH_ISO + } } // The equivalent first day in the Chinese calendar (based on inception of the calendar) const CHINESE_EPOCH: RataDie = RataDie::new(-963099); // Feb. 15, 2637 BCE (-2636) +const CHINESE_EPOCH_ISO: i32 = -2636; /// The Chinese calendar relies on knowing the current day at the moment of a new moon; /// however, this can vary depending on location. As such, new moon calculations are based @@ -47,6 +60,7 @@ const CHINESE_LOCATION_POST_1929: Location = // The first day in the Korean Dangi calendar (based on the founding of Gojoseon) const KOREAN_EPOCH: RataDie = RataDie::new(-852065); // Lunar new year 2333 BCE (-2332 ISO) +const KOREAN_EPOCH_ISO: i32 = -2332; // Lunar new year 2333 BCE (-2332 ISO) /// The Korean Dangi calendar relies on knowing the current day at the moment of a new moon; /// however, this can vary depending on location. As such, new moon calculations are based on @@ -122,6 +136,7 @@ impl ChineseBased for Chinese { } const EPOCH: RataDie = CHINESE_EPOCH; + const EPOCH_ISO: i32 = CHINESE_EPOCH_ISO; } impl ChineseBased for Dangi { @@ -140,6 +155,7 @@ impl ChineseBased for Dangi { } const EPOCH: RataDie = KOREAN_EPOCH; + const EPOCH_ISO: i32 = KOREAN_EPOCH_ISO; } /// Marks the bounds of a lunar year @@ -481,15 +497,14 @@ pub fn days_in_prev_year(new_year: RataDie) -> u16 { u16::try_from(new_year - prev_new_year).unwrap_or(360) } -/// Returns the last day of every month in the year, as well as a leap month index (1-indexed) if any -/// In the case of no leap months, month 12 and 13 will have identical "last day" -/// values +/// Returns the length of each month in the year, as well as a leap month index (1-indexed) if any. +/// Month lengths are stored as true for 30-day, false for 29-day. +/// In the case of no leap months, month 13 will have value false. pub fn month_structure_for_year( new_year: RataDie, next_new_year: RataDie, -) -> ([RataDie; 13], Option) { - let mut ret = [RataDie::new(0); 13]; - ret[12] = next_new_year - 1; // last day of last month is the day before the next new year +) -> ([bool; 13], Option) { + let mut ret = [false; 13]; let mut current_month_start = new_year; let mut current_month_major_solar_term = major_solar_term_from_fixed::(new_year); @@ -498,26 +513,22 @@ pub fn month_structure_for_year( let next_month_start = new_moon_on_or_after::((current_month_start + 28).as_moment()); let next_month_major_solar_term = major_solar_term_from_fixed::(next_month_start); - #[allow(clippy::indexing_slicing)] // array is of length 13, we iterate till i=11 - { - ret[usize::from(i)] = next_month_start - 1; - } - if next_month_major_solar_term == current_month_major_solar_term { leap_month_index = NonZeroU8::new(i + 1); } let diff = next_month_start - current_month_start; debug_assert!(diff == 29 || diff == 30); + #[allow(clippy::indexing_slicing)] // array is of length 13, we iterate till i=11 + if diff == 30 { + ret[usize::from(i)] = true; + } current_month_start = next_month_start; current_month_major_solar_term = next_month_major_solar_term; } - let first_month_diff = ret[0] - new_year + 1; // +1 since new_year is in the current month - debug_assert!(first_month_diff == 29 || first_month_diff == 30); - - if ret[11] == ret[12] { + if current_month_start == next_new_year { // not all months without solar terms are leap months; they are only leap months if // the year can admit them // @@ -528,11 +539,17 @@ pub fn month_structure_for_year( // // As such, if a month without a solar term is found in a non-leap year, we just ingnore it. leap_month_index = None; + } else { + let diff = next_new_year - current_month_start; + debug_assert!(diff == 29 || diff == 30); + if diff == 30 { + ret[12] = true; + } } - if ret[11] != ret[12] && leap_month_index.is_none() { + if current_month_start != next_new_year && leap_month_index.is_none() { leap_month_index = NonZeroU8::new(13); // The last month is a leap month debug_assert!( - major_solar_term_from_fixed::(ret[12]) == current_month_major_solar_term, + major_solar_term_from_fixed::(current_month_start) == current_month_major_solar_term, "A leap month is required here, but it had a major solar term!" ); } @@ -588,36 +605,27 @@ mod test { for year in 1900..2050 { let fixed = crate::iso::fixed_from_iso(year, 1, 1); let chinese_year = chinese_based_date_from_fixed::(fixed); - let (month_ends, leap) = month_structure_for_year::( + let (month_lengths, leap) = month_structure_for_year::( chinese_year.year_bounds.new_year, chinese_year.year_bounds.next_new_year, ); - let chinese_ny = chinese_year.year_bounds.new_year; - assert_eq!( - month_ends[12] - chinese_ny + 1, - i64::from(days_in_provided_year::(chinese_year.year)), - "Year length must be the same" - ); - let mut month_start = chinese_ny; - for (i, month_end) in month_ends.into_iter().enumerate() { - let next_month_start = month_end + 1; - let month_len = next_month_start - month_start; - if month_len == 0 && i == 12 { + + for (i, month_is_30) in month_lengths.into_iter().enumerate() { + if leap.is_none() && i == 12 { // month_days has no defined behavior for month 13 on non-leap-years continue; } + let month_len = 29 + i32::from(month_is_30); let month_days = month_days::(chinese_year.year, i as u8 + 1); assert_eq!( month_len, - i64::from(month_days), + i32::from(month_days), "Month length for month {} must be the same", i + 1 ); - - month_start = next_month_start; } println!( - "{year} (chinese {}): {month_ends:?} {leap:?}", + "{year} (chinese {}): {month_lengths:?} {leap:?}", chinese_year.year ); }