From 94e12ea8352a143cf1e42139023aa56d3aa46a81 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Tue, 2 Jan 2024 15:00:58 -0800 Subject: [PATCH 01/24] Improve Date's debug impl --- components/calendar/src/date.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/components/calendar/src/date.rs b/components/calendar/src/date.rs index 9f22f222efb..c857ac2201f 100644 --- a/components/calendar/src/date.rs +++ b/components/calendar/src/date.rs @@ -389,8 +389,11 @@ impl fmt::Debug for Date { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { write!( f, - "Date({:?}, for calendar {})", - self.inner, + "Date({}-{}-{}, {} era, for calendar {})", + self.year().number, + self.month().ordinal, + self.day_of_month().0, + self.year().era.0, self.calendar.as_calendar().debug_name() ) } From 93cb820d6df01d8bf724efb88298cd400359912b Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Tue, 2 Jan 2024 16:13:44 -0800 Subject: [PATCH 02/24] Add conversiomn bench for hebrew --- components/calendar/benches/convert.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/components/calendar/benches/convert.rs b/components/calendar/benches/convert.rs index 29d233866d7..38441f8fcc8 100644 --- a/components/calendar/benches/convert.rs +++ b/components/calendar/benches/convert.rs @@ -75,6 +75,15 @@ fn convert_benches(c: &mut Criterion) { icu::calendar::gregorian::Gregorian, ); + + #[cfg(feature = "bench")] + bench_calendar( + &mut group, + "calendar/hebrew", + icu::calendar::hebrew::Hebrew::new_always_calculating(), + ); + + #[cfg(feature = "bench")] bench_calendar( &mut group, From ae804099fcc3302da644d5e6897a2ace2b9a65b8 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Tue, 2 Jan 2024 10:25:56 -0800 Subject: [PATCH 03/24] Functions for getting YearInfo from Keviyah, computing the year that contains an R.D, and the year length --- .../src/hebrew_keviyah.rs | 109 +++++++++++++++++- 1 file changed, 105 insertions(+), 4 deletions(-) diff --git a/utils/calendrical_calculations/src/hebrew_keviyah.rs b/utils/calendrical_calculations/src/hebrew_keviyah.rs index ffa64a0605c..e71db8fb486 100644 --- a/utils/calendrical_calculations/src/hebrew_keviyah.rs +++ b/utils/calendrical_calculations/src/hebrew_keviyah.rs @@ -7,7 +7,9 @@ // the Apache License, Version 2.0 which can be found at the calendrical_calculations // package root or at http://www.apache.org/licenses/LICENSE-2.0. +use crate::helpers::i64_to_i32; use crate::rata_die::RataDie; +use core::cmp::Ordering; // The algorithms in this file are rather well-published in multiple places, // though the resource that was primarily used was @@ -98,6 +100,9 @@ const MOLAD_BEHERAD_OFFSET: i32 = ḥal!(2 - 5 - 204); /// From Adjler Appendix A const HEBREW_LUNATION_TIME: i32 = ḥal!(0-indexed 29-12-793); +/// From Reingold (ch 8.2, in implementation for fixed-from-hebrew) +const HEBREW_APPROX_YEAR_LENGTH: f64 = 35975351.0 / 98496.0; + /// The number of ḥalakim in a week /// /// (This is 181440) @@ -113,6 +118,7 @@ const HEBREW_CALENDAR_EPOCH: RataDie = crate::julian::fixed_from_julian_book_ver /// /// - The number of weeks since the week of Beharad (Oct 6, 3761 BCE Julian) /// - The number of ḥalakim since the start of the week (Hebrew Sunday, starting on Saturday at 18:00) +#[inline] fn molad_details(h_year: i32) -> (i64, i32) { let months_preceding = months_preceding_molad(h_year); @@ -162,6 +168,51 @@ impl YearInfo { } } + /// Returns the YearInfo and h_year for the year containing `date` + pub fn year_containing_rd(date: RataDie) -> (Self, i32) { + let days_since_epoch = (date - HEBREW_CALENDAR_EPOCH) as f64; + let maybe_approx = + i64_to_i32(1 + days_since_epoch.div_euclid(HEBREW_APPROX_YEAR_LENGTH) as i64); + let approx = maybe_approx.unwrap_or_else(|e| e.saturate()); + + let yi = Self::compute_for(approx); + + // compute if yi ⩼ rd + let cmp = yi.compare(date); + + let (yi, h_year) = match cmp { + // The approx year is a year greater. Go one year down + Ordering::Greater => (Self::compute_for(approx - 1), approx - 1), + // Bullseye + Ordering::Equal => (yi, approx), + // The approx year is a year lower. Go one year up. + Ordering::Less => (Self::compute_for(approx + 1), approx + 1), + }; + + debug_assert!(yi.compare(date).is_eq() || maybe_approx.is_err(), // The data will be incorrect if we saturate, and that's expected + "Date {date:?} calculated approximately to Hebrew Year {approx} (comparison: {cmp:?}), \ + should be contained in adjacent year {h_year} but that year is still {:?} it", yi.compare(date)); + + (yi, h_year) + } + + /// Compare this year against a date. Returns Ordering::Greater + /// when this year is after the given date + /// + /// i.e. this is computing self ⩼ rd + fn compare(self, rd: RataDie) -> Ordering { + let ny = self.new_year(); + let len = self.keviyah.year_length(); + + if rd < ny { + Ordering::Greater + } else if rd >= ny + len.into() { + Ordering::Less + } else { + Ordering::Equal + } + } + /// Compute the date of New Year's Day pub fn new_year(self) -> RataDie { // Beharad started on Monday @@ -233,11 +284,11 @@ pub enum Keviyah { #[allow(clippy::exhaustive_enums)] // This is intrinsic to the calendar pub enum YearType { /// חסרה: both Ḥesvan and Kislev have 29 days - Deficient, + Deficient = -1, /// כסדרה: Ḥesvan has 29, Kislev has 30 - Regular, + Regular = 0, /// שלמה: both Ḥesvan and Kislev have 30 days - Complete, + Complete = 1, } /// The day of the new year. Only these four days are permitted. @@ -309,6 +360,29 @@ impl Keviyah { self >= Self::בחה } + /// Given the hebrew year for this Keviyah, calculate the YearInfo + pub fn year_info(self, h_year: i32) -> YearInfo { + let (mut weeks_since_beharad, ḥalakim) = molad_details(h_year); + + // The last six hours of Hebrew Saturday (i.e. after noon on Regular Saturday) + // get unconditionally postponed to Monday according to the Four Gates table. This + // puts us in a new week! + if ḥalakim > ḥal!(7 - 18 - 0) { + weeks_since_beharad += 1; + } + + YearInfo { + keviyah: self, + weeks_since_beharad, + } + } + + /// How many days are in this year + pub fn year_length(self) -> u16 { + let base_year_length = if self.is_leap() { 384 } else { 354 }; + + (base_year_length + self.year_type() as i16) as u16 + } /// Construct this from an integer between 0 and 13 /// /// Potentially useful for bitpacking @@ -506,13 +580,15 @@ mod test { #[test] fn test_book_parity() { + let mut last_year = None; for h_year in (1..100).chain(5600..5900).chain(10000..10100) { let book_date = BookHebrew::from_civil_date(h_year, 1, 1); let book_ny = BookHebrew::fixed_from_book_hebrew(book_date); let kv_yearinfo = YearInfo::compute_for(h_year); + let kv_ny = kv_yearinfo.new_year(); assert_eq!( book_ny, - kv_yearinfo.new_year(), + kv_ny, "Book and Keviyah-based years should match for Hebrew Year {h_year}. Got YearInfo {kv_yearinfo:?}" ); let book_is_leap = BookHebrew::is_hebrew_leap_year(h_year); @@ -534,6 +610,31 @@ mod test { kv_yearinfo.keviyah.year_type(), "Book and Keviyah-based years should match for Hebrew Year {h_year}. Got YearInfo {kv_yearinfo:?}" ); + + let kv_recomputed_yearinfo = kv_yearinfo.keviyah.year_info(h_year); + assert_eq!( + kv_recomputed_yearinfo, + kv_yearinfo, + "Recomputed YearInfo should match for Hebrew Year {h_year}. Got YearInfo {kv_yearinfo:?}" + ); + + let year_len = kv_yearinfo.keviyah.year_length(); + + for offset in [0, 1, 100, year_len - 100, year_len - 2, year_len - 1] { + let offset_date = kv_ny + offset.into(); + let (offset_yearinfo, offset_h_year) = YearInfo::year_containing_rd(offset_date); + + assert_eq!(offset_h_year, h_year, "Backcomputed h_year should be same for date {offset_date:?} in Hebrew Year {h_year} (offset from ny {offset})"); + assert_eq!(offset_yearinfo, kv_yearinfo, "Backcomputed YearInfo should be same for date {offset_date:?} in Hebrew Year {h_year} (offset from ny {offset})"); + } + + if let Some((last_h_year, predicted_ny)) = last_year { + if last_h_year + 1 == h_year { + assert_eq!(predicted_ny, kv_ny, "{last_h_year}'s YearInfo predicts New Year {predicted_ny:?}, which does not match current new year. Got YearInfo {kv_yearinfo:?}"); + } + } + + last_year = Some((h_year, kv_ny + year_len.into())) } } } From 8d3931bb1152145aefa9f295df6c2abb3412ac42 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Tue, 2 Jan 2024 14:00:59 -0800 Subject: [PATCH 04/24] Add month length code --- .../src/hebrew_keviyah.rs | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) diff --git a/utils/calendrical_calculations/src/hebrew_keviyah.rs b/utils/calendrical_calculations/src/hebrew_keviyah.rs index e71db8fb486..ef52eff68ee 100644 --- a/utils/calendrical_calculations/src/hebrew_keviyah.rs +++ b/utils/calendrical_calculations/src/hebrew_keviyah.rs @@ -114,6 +114,23 @@ const ḤALAKIM_IN_WEEK: i64 = 1080 * 24 * 7; /// (note that the molad Beherad occurs on standard Sunday, but because it happens after 6PM it is still Hebrew Monday) const HEBREW_CALENDAR_EPOCH: RataDie = crate::julian::fixed_from_julian_book_version(-3761, 10, 7); +// Month lengths (ref: https://en.wikipedia.org/wiki/Hebrew_calendar#Months) +const TISHREI_LEN: u8 = 30; +// except in Complete years +const ḤESVAN_DEFAULT_LEN: u8 = 29; +// Except in Deficient years +const KISLEV_DEFAULT_LEN: u8 = 30; +const TEVET_LEN: u8 = 29; +const SHEVAT_LEN: u8 = 30; +const ADARI_LEN: u8 = 30; +const ADAR_LEN: u8 = 29; +const NISAN_LEN: u8 = 30; +const IYAR_LEN: u8 = 29; +const SIVAN_LEN: u8 = 30; +const TAMMUZ_LEN: u8 = 29; +const AV_LEN: u8 = 30; +const ELUL_LEN: u8 = 29; + /// Given a Hebrew Year, returns its molad specified as: /// /// - The number of weeks since the week of Beharad (Oct 6, 3761 BCE Julian) @@ -170,6 +187,7 @@ impl YearInfo { /// Returns the YearInfo and h_year for the year containing `date` pub fn year_containing_rd(date: RataDie) -> (Self, i32) { + use core_maths::*; let days_since_epoch = (date - HEBREW_CALENDAR_EPOCH) as f64; let maybe_approx = i64_to_i32(1 + days_since_epoch.div_euclid(HEBREW_APPROX_YEAR_LENGTH) as i64); @@ -352,6 +370,160 @@ impl Keviyah { } } + /// Normalize the ordinal month to the "month number" in the year (ignoring + /// leap months), i.e. Adar and Adar II are both represented by 6. + /// + /// Returns None if given the index of Adar I (6 in a leap year) + fn normalized_ordinal_month(self, ordinal_month: u8) -> Option { + if self.is_leap() { + if ordinal_month == 6 { + // Adar 1 + None + } else if ordinal_month < 6 { + Some(ordinal_month) + } else { + Some(ordinal_month - 1) + } + } else { + Some(ordinal_month) + } + } + + /// Given an ordinal, civil month (1-indexed month starting at Tishrei) + /// return its length + pub fn month_len(self, ordinal_month: u8) -> u8 { + // Normalize it to the month number + let Some(normalized_ordinal_month) = self.normalized_ordinal_month(ordinal_month) else { + return 30; + }; + debug_assert!(normalized_ordinal_month <= 12 && normalized_ordinal_month > 0); + match normalized_ordinal_month { + // Tishrei + 1 => TISHREI_LEN, + // Ḥesvan + 2 => { + if self.year_type() == YearType::Complete { + ḤESVAN_DEFAULT_LEN + 1 + } else { + ḤESVAN_DEFAULT_LEN + } + } + // Kislev + 3 => { + if self.year_type() == YearType::Deficient { + KISLEV_DEFAULT_LEN - 1 + } else { + KISLEV_DEFAULT_LEN + } + } + // Tevet + 4 => TEVET_LEN, + // Shevat + 5 => SHEVAT_LEN, + // Adar & Adar II + 6 => ADAR_LEN, + // Nisan + 7 => NISAN_LEN, + // Iyar + 8 => IYAR_LEN, + // Sivan + 9 => SIVAN_LEN, + // Tammuz + 10 => TAMMUZ_LEN, + // Av + 11 => AV_LEN, + // Elul + 12 => ELUL_LEN, + _ => { + debug_assert!(false, "Got unknown month index {ordinal_month}"); + 30 + } + } + } + + /// Get the number of days preceding this month + pub fn days_preceding(self, ordinal_month: u8) -> u16 { + let Some(normalized_ordinal_month) = self.normalized_ordinal_month(ordinal_month) else { + let month_lengths: u16 = u16::from(TISHREI_LEN) + + u16::from(ḤESVAN_DEFAULT_LEN) + + u16::from(KISLEV_DEFAULT_LEN) + + u16::from(TEVET_LEN) + + u16::from(SHEVAT_LEN); + let corrected = month_lengths as i16 + self.year_type() as i16; + return u16::try_from(corrected).unwrap_or(month_lengths); + }; + debug_assert!(normalized_ordinal_month <= 12 && normalized_ordinal_month > 0); + + let mut days = 0u16; + if normalized_ordinal_month == 1 { + return days; + } + days += u16::from(TISHREI_LEN); + + if normalized_ordinal_month == 2 { + return days; + } + days += u16::from(ḤESVAN_DEFAULT_LEN); + let year_type = self.year_type(); + if year_type == YearType::Complete { + days += 1; + } + + if normalized_ordinal_month == 3 { + return days; + } + days += u16::from(KISLEV_DEFAULT_LEN); + let year_type = self.year_type(); + if year_type == YearType::Deficient { + days -= 1; + } + + if normalized_ordinal_month == 4 { + return days; + } + days += u16::from(TEVET_LEN); + + if normalized_ordinal_month == 5 { + return days; + } + days += u16::from(SHEVAT_LEN); + + if self.is_leap() { + days += u16::from(ADARI_LEN); + } + if normalized_ordinal_month == 6 { + return days; + } + days += u16::from(ADAR_LEN); + + if normalized_ordinal_month == 7 { + return days; + } + days += u16::from(NISAN_LEN); + + if normalized_ordinal_month == 8 { + return days; + } + days += u16::from(IYAR_LEN); + + if normalized_ordinal_month == 9 { + return days; + } + days += u16::from(SIVAN_LEN); + + if normalized_ordinal_month == 10 { + return days; + } + days += u16::from(TAMMUZ_LEN); + + if normalized_ordinal_month == 11 { + return days; + } + days += u16::from(AV_LEN); + + return days; + } + /// Whether this year is a leap year pub fn is_leap(self) -> bool { debug_assert_eq!(Self::בחה as u8, 7, "Representation of keviyot changed!"); @@ -620,6 +792,25 @@ mod test { let year_len = kv_yearinfo.keviyah.year_length(); + let month_range = if kv_yearinfo.keviyah.is_leap() { + 1..14 + } else { + 1..13 + }; + + let mut days_preceding = 0; + + for month in month_range { + let kv_month_len = kv_yearinfo.keviyah.month_len(month); + let book_date = BookHebrew::from_civil_date(h_year, month, 1); + let book_month_len = + BookHebrew::last_day_of_book_hebrew_month(book_date.year, book_date.month); + assert_eq!(kv_month_len, book_month_len, "Month lengths should be same for ordinal hebrew month {month} in year {h_year}. Got YearInfo {kv_yearinfo:?}"); + + assert_eq!(days_preceding, kv_yearinfo.keviyah.days_preceding(month), "Days preceding should be the sum of preceding days for ordinal hebrew month {month} in year {h_year}. Got YearInfo {kv_yearinfo:?}"); + days_preceding += u16::from(kv_month_len); + } + for offset in [0, 1, 100, year_len - 100, year_len - 2, year_len - 1] { let offset_date = kv_ny + offset.into(); let (offset_yearinfo, offset_h_year) = YearInfo::year_containing_rd(offset_date); From 127d50e3426b74eafc301a36c32e9cad2037a104 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Tue, 2 Jan 2024 15:09:29 -0800 Subject: [PATCH 05/24] inline --- .../calendrical_calculations/src/hebrew_keviyah.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/utils/calendrical_calculations/src/hebrew_keviyah.rs b/utils/calendrical_calculations/src/hebrew_keviyah.rs index ef52eff68ee..0468478a424 100644 --- a/utils/calendrical_calculations/src/hebrew_keviyah.rs +++ b/utils/calendrical_calculations/src/hebrew_keviyah.rs @@ -56,6 +56,7 @@ use core::cmp::Ordering; // However, the time in the week that the molad occurs is sufficient to know when it gets postponed to. /// Calculate the number of months preceding the molad Tishri for a given hebrew year (Tishri is the first month) +#[inline] fn months_preceding_molad(h_year: i32) -> i32 { // Ft = INT((235N + 1 / 19)) // Where N = h_year - 1 (number of elapsed years since epoch) @@ -165,6 +166,7 @@ pub struct YearInfo { impl YearInfo { /// Compute the YearInfo for a given year + #[inline] pub fn compute_for(h_year: i32) -> Self { let (mut weeks_since_beharad, ḥalakim) = molad_details(h_year); @@ -186,6 +188,7 @@ impl YearInfo { } /// Returns the YearInfo and h_year for the year containing `date` + #[inline] pub fn year_containing_rd(date: RataDie) -> (Self, i32) { use core_maths::*; let days_since_epoch = (date - HEBREW_CALENDAR_EPOCH) as f64; @@ -232,6 +235,7 @@ impl YearInfo { } /// Compute the date of New Year's Day + #[inline] pub fn new_year(self) -> RataDie { // Beharad started on Monday const BEHARAD_START_OF_YEAR: StartOfYear = StartOfYear::Monday; @@ -329,6 +333,7 @@ impl Keviyah { /// /// Comes from the second letter in this Keviyah: /// ח = D, כ = R, ש = C + #[inline] pub fn year_type(self) -> YearType { match self { Self::בחג => YearType::Deficient, @@ -351,6 +356,7 @@ impl Keviyah { /// /// Comes from the first letter in this Keviyah: /// ב = 2 = Monday, ג = 3 = Tuesday, ה = 5 = Thursday, ז = 7 = Saturday + #[inline] pub fn start_of_year(self) -> StartOfYear { match self { Self::בחג => StartOfYear::Monday, @@ -374,6 +380,7 @@ impl Keviyah { /// leap months), i.e. Adar and Adar II are both represented by 6. /// /// Returns None if given the index of Adar I (6 in a leap year) + #[inline] fn normalized_ordinal_month(self, ordinal_month: u8) -> Option { if self.is_leap() { if ordinal_month == 6 { @@ -391,6 +398,7 @@ impl Keviyah { /// Given an ordinal, civil month (1-indexed month starting at Tishrei) /// return its length + #[inline] pub fn month_len(self, ordinal_month: u8) -> u8 { // Normalize it to the month number let Some(normalized_ordinal_month) = self.normalized_ordinal_month(ordinal_month) else { @@ -442,6 +450,7 @@ impl Keviyah { } /// Get the number of days preceding this month + #[inline] pub fn days_preceding(self, ordinal_month: u8) -> u16 { let Some(normalized_ordinal_month) = self.normalized_ordinal_month(ordinal_month) else { let month_lengths: u16 = u16::from(TISHREI_LEN) @@ -525,6 +534,7 @@ impl Keviyah { } /// Whether this year is a leap year + #[inline] pub fn is_leap(self) -> bool { debug_assert_eq!(Self::בחה as u8, 7, "Representation of keviyot changed!"); // Because we have arranged our keviyot such that all leap keviyot come after @@ -533,6 +543,7 @@ impl Keviyah { } /// Given the hebrew year for this Keviyah, calculate the YearInfo + #[inline] pub fn year_info(self, h_year: i32) -> YearInfo { let (mut weeks_since_beharad, ḥalakim) = molad_details(h_year); @@ -550,6 +561,7 @@ impl Keviyah { } /// How many days are in this year + #[inline] pub fn year_length(self) -> u16 { let base_year_length = if self.is_leap() { 384 } else { 354 }; @@ -558,6 +570,7 @@ impl Keviyah { /// Construct this from an integer between 0 and 13 /// /// Potentially useful for bitpacking + #[inline] pub fn from_integer(integer: u8) -> Self { debug_assert!( integer < 14, @@ -674,6 +687,7 @@ const FOUR_GATES_LEAP: [i32; 7] = [ /// Perform the four gates calculation, giving you the Keviyah for a given year type and /// the ḥalakim-since-beginning-of-week of its molad Tishri +#[inline] fn keviyah_for(year_type: MetonicCycleType, ḥalakim: i32) -> Keviyah { let gate = match year_type { MetonicCycleType::LMinusOne => FOUR_GATES_LMINUSONE, From b805dde5e9be1227f1718cc6bbba511a85b5bffd Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Tue, 2 Jan 2024 15:00:58 -0800 Subject: [PATCH 06/24] Plug keviyah into hebrew calendar impl --- .../calendar/src/calendar_arithmetic.rs | 17 +- components/calendar/src/hebrew.rs | 243 ++++++++---------- 2 files changed, 123 insertions(+), 137 deletions(-) diff --git a/components/calendar/src/calendar_arithmetic.rs b/components/calendar/src/calendar_arithmetic.rs index f0d29dc3948..4428de8e0b4 100644 --- a/components/calendar/src/calendar_arithmetic.rs +++ b/components/calendar/src/calendar_arithmetic.rs @@ -382,14 +382,25 @@ impl ArithmeticDate { where C: CalendarArithmetic, { - let max_month = C::months_for_every_year(year, ()); + Self::new_from_ordinals_with_info(year, month, day, ()) + } + + /// Construct a new arithmetic date from a year, month ordinal, and day, bounds checking + /// the month and day + pub fn new_from_ordinals_with_info( + year: i32, + month: u8, + day: u8, + info: C::YearInfo, + ) -> Result { + let max_month = C::months_for_every_year(year, info); if month > max_month { return Err(CalendarError::Overflow { field: "month", max: max_month as usize, }); } - let max_day = C::month_days(year, month, ()); + let max_day = C::month_days(year, month, info); if day > max_day { return Err(CalendarError::Overflow { field: "day", @@ -397,7 +408,7 @@ impl ArithmeticDate { }); } - Ok(Self::new_unchecked(year, month, day)) + Ok(Self::new_unchecked_with_info(year, month, day, info)) } } diff --git a/components/calendar/src/hebrew.rs b/components/calendar/src/hebrew.rs index e7b32c9ecd5..65a27cdb24f 100644 --- a/components/calendar/src/hebrew.rs +++ b/components/calendar/src/hebrew.rs @@ -35,6 +35,7 @@ //! assert_eq!(hebrew_datetime.time.second.number(), 0); //! ``` +use crate::calendar_arithmetic::PrecomputedDataSource; use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic}; use crate::types::FormattableMonth; use crate::AnyCalendarKind; @@ -42,8 +43,7 @@ use crate::AsCalendar; use crate::Iso; use crate::{types, Calendar, CalendarError, Date, DateDuration, DateDurationUnit, DateTime}; use ::tinystr::tinystr; -use calendrical_calculations::hebrew::BookHebrew; -use calendrical_calculations::rata_die::RataDie; +use calendrical_calculations::hebrew_keviyah::{Keviyah, YearInfo}; /// The Civil Hebrew Calendar /// @@ -87,33 +87,68 @@ impl Hebrew { } } +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub(crate) struct HebrewYearInfo { + keviyah: Keviyah, + prev_keviyah: Keviyah, +} + +impl HebrewYearInfo { + /// Convenience method to compute for a given year. Don't use this if you actually need + /// a YearInfo that you want to call .new_year() on. + /// + /// This can potentially be optimized with adjacent-year knowledge, but it's complex + #[inline] + fn compute(h_year: i32) -> Self { + let keviyah = YearInfo::compute_for(h_year).keviyah; + Self::compute_with_keviyah(keviyah, h_year) + } + /// Compute for a given year when the keviyah is already known + #[inline] + fn compute_with_keviyah(keviyah: Keviyah, h_year: i32) -> Self { + let prev_keviyah = YearInfo::compute_for(h_year - 1).keviyah; + Self { + keviyah, + prev_keviyah, + } + } +} // HEBREW CALENDAR impl CalendarArithmetic for Hebrew { - type YearInfo = (); + type YearInfo = HebrewYearInfo; - fn month_days(civil_year: i32, civil_month: u8, _data: ()) -> u8 { - Self::last_day_of_civil_hebrew_month(civil_year, civil_month) + fn month_days(_h_year: i32, ordinal_month: u8, info: HebrewYearInfo) -> u8 { + info.keviyah.month_len(ordinal_month) } - fn months_for_every_year(civil_year: i32, _data: ()) -> u8 { - Self::last_month_of_civil_hebrew_year(civil_year) + fn months_for_every_year(_h_year: i32, info: HebrewYearInfo) -> u8 { + if info.keviyah.is_leap() { + 13 + } else { + 12 + } } - fn days_in_provided_year(civil_year: i32, _data: ()) -> u16 { - BookHebrew::days_in_book_hebrew_year(civil_year) // number of days don't change between BookHebrew and Civil Hebrew + fn days_in_provided_year(_h_year: i32, info: HebrewYearInfo) -> u16 { + info.keviyah.year_length() } - fn is_leap_year(civil_year: i32, _data: ()) -> bool { - // civil and book years are the same - BookHebrew::is_hebrew_leap_year(civil_year) + fn is_leap_year(_h_year: i32, info: HebrewYearInfo) -> bool { + info.keviyah.is_leap() } - fn last_month_day_in_year(civil_year: i32, _data: ()) -> (u8, u8) { - let civil_month = Self::last_month_of_civil_hebrew_year(civil_year); - let civil_day = Self::last_day_of_civil_hebrew_month(civil_year, civil_month); + fn last_month_day_in_year(h_year: i32, info: HebrewYearInfo) -> (u8, u8) { + // Calculate the index of the last month (Elul) + let last_month = Self::months_for_every_year(h_year, info); + // Elul always has 29 days + (last_month, 29) + } +} - (civil_month, civil_day) +impl PrecomputedDataSource for () { + fn load_or_compute_info(&self, h_year: i32) -> HebrewYearInfo { + HebrewYearInfo::compute(h_year) } } @@ -127,13 +162,16 @@ impl Calendar for Hebrew { month_code: types::MonthCode, day: u8, ) -> Result { - let is_leap_year = Self::is_leap_year(year, ()); let year = if era.0 == tinystr!(16, "hebrew") || era.0 == tinystr!(16, "am") { year } else { return Err(CalendarError::UnknownEra(era.0, self.debug_name())); }; + let year_info = HebrewYearInfo::compute(year); + + let is_leap_year = year_info.keviyah.is_leap(); + let month_code_str = month_code.0.as_str(); let month_ordinal = if is_leap_year { @@ -182,17 +220,42 @@ impl Calendar for Hebrew { } }; - ArithmeticDate::new_from_ordinals(year, month_ordinal, day).map(HebrewDateInner) + ArithmeticDate::new_from_ordinals_with_info(year, month_ordinal, day, year_info) + .map(HebrewDateInner) } fn date_from_iso(&self, iso: Date) -> Self::DateInner { let fixed_iso = Iso::fixed_from_iso(*iso.inner()); - Self::civil_hebrew_from_fixed(fixed_iso).inner + let (year_info, h_year) = YearInfo::year_containing_rd(fixed_iso); + // Obtaining a 1-indexed day-in-year value + let mut day = fixed_iso - year_info.new_year() + 1; + + let year_info = HebrewYearInfo::compute_with_keviyah(year_info.keviyah, h_year); + for month in 1..14 { + let month_len = year_info.keviyah.month_len(month); + if let Ok(day) = u8::try_from(day) { + if day <= month_len { + return HebrewDateInner(ArithmeticDate::new_unchecked_with_info( + h_year, month, day, year_info, + )); + } + } + day -= i64::from(month_len); + } + debug_assert!(false, "Attempted to get Hebrew date for {fixed_iso:?}, in year {h_year}, didn't have enough days in the year"); + return HebrewDateInner(ArithmeticDate::new_unchecked_with_info( + h_year, 13, 29, year_info, + )); } fn date_to_iso(&self, date: &Self::DateInner) -> Date { - let fixed_hebrew = Self::fixed_from_civil_hebrew(*date); - Iso::iso_from_fixed(fixed_hebrew) + let year_info = date.0.year_info.keviyah.year_info(date.0.year); + + let ny = year_info.new_year(); + let days_preceding = year_info.keviyah.days_preceding(date.0.month); + + // Need to subtract 1 since the new year is itself in this year + Iso::iso_from_fixed(ny + i64::from(days_preceding) + i64::from(date.0.day) - 1) } fn months_in_year(&self, date: &Self::DateInner) -> u8 { @@ -231,12 +294,12 @@ impl Calendar for Hebrew { } fn is_in_leap_year(&self, date: &Self::DateInner) -> bool { - Self::is_leap_year(date.0.year, ()) + Self::is_leap_year(date.0.year, date.0.year_info) } fn month(&self, date: &Self::DateInner) -> FormattableMonth { let mut ordinal = date.0.month; - let is_leap_year = Self::is_leap_year(date.0.year, ()); + let is_leap_year = Self::is_leap_year(date.0.year, date.0.year_info); if is_leap_year { if ordinal == 6 { @@ -289,7 +352,7 @@ impl Calendar for Hebrew { day_of_year: date.0.day_of_year(), days_in_year: date.0.days_in_year(), prev_year: Self::year_as_hebrew(prev_year), - days_in_prev_year: Self::days_in_provided_year(prev_year, ()), + days_in_prev_year: date.0.year_info.prev_keviyah.year_length(), next_year: Self::year_as_hebrew(next_year), } } @@ -299,45 +362,6 @@ impl Calendar for Hebrew { } impl Hebrew { - // Converts a Biblical Hebrew Date to a Civil Hebrew Date - fn biblical_to_civil_date(biblical_date: BookHebrew) -> HebrewDateInner { - let (y, m, d) = biblical_date.to_civil_date(); - - debug_assert!(ArithmeticDate::::new_from_ordinals(y, m, d,).is_ok()); - HebrewDateInner(ArithmeticDate::new_unchecked(y, m, d)) - } - - // Converts a Civil Hebrew Date to a Biblical Hebrew Date - fn civil_to_biblical_date(civil_date: HebrewDateInner) -> BookHebrew { - BookHebrew::from_civil_date(civil_date.0.year, civil_date.0.month, civil_date.0.day) - } - - fn last_month_of_civil_hebrew_year(civil_year: i32) -> u8 { - if Self::is_leap_year(civil_year, ()) { - 13 // there are 13 months in a leap year - } else { - 12 - } - } - - fn last_day_of_civil_hebrew_month(civil_year: i32, civil_month: u8) -> u8 { - let book_date = Hebrew::civil_to_biblical_date(HebrewDateInner( - ArithmeticDate::new_unchecked(civil_year, civil_month, 1), - )); - BookHebrew::last_day_of_book_hebrew_month(book_date.year, book_date.month) - } - - // "Fixed" is a day count representation of calendars staring from Jan 1st of year 1 of the Georgian Calendar. - fn fixed_from_civil_hebrew(date: HebrewDateInner) -> RataDie { - let book_date = Hebrew::civil_to_biblical_date(date); - BookHebrew::fixed_from_book_hebrew(book_date) - } - - fn civil_hebrew_from_fixed(date: RataDie) -> Date { - let book_hebrew = BookHebrew::book_hebrew_from_fixed(date); - Date::from_raw(Hebrew::biblical_to_civil_date(book_hebrew), Hebrew) - } - fn year_as_hebrew(civil_year: i32) -> types::FormattableYear { types::FormattableYear { era: types::Era(tinystr!(16, "hebrew")), @@ -375,7 +399,9 @@ impl> Date { day: u8, calendar: A, ) -> Result, CalendarError> { - ArithmeticDate::new_from_ordinals(year, month, day) + let year_info = HebrewYearInfo::compute(year); + + ArithmeticDate::new_from_ordinals_with_info(year, month, day, year_info) .map(HebrewDateInner) .map(|inner| Date::from_raw(inner, calendar)) } @@ -531,86 +557,35 @@ mod tests { (1, TEVET, 5783), ]; - let civil_hebrew_dates: [(u8, u8, i32); 48] = [ - (26, 4, 5781), - (12, 5, 5781), - (28, 5, 5781), - (13, 6, 5781), - (26, 6, 5781), - (12, 7, 5781), - (28, 7, 5781), - (13, 8, 5781), - (28, 8, 5781), - (14, 9, 5781), - (30, 9, 5781), - (15, 10, 5781), - (1, 11, 5781), - (16, 11, 5781), - (2, 12, 5781), - (17, 12, 5781), - (4, 1, 5782), - (19, 1, 5782), - (4, 2, 5782), - (19, 2, 5782), - (6, 3, 5782), - (21, 3, 5782), - (6, 4, 5782), - (21, 4, 5782), - (8, 5, 5782), - (23, 5, 5782), - (9, 6, 5782), - (24, 6, 5782), - (7, 7, 5782), - (22, 7, 5782), - (9, 8, 5782), - (24, 8, 5782), - (9, 9, 5782), - (24, 9, 5782), - (11, 10, 5782), - (26, 10, 5782), - (11, 11, 5782), - (26, 11, 5782), - (13, 12, 5782), - (28, 12, 5782), - (14, 13, 5782), - (29, 13, 5782), - (15, 1, 5783), - (30, 1, 5783), - (16, 2, 5783), - (1, 3, 5783), - (16, 3, 5783), - (1, 4, 5783), - ]; - - for (iso_date, (book_date_nums, civil_date_nums)) in iso_dates - .iter() - .zip(book_hebrew_dates.iter().zip(civil_hebrew_dates.iter())) - { + for (iso_date, book_date_nums) in iso_dates.iter().zip(book_hebrew_dates.iter()) { + // This just checks the integrity of the test data let book_date = BookHebrew { year: book_date_nums.2, month: book_date_nums.1, day: book_date_nums.0, }; - let civil_date: HebrewDateInner = HebrewDateInner(ArithmeticDate::new_unchecked( - civil_date_nums.2, - civil_date_nums.1, - civil_date_nums.0, - )); - let book_to_civil = Hebrew::biblical_to_civil_date(book_date); - let civil_to_book = Hebrew::civil_to_biblical_date(civil_date); + let (y, m, d) = book_date.to_civil_date(); + // let book_from_civil = BookHebrew::from_civil_date(civil_date_nums.2, civil_date_nums.1, civil_date_nums.1); + // assert_eq!(book_date, book_from_civil); - assert_eq!(civil_date, book_to_civil); - assert_eq!(book_date, civil_to_book); + let hy = HebrewYearInfo::compute(y); + let hebrew_date: HebrewDateInner = + HebrewDateInner(ArithmeticDate::new_unchecked_with_info(y, m, d, hy)); + let hebrew_date = Date::from_raw(hebrew_date, Hebrew); - let iso_to_fixed = Iso::fixed_from_iso(iso_date.inner); - let fixed_to_hebrew = Hebrew::civil_hebrew_from_fixed(iso_to_fixed); + let iso_to_hebrew = iso_date.to_calendar(Hebrew); - let hebrew_to_fixed = Hebrew::fixed_from_civil_hebrew(civil_date); - let fixed_to_iso = Iso::iso_from_fixed(hebrew_to_fixed); + let hebrew_to_iso = hebrew_date.to_calendar(Iso); - assert_eq!(fixed_to_hebrew.inner, civil_date); - assert_eq!(fixed_to_iso.inner, iso_date.inner); + assert_eq!( + hebrew_to_iso, *iso_date, + "Failed comparing to-ISO value for {hebrew_date:?} => {iso_date:?}" + ); + assert_eq!( + iso_to_hebrew, hebrew_date, + "Failed comparing to-hebrew value for {iso_date:?} => {hebrew_date:?}" + ); } } From 55c8256ee2facb913773e9e6bcd23746f0985a22 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Tue, 2 Jan 2024 16:31:21 -0800 Subject: [PATCH 07/24] fixes --- components/calendar/benches/convert.rs | 2 -- components/calendar/src/hebrew.rs | 4 ++-- .../src/hebrew_keviyah.rs | 15 +++++++-------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/components/calendar/benches/convert.rs b/components/calendar/benches/convert.rs index 38441f8fcc8..d28301536ac 100644 --- a/components/calendar/benches/convert.rs +++ b/components/calendar/benches/convert.rs @@ -75,7 +75,6 @@ fn convert_benches(c: &mut Criterion) { icu::calendar::gregorian::Gregorian, ); - #[cfg(feature = "bench")] bench_calendar( &mut group, @@ -83,7 +82,6 @@ fn convert_benches(c: &mut Criterion) { icu::calendar::hebrew::Hebrew::new_always_calculating(), ); - #[cfg(feature = "bench")] bench_calendar( &mut group, diff --git a/components/calendar/src/hebrew.rs b/components/calendar/src/hebrew.rs index 65a27cdb24f..ebed805dbb3 100644 --- a/components/calendar/src/hebrew.rs +++ b/components/calendar/src/hebrew.rs @@ -243,9 +243,9 @@ impl Calendar for Hebrew { day -= i64::from(month_len); } debug_assert!(false, "Attempted to get Hebrew date for {fixed_iso:?}, in year {h_year}, didn't have enough days in the year"); - return HebrewDateInner(ArithmeticDate::new_unchecked_with_info( + HebrewDateInner(ArithmeticDate::new_unchecked_with_info( h_year, 13, 29, year_info, - )); + )) } fn date_to_iso(&self, date: &Self::DateInner) -> Date { diff --git a/utils/calendrical_calculations/src/hebrew_keviyah.rs b/utils/calendrical_calculations/src/hebrew_keviyah.rs index 0468478a424..b071a60cc41 100644 --- a/utils/calendrical_calculations/src/hebrew_keviyah.rs +++ b/utils/calendrical_calculations/src/hebrew_keviyah.rs @@ -190,6 +190,7 @@ impl YearInfo { /// Returns the YearInfo and h_year for the year containing `date` #[inline] pub fn year_containing_rd(date: RataDie) -> (Self, i32) { + #[allow(unused_imports)] use core_maths::*; let days_since_epoch = (date - HEBREW_CALENDAR_EPOCH) as f64; let maybe_approx = @@ -383,13 +384,11 @@ impl Keviyah { #[inline] fn normalized_ordinal_month(self, ordinal_month: u8) -> Option { if self.is_leap() { - if ordinal_month == 6 { - // Adar 1 - None - } else if ordinal_month < 6 { - Some(ordinal_month) - } else { - Some(ordinal_month - 1) + match ordinal_month.cmp(&6) { + // Adar I + Ordering::Equal => None, + Ordering::Less => Some(ordinal_month), + Ordering::Greater => Some(ordinal_month - 1), } } else { Some(ordinal_month) @@ -530,7 +529,7 @@ impl Keviyah { } days += u16::from(AV_LEN); - return days; + days } /// Whether this year is a leap year From 805092439e77f7df2f9d32de4df2ec9dc9b6f7c4 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Wed, 3 Jan 2024 18:49:54 -0800 Subject: [PATCH 08/24] Add month_day_for(), cleanup --- components/calendar/src/hebrew.rs | 29 +++++-------------- .../src/hebrew_keviyah.rs | 26 +++++++++++++++++ 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/components/calendar/src/hebrew.rs b/components/calendar/src/hebrew.rs index ebed805dbb3..3444b529747 100644 --- a/components/calendar/src/hebrew.rs +++ b/components/calendar/src/hebrew.rs @@ -138,11 +138,8 @@ impl CalendarArithmetic for Hebrew { info.keviyah.is_leap() } - fn last_month_day_in_year(h_year: i32, info: HebrewYearInfo) -> (u8, u8) { - // Calculate the index of the last month (Elul) - let last_month = Self::months_for_every_year(h_year, info); - // Elul always has 29 days - (last_month, 29) + fn last_month_day_in_year(_h_year: i32, info: HebrewYearInfo) -> (u8, u8) { + info.keviyah.last_month_day_in_year() } } @@ -228,24 +225,14 @@ impl Calendar for Hebrew { let fixed_iso = Iso::fixed_from_iso(*iso.inner()); let (year_info, h_year) = YearInfo::year_containing_rd(fixed_iso); // Obtaining a 1-indexed day-in-year value - let mut day = fixed_iso - year_info.new_year() + 1; + let day = fixed_iso - year_info.new_year() + 1; + let day = u16::try_from(day).unwrap_or(u16::MAX); let year_info = HebrewYearInfo::compute_with_keviyah(year_info.keviyah, h_year); - for month in 1..14 { - let month_len = year_info.keviyah.month_len(month); - if let Ok(day) = u8::try_from(day) { - if day <= month_len { - return HebrewDateInner(ArithmeticDate::new_unchecked_with_info( - h_year, month, day, year_info, - )); - } - } - day -= i64::from(month_len); - } - debug_assert!(false, "Attempted to get Hebrew date for {fixed_iso:?}, in year {h_year}, didn't have enough days in the year"); - HebrewDateInner(ArithmeticDate::new_unchecked_with_info( - h_year, 13, 29, year_info, - )) + let (month, day) = year_info.keviyah.month_day_for(day); + return HebrewDateInner(ArithmeticDate::new_unchecked_with_info( + h_year, month, day, year_info, + )); } fn date_to_iso(&self, date: &Self::DateInner) -> Date { diff --git a/utils/calendrical_calculations/src/hebrew_keviyah.rs b/utils/calendrical_calculations/src/hebrew_keviyah.rs index b071a60cc41..a3ef71e36b7 100644 --- a/utils/calendrical_calculations/src/hebrew_keviyah.rs +++ b/utils/calendrical_calculations/src/hebrew_keviyah.rs @@ -532,6 +532,32 @@ impl Keviyah { days } + /// Given a day of the year, return the ordinal month and day as (month, day). + pub fn month_day_for(self, mut day: u16) -> (u8, u8) { + for month in 1..14 { + let month_len = self.month_len(month); + if let Ok(day) = u8::try_from(day) { + if day <= month_len { + return (month, day); + } + } + day -= u16::from(month_len); + } + debug_assert!(false, "Attempted to get Hebrew date for {day:?}, in keviyah {self:?}, didn't have enough days in the year"); + self.last_month_day_in_year() + } + + /// Return the last ordinal month and day in this year as (month, day) + #[inline] + pub fn last_month_day_in_year(self) -> (u8, u8) { + // Elul is always the last month of the year + if self.is_leap() { + (13, ELUL_LEN) + } else { + (12, ELUL_LEN) + } + } + /// Whether this year is a leap year #[inline] pub fn is_leap(self) -> bool { From 3b8a6c418692bcca6af3e918fb916334c566f70b Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Wed, 3 Jan 2024 19:56:38 -0800 Subject: [PATCH 09/24] clippy --- components/calendar/src/hebrew.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/calendar/src/hebrew.rs b/components/calendar/src/hebrew.rs index 3444b529747..f22763ce460 100644 --- a/components/calendar/src/hebrew.rs +++ b/components/calendar/src/hebrew.rs @@ -230,9 +230,9 @@ impl Calendar for Hebrew { let year_info = HebrewYearInfo::compute_with_keviyah(year_info.keviyah, h_year); let (month, day) = year_info.keviyah.month_day_for(day); - return HebrewDateInner(ArithmeticDate::new_unchecked_with_info( + HebrewDateInner(ArithmeticDate::new_unchecked_with_info( h_year, month, day, year_info, - )); + )) } fn date_to_iso(&self, date: &Self::DateInner) -> Date { From fdbb0cba3115018668e097d760ebfb71c1b13a46 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Wed, 3 Jan 2024 20:25:59 -0800 Subject: [PATCH 10/24] test --- .../src/hebrew_keviyah.rs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/utils/calendrical_calculations/src/hebrew_keviyah.rs b/utils/calendrical_calculations/src/hebrew_keviyah.rs index a3ef71e36b7..2f8d9887096 100644 --- a/utils/calendrical_calculations/src/hebrew_keviyah.rs +++ b/utils/calendrical_calculations/src/hebrew_keviyah.rs @@ -789,6 +789,36 @@ mod test { assert_eq!(HEBREW_CALENDAR_EPOCH, hebrew::FIXED_HEBREW_EPOCH); } + #[test] + fn test_roundtrip_days() { + for h_year in (1..10).chain(5775..5795).chain(10000..10010) { + let year_info = YearInfo::compute_for(h_year); + let ny = year_info.new_year(); + for day in 1..=year_info.keviyah.year_length() { + let offset_date = ny + i64::from(day) - 1; + let (offset_yearinfo, offset_h_year) = YearInfo::year_containing_rd(offset_date); + + assert_eq!( + offset_h_year, h_year, + "Backcomputed h_year should be same for day {day} in Hebrew Year {h_year}" + ); + assert_eq!( + offset_yearinfo, year_info, + "Backcomputed YearInfo should be same for day {day} in Hebrew Year {h_year}" + ); + + let (month, day2) = year_info.keviyah.month_day_for(day); + + let days_preceding = year_info.keviyah.days_preceding(month); + + assert_eq!( + days_preceding + u16::from(day2), + day, + "{h_year}-{month}-{day2} should round trip for day-of-year {day}" + ) + } + } + } #[test] fn test_book_parity() { let mut last_year = None; From 1a9ed56c98bfa79ffb2a2afeffbc53d86a91ee71 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 4 Jan 2024 18:04:45 +0000 Subject: [PATCH 11/24] Update utils/calendrical_calculations/src/hebrew_keviyah.rs Co-authored-by: Shane F. Carr --- utils/calendrical_calculations/src/hebrew_keviyah.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/calendrical_calculations/src/hebrew_keviyah.rs b/utils/calendrical_calculations/src/hebrew_keviyah.rs index 2f8d9887096..44d5fca9617 100644 --- a/utils/calendrical_calculations/src/hebrew_keviyah.rs +++ b/utils/calendrical_calculations/src/hebrew_keviyah.rs @@ -401,7 +401,7 @@ impl Keviyah { pub fn month_len(self, ordinal_month: u8) -> u8 { // Normalize it to the month number let Some(normalized_ordinal_month) = self.normalized_ordinal_month(ordinal_month) else { - return 30; + return ADARI_LEN; }; debug_assert!(normalized_ordinal_month <= 12 && normalized_ordinal_month > 0); match normalized_ordinal_month { From 201b4b90328d7de9549dce9e1371a75f5465d3b4 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 4 Jan 2024 10:26:39 -0800 Subject: [PATCH 12/24] Better length correction APIs --- .../src/hebrew_keviyah.rs | 56 ++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/utils/calendrical_calculations/src/hebrew_keviyah.rs b/utils/calendrical_calculations/src/hebrew_keviyah.rs index 44d5fca9617..bb73d47ce7f 100644 --- a/utils/calendrical_calculations/src/hebrew_keviyah.rs +++ b/utils/calendrical_calculations/src/hebrew_keviyah.rs @@ -314,6 +314,30 @@ pub enum YearType { Complete = 1, } +impl YearType { + /// The length correction from a regular year (354/385) + fn length_correction(self) -> i8 { + self as i8 + } + + /// The length of Ḥesvan + fn ḥesvan_length(self) -> u8 { + if self == Self::Complete { + ḤESVAN_DEFAULT_LEN + 1 + } else { + ḤESVAN_DEFAULT_LEN + 0 + } + } + + /// The length correction of Kislev + fn kislev_length(self) -> u8 { + if self == Self::Deficient { + KISLEV_DEFAULT_LEN - 1 + } else { + KISLEV_DEFAULT_LEN + 0 + } + } +} /// The day of the new year. Only these four days are permitted. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] #[allow(clippy::exhaustive_enums)] // This is intrinsic to the calendar @@ -408,21 +432,9 @@ impl Keviyah { // Tishrei 1 => TISHREI_LEN, // Ḥesvan - 2 => { - if self.year_type() == YearType::Complete { - ḤESVAN_DEFAULT_LEN + 1 - } else { - ḤESVAN_DEFAULT_LEN - } - } + 2 => self.year_type().ḥesvan_length(), // Kislev - 3 => { - if self.year_type() == YearType::Deficient { - KISLEV_DEFAULT_LEN - 1 - } else { - KISLEV_DEFAULT_LEN - } - } + 3 => self.year_type().kislev_length(), // Tevet 4 => TEVET_LEN, // Shevat @@ -457,7 +469,7 @@ impl Keviyah { + u16::from(KISLEV_DEFAULT_LEN) + u16::from(TEVET_LEN) + u16::from(SHEVAT_LEN); - let corrected = month_lengths as i16 + self.year_type() as i16; + let corrected = month_lengths as i16 + i16::from(self.year_type().length_correction()); return u16::try_from(corrected).unwrap_or(month_lengths); }; debug_assert!(normalized_ordinal_month <= 12 && normalized_ordinal_month > 0); @@ -471,20 +483,14 @@ impl Keviyah { if normalized_ordinal_month == 2 { return days; } - days += u16::from(ḤESVAN_DEFAULT_LEN); + let year_type = self.year_type(); - if year_type == YearType::Complete { - days += 1; - } + days += u16::from(year_type.ḥesvan_length()); if normalized_ordinal_month == 3 { return days; } - days += u16::from(KISLEV_DEFAULT_LEN); - let year_type = self.year_type(); - if year_type == YearType::Deficient { - days -= 1; - } + days += u16::from(year_type.kislev_length()); if normalized_ordinal_month == 4 { return days; @@ -590,7 +596,7 @@ impl Keviyah { pub fn year_length(self) -> u16 { let base_year_length = if self.is_leap() { 384 } else { 354 }; - (base_year_length + self.year_type() as i16) as u16 + (base_year_length + i16::from(self.year_type().length_correction())) as u16 } /// Construct this from an integer between 0 and 13 /// From 1bc54960dbc4fdd1a57051995fe9a2a4f21d12d4 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 4 Jan 2024 10:29:41 -0800 Subject: [PATCH 13/24] saturate --- utils/calendrical_calculations/src/hebrew_keviyah.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/utils/calendrical_calculations/src/hebrew_keviyah.rs b/utils/calendrical_calculations/src/hebrew_keviyah.rs index bb73d47ce7f..1dbab000008 100644 --- a/utils/calendrical_calculations/src/hebrew_keviyah.rs +++ b/utils/calendrical_calculations/src/hebrew_keviyah.rs @@ -204,11 +204,17 @@ impl YearInfo { let (yi, h_year) = match cmp { // The approx year is a year greater. Go one year down - Ordering::Greater => (Self::compute_for(approx - 1), approx - 1), + Ordering::Greater => { + let prev = approx.saturating_sub(1); + (Self::compute_for(prev), prev) + } // Bullseye Ordering::Equal => (yi, approx), // The approx year is a year lower. Go one year up. - Ordering::Less => (Self::compute_for(approx + 1), approx + 1), + Ordering::Less => { + let next = approx.saturating_add(1); + (Self::compute_for(next), next) + } }; debug_assert!(yi.compare(date).is_eq() || maybe_approx.is_err(), // The data will be incorrect if we saturate, and that's expected From bebd22ae51d45fd6d0fa4eeebbc13abfe801931e Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 4 Jan 2024 10:56:38 -0800 Subject: [PATCH 14/24] Clamp to min/max r.d. --- .../src/hebrew_keviyah.rs | 67 ++++++++++++++++--- 1 file changed, 57 insertions(+), 10 deletions(-) diff --git a/utils/calendrical_calculations/src/hebrew_keviyah.rs b/utils/calendrical_calculations/src/hebrew_keviyah.rs index 1dbab000008..10292080345 100644 --- a/utils/calendrical_calculations/src/hebrew_keviyah.rs +++ b/utils/calendrical_calculations/src/hebrew_keviyah.rs @@ -57,13 +57,13 @@ use core::cmp::Ordering; /// Calculate the number of months preceding the molad Tishri for a given hebrew year (Tishri is the first month) #[inline] -fn months_preceding_molad(h_year: i32) -> i32 { +fn months_preceding_molad(h_year: i32) -> i64 { // Ft = INT((235N + 1 / 19)) // Where N = h_year - 1 (number of elapsed years since epoch) // This math essentially comes from the Metonic cycle of 19 years containing // 235 months: 12 months per year, plus an extra month for each of the 7 leap years. - (235 * (h_year - 1) + 1) / 19 + (235 * (i64::from(h_year) - 1) + 1) / 19 } /// Conveniently create a constant for a ḥalakim (by default in 1-indexed notation). Produces a constant @@ -132,6 +132,18 @@ const TAMMUZ_LEN: u8 = 29; const AV_LEN: u8 = 30; const ELUL_LEN: u8 = 29; +/// The minumum hebrew year supported by this code (this is the minimum value for i32) +pub const HEBREW_MIN_YEAR: i32 = i32::min_value(); +/// The minumum R.D. supported by this code (this code will clamp outside of it) +// (this constant is verified by tests) +pub const HEBREW_MIN_RD: RataDie = RataDie::new(-784362951949); +/// The maximum hebrew year supported by this code (this is the maximum alue for i32) +// (this constant is verified by tests) +pub const HEBREW_MAX_YEAR: i32 = i32::max_value(); +/// The maximum R.D. supported by this code (this is the last day in [`HEBREW_MAX_YEAR`]) +// (this constant is verified by tests) +pub const HEBREW_MAX_RD: RataDie = RataDie::new(784360204356); + /// Given a Hebrew Year, returns its molad specified as: /// /// - The number of weeks since the week of Beharad (Oct 6, 3761 BCE Julian) @@ -142,7 +154,7 @@ fn molad_details(h_year: i32) -> (i64, i32) { // The molad tishri expressed in parts since the beginning of the week containing Molad of Beharad // Formula from Adjler Appendix A - let molad = MOLAD_BEHERAD_OFFSET as i64 + months_preceding as i64 * HEBREW_LUNATION_TIME as i64; + let molad = MOLAD_BEHERAD_OFFSET as i64 + months_preceding * HEBREW_LUNATION_TIME as i64; // Split into quotient and remainder let weeks_since_beharad = molad.div_euclid(ḤALAKIM_IN_WEEK); @@ -188,13 +200,21 @@ impl YearInfo { } /// Returns the YearInfo and h_year for the year containing `date` + /// + /// This will clamp the R.D. such that the hebrew year is within range for i32 #[inline] pub fn year_containing_rd(date: RataDie) -> (Self, i32) { #[allow(unused_imports)] use core_maths::*; + + let date = date.clamp(HEBREW_MIN_RD, HEBREW_MAX_RD); + let days_since_epoch = (date - HEBREW_CALENDAR_EPOCH) as f64; let maybe_approx = i64_to_i32(1 + days_since_epoch.div_euclid(HEBREW_APPROX_YEAR_LENGTH) as i64); + debug_assert!(maybe_approx.is_ok(), + "year_containing_rd should have clamped {date:?} between {HEBREW_MIN_RD:?} and {HEBREW_MAX_RD:?} \ + and thus be well in bounds for year calculation math"); let approx = maybe_approx.unwrap_or_else(|e| e.saturate()); let yi = Self::compute_for(approx); @@ -217,7 +237,7 @@ impl YearInfo { } }; - debug_assert!(yi.compare(date).is_eq() || maybe_approx.is_err(), // The data will be incorrect if we saturate, and that's expected + debug_assert!(yi.compare(date).is_eq(), "Date {date:?} calculated approximately to Hebrew Year {approx} (comparison: {cmp:?}), \ should be contained in adjacent year {h_year} but that year is still {:?} it", yi.compare(date)); @@ -658,15 +678,22 @@ enum MetonicCycleType { impl MetonicCycleType { fn for_h_year(h_year: i32) -> Self { - // The -1 is because h_year is 1-indexed - // The +1 is because our match statement is also 1-indexed - // and we want to have this match statement match resources that list - // these year types (both Adjler and Wikipedia). - match ((h_year - 1) % 19) + 1 { + // h_year is 1-indexed, and our metonic cycle year numberings + // are 1-indexed, so we really need to do `(h_year - 1) % 19 + 1` + // + // However, that is equivalent to `h_year % 19` provided you handle the + // fact that that operation will produce 0 instead of 19. + // Both numbers end up in our wildcard leap year arm so that's fine. + let remainder = h_year.rem_euclid(19); + match remainder { + // These numbers are 1-indexed 2 | 5 | 10 | 13 | 16 => Self::LMinusOne, 1 | 4 | 9 | 12 | 15 => Self::LPlusOne, 7 | 18 => Self::LPlusMinusOne, - _ => Self::Leap, + _ => { + debug_assert!(matches!(remainder, 3 | 6 | 8 | 11 | 14 | 17 | 0 | 19)); + Self::Leap + } } } } @@ -909,4 +936,24 @@ mod test { last_year = Some((h_year, kv_ny + year_len.into())) } } + #[test] + fn test_minmax() { + let min = YearInfo::compute_for(HEBREW_MIN_YEAR); + let min_ny = min.new_year(); + assert_eq!(min_ny, HEBREW_MIN_RD); + + let (recomputed_yi, recomputed_y) = YearInfo::year_containing_rd(min_ny); + assert_eq!(recomputed_y, HEBREW_MIN_YEAR); + assert_eq!(recomputed_yi, min); + + let max = YearInfo::compute_for(HEBREW_MAX_YEAR); + let max_ny = max.new_year(); + // -1 since max_ny is also a part of the year + let max_last = max_ny + i64::from(max.keviyah.year_length()) - 1; + assert_eq!(max_last, HEBREW_MAX_RD); + + let (recomputed_yi, recomputed_y) = YearInfo::year_containing_rd(max_last); + assert_eq!(recomputed_y, HEBREW_MAX_YEAR); + assert_eq!(recomputed_yi, max); + } } From 88f11b144448c7bb5c7d70747e9e4e4a7b7299c9 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 4 Jan 2024 11:47:55 -0800 Subject: [PATCH 15/24] Write days_preceding as match statement --- .../src/hebrew_keviyah.rs | 112 ++++++++---------- 1 file changed, 52 insertions(+), 60 deletions(-) diff --git a/utils/calendrical_calculations/src/hebrew_keviyah.rs b/utils/calendrical_calculations/src/hebrew_keviyah.rs index 10292080345..d0a34a3fc08 100644 --- a/utils/calendrical_calculations/src/hebrew_keviyah.rs +++ b/utils/calendrical_calculations/src/hebrew_keviyah.rs @@ -379,6 +379,22 @@ pub enum StartOfYear { Saturday = 7, } +// Given a constant expression of the type FOO + BAR + BAZ convert +// every element into a u16 and return +macro_rules! u16_cvt( + // the $first / $rest pattern is needed since + // macros cannot use `+` as a separator in repetition + ($first:ident $(+ $rest:ident)*) => { + { + // make sure it is constant + // we use as here because it works in consts and in this context + // overflow will panic anyway + const COMPUTED: u16 = $first as u16 $(+ $rest as u16)*; + COMPUTED + } + }; +); + impl Keviyah { /// Get the type of year for this Keviyah. /// @@ -489,77 +505,53 @@ impl Keviyah { /// Get the number of days preceding this month #[inline] pub fn days_preceding(self, ordinal_month: u8) -> u16 { + // convenience constant to keep the additions smallish + // Number of days before (any) Adar in a regular year + const BEFORE_ADAR_DEFAULT_LEN: u16 = u16_cvt!( + TISHREI_LEN + ḤESVAN_DEFAULT_LEN + KISLEV_DEFAULT_LEN + TEVET_LEN + SHEVAT_LEN + ); + let Some(normalized_ordinal_month) = self.normalized_ordinal_month(ordinal_month) else { - let month_lengths: u16 = u16::from(TISHREI_LEN) - + u16::from(ḤESVAN_DEFAULT_LEN) - + u16::from(KISLEV_DEFAULT_LEN) - + u16::from(TEVET_LEN) - + u16::from(SHEVAT_LEN); - let corrected = month_lengths as i16 + i16::from(self.year_type().length_correction()); - return u16::try_from(corrected).unwrap_or(month_lengths); + // Get Adar I out of the way + let corrected = + BEFORE_ADAR_DEFAULT_LEN as i16 + i16::from(self.year_type().length_correction()); + return corrected as u16; }; debug_assert!(normalized_ordinal_month <= 12 && normalized_ordinal_month > 0); - let mut days = 0u16; - if normalized_ordinal_month == 1 { - return days; - } - days += u16::from(TISHREI_LEN); - - if normalized_ordinal_month == 2 { - return days; - } - let year_type = self.year_type(); - days += u16::from(year_type.ḥesvan_length()); - - if normalized_ordinal_month == 3 { - return days; - } - days += u16::from(year_type.kislev_length()); - if normalized_ordinal_month == 4 { - return days; - } - days += u16::from(TEVET_LEN); + let mut days = match normalized_ordinal_month { + 1 => 0, + 2 => u16_cvt!(TISHREI_LEN), + 3 => u16_cvt!(TISHREI_LEN) + u16::from(year_type.ḥesvan_length()), + // Use default lengths after this, we'll apply the correction later + // (This helps optimize this into a simple jump table) + 4 => u16_cvt!(TISHREI_LEN + ḤESVAN_DEFAULT_LEN + KISLEV_DEFAULT_LEN), + 5 => u16_cvt!(TISHREI_LEN + ḤESVAN_DEFAULT_LEN + KISLEV_DEFAULT_LEN + TEVET_LEN), + 6 => BEFORE_ADAR_DEFAULT_LEN, + 7 => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN), + 8 => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN), + 9 => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYAR_LEN), + 10 => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYAR_LEN + SIVAN_LEN), + #[rustfmt::skip] + 11 => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYAR_LEN + SIVAN_LEN + TAMMUZ_LEN), + #[rustfmt::skip] + _ => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYAR_LEN + SIVAN_LEN + TAMMUZ_LEN + AV_LEN), + }; - if normalized_ordinal_month == 5 { - return days; + // If it is after Kislev and Ḥesvan, we should add the year correction + if normalized_ordinal_month >= 4 { + // Ensure the casts are fine + debug_assert!(days > 1 && year_type.length_correction().abs() <= 1); + days = (days as i16 + year_type.length_correction() as i16) as u16; } - days += u16::from(SHEVAT_LEN); - if self.is_leap() { + // In a leap year, after Adar (and including Adar II), we should add + // the length of Adar 1 + if normalized_ordinal_month >= 6 && self.is_leap() { days += u16::from(ADARI_LEN); } - if normalized_ordinal_month == 6 { - return days; - } - days += u16::from(ADAR_LEN); - - if normalized_ordinal_month == 7 { - return days; - } - days += u16::from(NISAN_LEN); - - if normalized_ordinal_month == 8 { - return days; - } - days += u16::from(IYAR_LEN); - - if normalized_ordinal_month == 9 { - return days; - } - days += u16::from(SIVAN_LEN); - - if normalized_ordinal_month == 10 { - return days; - } - days += u16::from(TAMMUZ_LEN); - - if normalized_ordinal_month == 11 { - return days; - } - days += u16::from(AV_LEN); days } From 9e019781993aee50fabc2dc2acb18df726187351 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 4 Jan 2024 11:50:04 -0800 Subject: [PATCH 16/24] clip --- utils/calendrical_calculations/src/hebrew_keviyah.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/calendrical_calculations/src/hebrew_keviyah.rs b/utils/calendrical_calculations/src/hebrew_keviyah.rs index d0a34a3fc08..d8e2c7d0dd6 100644 --- a/utils/calendrical_calculations/src/hebrew_keviyah.rs +++ b/utils/calendrical_calculations/src/hebrew_keviyah.rs @@ -351,7 +351,7 @@ impl YearType { if self == Self::Complete { ḤESVAN_DEFAULT_LEN + 1 } else { - ḤESVAN_DEFAULT_LEN + 0 + ḤESVAN_DEFAULT_LEN } } @@ -360,7 +360,7 @@ impl YearType { if self == Self::Deficient { KISLEV_DEFAULT_LEN - 1 } else { - KISLEV_DEFAULT_LEN + 0 + KISLEV_DEFAULT_LEN } } } From 162011ed54a9de5b2ddbb23a708d0dfcbdc7e477 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 4 Jan 2024 14:08:43 -0800 Subject: [PATCH 17/24] Add normalized month constants --- .../src/hebrew_keviyah.rs | 94 +++++++++++-------- 1 file changed, 56 insertions(+), 38 deletions(-) diff --git a/utils/calendrical_calculations/src/hebrew_keviyah.rs b/utils/calendrical_calculations/src/hebrew_keviyah.rs index d8e2c7d0dd6..4457c26fbad 100644 --- a/utils/calendrical_calculations/src/hebrew_keviyah.rs +++ b/utils/calendrical_calculations/src/hebrew_keviyah.rs @@ -132,6 +132,34 @@ const TAMMUZ_LEN: u8 = 29; const AV_LEN: u8 = 30; const ELUL_LEN: u8 = 29; +/// Normalized month constant for Tishrei +/// +/// These are not ordinal months, rather these are the month number in a regular year +/// Adar, Adar I and Adar II all normalize to 6 +pub const TISHREI: u8 = 1; +/// Normalized month constant (see [`TISHREI`]) +pub const ḤESVAN: u8 = 2; +/// Normalized month constant (see [`TISHREI`]) +pub const KISLEV: u8 = 3; +/// Normalized month constant (see [`TISHREI`]) +pub const TEVET: u8 = 4; +/// Normalized month constant (see [`TISHREI`]) +pub const SHEVAT: u8 = 5; +/// Normalized month constant (see [`TISHREI`]) +pub const ADAR: u8 = 6; +/// Normalized month constant (see [`TISHREI`]) +pub const NISAN: u8 = 7; +/// Normalized month constant (see [`TISHREI`]) +pub const IYAR: u8 = 8; +/// Normalized month constant (see [`TISHREI`]) +pub const SIVAN: u8 = 9; +/// Normalized month constant (see [`TISHREI`]) +pub const TAMMUZ: u8 = 10; +/// Normalized month constant (see [`TISHREI`]) +pub const AV: u8 = 11; +/// Normalized month constant (see [`TISHREI`]) +pub const ELUL: u8 = 12; + /// The minumum hebrew year supported by this code (this is the minimum value for i32) pub const HEBREW_MIN_YEAR: i32 = i32::min_value(); /// The minumum R.D. supported by this code (this code will clamp outside of it) @@ -471,30 +499,18 @@ impl Keviyah { }; debug_assert!(normalized_ordinal_month <= 12 && normalized_ordinal_month > 0); match normalized_ordinal_month { - // Tishrei - 1 => TISHREI_LEN, - // Ḥesvan - 2 => self.year_type().ḥesvan_length(), - // Kislev - 3 => self.year_type().kislev_length(), - // Tevet - 4 => TEVET_LEN, - // Shevat - 5 => SHEVAT_LEN, - // Adar & Adar II - 6 => ADAR_LEN, - // Nisan - 7 => NISAN_LEN, - // Iyar - 8 => IYAR_LEN, - // Sivan - 9 => SIVAN_LEN, - // Tammuz - 10 => TAMMUZ_LEN, - // Av - 11 => AV_LEN, - // Elul - 12 => ELUL_LEN, + TISHREI => TISHREI_LEN, + ḤESVAN => self.year_type().ḥesvan_length(), + KISLEV => self.year_type().kislev_length(), + TEVET => TEVET_LEN, + SHEVAT => SHEVAT_LEN, + ADAR => ADAR_LEN, + NISAN => NISAN_LEN, + IYAR => IYAR_LEN, + SIVAN => SIVAN_LEN, + TAMMUZ => TAMMUZ_LEN, + AV => AV_LEN, + ELUL => ELUL_LEN, _ => { debug_assert!(false, "Got unknown month index {ordinal_month}"); 30 @@ -517,31 +533,33 @@ impl Keviyah { BEFORE_ADAR_DEFAULT_LEN as i16 + i16::from(self.year_type().length_correction()); return corrected as u16; }; - debug_assert!(normalized_ordinal_month <= 12 && normalized_ordinal_month > 0); + debug_assert!(normalized_ordinal_month <= ELUL && normalized_ordinal_month > 0); let year_type = self.year_type(); let mut days = match normalized_ordinal_month { - 1 => 0, - 2 => u16_cvt!(TISHREI_LEN), - 3 => u16_cvt!(TISHREI_LEN) + u16::from(year_type.ḥesvan_length()), + TISHREI => 0, + ḤESVAN => u16_cvt!(TISHREI_LEN), + KISLEV => u16_cvt!(TISHREI_LEN) + u16::from(year_type.ḥesvan_length()), // Use default lengths after this, we'll apply the correction later // (This helps optimize this into a simple jump table) - 4 => u16_cvt!(TISHREI_LEN + ḤESVAN_DEFAULT_LEN + KISLEV_DEFAULT_LEN), - 5 => u16_cvt!(TISHREI_LEN + ḤESVAN_DEFAULT_LEN + KISLEV_DEFAULT_LEN + TEVET_LEN), - 6 => BEFORE_ADAR_DEFAULT_LEN, - 7 => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN), - 8 => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN), - 9 => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYAR_LEN), - 10 => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYAR_LEN + SIVAN_LEN), + TEVET => u16_cvt!(TISHREI_LEN + ḤESVAN_DEFAULT_LEN + KISLEV_DEFAULT_LEN), + SHEVAT => u16_cvt!(TISHREI_LEN + ḤESVAN_DEFAULT_LEN + KISLEV_DEFAULT_LEN + TEVET_LEN), + ADAR => BEFORE_ADAR_DEFAULT_LEN, + NISAN => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN), + IYAR => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN), + SIVAN => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYAR_LEN), + TAMMUZ => { + u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYAR_LEN + SIVAN_LEN) + } #[rustfmt::skip] - 11 => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYAR_LEN + SIVAN_LEN + TAMMUZ_LEN), + AV => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYAR_LEN + SIVAN_LEN + TAMMUZ_LEN), #[rustfmt::skip] _ => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYAR_LEN + SIVAN_LEN + TAMMUZ_LEN + AV_LEN), }; // If it is after Kislev and Ḥesvan, we should add the year correction - if normalized_ordinal_month >= 4 { + if normalized_ordinal_month > KISLEV { // Ensure the casts are fine debug_assert!(days > 1 && year_type.length_correction().abs() <= 1); days = (days as i16 + year_type.length_correction() as i16) as u16; @@ -549,7 +567,7 @@ impl Keviyah { // In a leap year, after Adar (and including Adar II), we should add // the length of Adar 1 - if normalized_ordinal_month >= 6 && self.is_leap() { + if normalized_ordinal_month >= ADAR && self.is_leap() { days += u16::from(ADARI_LEN); } From 2782bae40331f54b3019ad1e76e7ed3e529bde81 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 4 Jan 2024 14:26:33 -0800 Subject: [PATCH 18/24] Remove all book code --- components/calendar/src/hebrew.rs | 69 +++++++++++++++---------------- components/calendar/src/types.rs | 14 +++++++ 2 files changed, 47 insertions(+), 36 deletions(-) diff --git a/components/calendar/src/hebrew.rs b/components/calendar/src/hebrew.rs index f22763ce460..72c0a76621d 100644 --- a/components/calendar/src/hebrew.rs +++ b/components/calendar/src/hebrew.rs @@ -70,9 +70,6 @@ use calendrical_calculations::hebrew_keviyah::{Keviyah, YearInfo}; #[non_exhaustive] // we'll be adding precompiled data to this pub struct Hebrew; -/// The inner date type used for representing [`Date`]s of [`BookHebrew`]. See [`Date`] and [`BookHebrew`] for more details. -#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)] -struct BookHebrewDateInner; /// The inner date type used for representing [`Date`]s of [`Hebrew`]. See [`Date`] and [`Hebrew`] for more details. #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)] pub struct HebrewDateInner(ArithmeticDate); @@ -438,7 +435,8 @@ impl> DateTime { mod tests { use super::*; - use calendrical_calculations::hebrew::*; + use crate::types::MonthCode; + use calendrical_calculations::hebrew_keviyah::*; #[test] fn test_conversions() { @@ -493,7 +491,13 @@ mod tests { Date::try_new_iso_date(2022, 12, 25).unwrap(), ]; - let book_hebrew_dates: [(u8, u8, i32); 48] = [ + // Sentinel value for Adar I + // We're using normalized month values here so that we can use constants. These do not + // distinguish between the different Adars. We add an out-of-range sentinel value of 13 to + // specifically talk about Adar I in a leap year + const ADARI: u8 = 13; + + let hebrew_dates: [(u8, u8, i32); 48] = [ (26, TEVET, 5781), (12, SHEVAT, 5781), (28, SHEVAT, 5781), @@ -501,8 +505,8 @@ mod tests { (26, ADAR, 5781), (12, NISAN, 5781), (28, NISAN, 5781), - (13, IYYAR, 5781), - (28, IYYAR, 5781), + (13, IYAR, 5781), + (28, IYAR, 5781), (14, SIVAN, 5781), (30, SIVAN, 5781), (15, TAMMUZ, 5781), @@ -510,24 +514,24 @@ mod tests { (16, AV, 5781), (2, ELUL, 5781), (17, ELUL, 5781), - (4, TISHRI, 5782), - (19, TISHRI, 5782), - (4, MARHESHVAN, 5782), - (19, MARHESHVAN, 5782), + (4, TISHREI, 5782), + (19, TISHREI, 5782), + (4, ḤESVAN, 5782), + (19, ḤESVAN, 5782), (6, KISLEV, 5782), (21, KISLEV, 5782), (6, TEVET, 5782), (21, TEVET, 5782), (8, SHEVAT, 5782), (23, SHEVAT, 5782), - (9, ADAR, 5782), - (24, ADAR, 5782), - (7, ADARII, 5782), - (22, ADARII, 5782), + (9, ADARI, 5782), + (24, ADARI, 5782), + (7, ADAR, 5782), + (22, ADAR, 5782), (9, NISAN, 5782), (24, NISAN, 5782), - (9, IYYAR, 5782), - (24, IYYAR, 5782), + (9, IYAR, 5782), + (24, IYAR, 5782), (11, SIVAN, 5782), (26, SIVAN, 5782), (11, TAMMUZ, 5782), @@ -536,30 +540,22 @@ mod tests { (28, AV, 5782), (14, ELUL, 5782), (29, ELUL, 5782), - (15, TISHRI, 5783), - (30, TISHRI, 5783), - (16, MARHESHVAN, 5783), + (15, TISHREI, 5783), + (30, TISHREI, 5783), + (16, ḤESVAN, 5783), (1, KISLEV, 5783), (16, KISLEV, 5783), (1, TEVET, 5783), ]; - for (iso_date, book_date_nums) in iso_dates.iter().zip(book_hebrew_dates.iter()) { - // This just checks the integrity of the test data - let book_date = BookHebrew { - year: book_date_nums.2, - month: book_date_nums.1, - day: book_date_nums.0, + for (iso_date, (d, m, y)) in iso_dates.iter().zip(hebrew_dates.into_iter()) { + let m = if m == ADARI { + MonthCode(tinystr!(4, "M05L")) + } else { + MonthCode::new_normal(m).unwrap() }; - - let (y, m, d) = book_date.to_civil_date(); - // let book_from_civil = BookHebrew::from_civil_date(civil_date_nums.2, civil_date_nums.1, civil_date_nums.1); - // assert_eq!(book_date, book_from_civil); - - let hy = HebrewYearInfo::compute(y); - let hebrew_date: HebrewDateInner = - HebrewDateInner(ArithmeticDate::new_unchecked_with_info(y, m, d, hy)); - let hebrew_date = Date::from_raw(hebrew_date, Hebrew); + let hebrew_date = Date::try_new_from_codes(tinystr!(16, "am").into(), y, m, d, Hebrew) + .expect("Date should parse"); let iso_to_hebrew = iso_date.to_calendar(Hebrew); @@ -578,6 +574,7 @@ mod tests { #[test] fn test_icu_bug_22441() { - assert_eq!(BookHebrew::days_in_book_hebrew_year(88369), 383); + let yi = YearInfo::compute_for(88369); + assert_eq!(yi.keviyah.year_length(), 383); } } diff --git a/components/calendar/src/types.rs b/components/calendar/src/types.rs index af656406dda..a70b9b0e397 100644 --- a/components/calendar/src/types.rs +++ b/components/calendar/src/types.rs @@ -137,6 +137,20 @@ impl MonthCode { } None } + + /// Construct a "normal" month code given a number ("Mxx"). + /// + /// Returns an error for months greater than 99 + pub fn new_normal(number: u8) -> Option { + let tens = number / 10; + let ones = number % 10; + if tens > 9 { + return None; + } + + let bytes = [b'M', b'0' + tens, b'0' + ones, 0]; + Some(MonthCode(TinyAsciiStr::try_from_raw(bytes).ok()?)) + } } #[test] From d8013c1088c974e22df9b07d5705e0b3811417a6 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 4 Jan 2024 14:28:51 -0800 Subject: [PATCH 19/24] Use consistent spelling of month names --- components/calendar/src/hebrew.rs | 14 +++--- .../src/hebrew_keviyah.rs | 48 +++++++++---------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/components/calendar/src/hebrew.rs b/components/calendar/src/hebrew.rs index 72c0a76621d..7b07cb1728a 100644 --- a/components/calendar/src/hebrew.rs +++ b/components/calendar/src/hebrew.rs @@ -505,8 +505,8 @@ mod tests { (26, ADAR, 5781), (12, NISAN, 5781), (28, NISAN, 5781), - (13, IYAR, 5781), - (28, IYAR, 5781), + (13, IYYAR, 5781), + (28, IYYAR, 5781), (14, SIVAN, 5781), (30, SIVAN, 5781), (15, TAMMUZ, 5781), @@ -516,8 +516,8 @@ mod tests { (17, ELUL, 5781), (4, TISHREI, 5782), (19, TISHREI, 5782), - (4, ḤESVAN, 5782), - (19, ḤESVAN, 5782), + (4, ḤESHVAN, 5782), + (19, ḤESHVAN, 5782), (6, KISLEV, 5782), (21, KISLEV, 5782), (6, TEVET, 5782), @@ -530,8 +530,8 @@ mod tests { (22, ADAR, 5782), (9, NISAN, 5782), (24, NISAN, 5782), - (9, IYAR, 5782), - (24, IYAR, 5782), + (9, IYYAR, 5782), + (24, IYYAR, 5782), (11, SIVAN, 5782), (26, SIVAN, 5782), (11, TAMMUZ, 5782), @@ -542,7 +542,7 @@ mod tests { (29, ELUL, 5782), (15, TISHREI, 5783), (30, TISHREI, 5783), - (16, ḤESVAN, 5783), + (16, ḤESHVAN, 5783), (1, KISLEV, 5783), (16, KISLEV, 5783), (1, TEVET, 5783), diff --git a/utils/calendrical_calculations/src/hebrew_keviyah.rs b/utils/calendrical_calculations/src/hebrew_keviyah.rs index 4457c26fbad..7cbce9dd3aa 100644 --- a/utils/calendrical_calculations/src/hebrew_keviyah.rs +++ b/utils/calendrical_calculations/src/hebrew_keviyah.rs @@ -118,7 +118,7 @@ const HEBREW_CALENDAR_EPOCH: RataDie = crate::julian::fixed_from_julian_book_ver // Month lengths (ref: https://en.wikipedia.org/wiki/Hebrew_calendar#Months) const TISHREI_LEN: u8 = 30; // except in Complete years -const ḤESVAN_DEFAULT_LEN: u8 = 29; +const ḤESHVAN_DEFAULT_LEN: u8 = 29; // Except in Deficient years const KISLEV_DEFAULT_LEN: u8 = 30; const TEVET_LEN: u8 = 29; @@ -126,7 +126,7 @@ const SHEVAT_LEN: u8 = 30; const ADARI_LEN: u8 = 30; const ADAR_LEN: u8 = 29; const NISAN_LEN: u8 = 30; -const IYAR_LEN: u8 = 29; +const IYYAR_LEN: u8 = 29; const SIVAN_LEN: u8 = 30; const TAMMUZ_LEN: u8 = 29; const AV_LEN: u8 = 30; @@ -138,7 +138,7 @@ const ELUL_LEN: u8 = 29; /// Adar, Adar I and Adar II all normalize to 6 pub const TISHREI: u8 = 1; /// Normalized month constant (see [`TISHREI`]) -pub const ḤESVAN: u8 = 2; +pub const ḤESHVAN: u8 = 2; /// Normalized month constant (see [`TISHREI`]) pub const KISLEV: u8 = 3; /// Normalized month constant (see [`TISHREI`]) @@ -150,7 +150,7 @@ pub const ADAR: u8 = 6; /// Normalized month constant (see [`TISHREI`]) pub const NISAN: u8 = 7; /// Normalized month constant (see [`TISHREI`]) -pub const IYAR: u8 = 8; +pub const IYYAR: u8 = 8; /// Normalized month constant (see [`TISHREI`]) pub const SIVAN: u8 = 9; /// Normalized month constant (see [`TISHREI`]) @@ -360,11 +360,11 @@ pub enum Keviyah { #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] #[allow(clippy::exhaustive_enums)] // This is intrinsic to the calendar pub enum YearType { - /// חסרה: both Ḥesvan and Kislev have 29 days + /// חסרה: both Ḥeshvan and Kislev have 29 days Deficient = -1, - /// כסדרה: Ḥesvan has 29, Kislev has 30 + /// כסדרה: Ḥeshvan has 29, Kislev has 30 Regular = 0, - /// שלמה: both Ḥesvan and Kislev have 30 days + /// שלמה: both Ḥeshvan and Kislev have 30 days Complete = 1, } @@ -374,12 +374,12 @@ impl YearType { self as i8 } - /// The length of Ḥesvan - fn ḥesvan_length(self) -> u8 { + /// The length of Ḥeshvan + fn ḥeshvan_length(self) -> u8 { if self == Self::Complete { - ḤESVAN_DEFAULT_LEN + 1 + ḤESHVAN_DEFAULT_LEN + 1 } else { - ḤESVAN_DEFAULT_LEN + ḤESHVAN_DEFAULT_LEN } } @@ -500,13 +500,13 @@ impl Keviyah { debug_assert!(normalized_ordinal_month <= 12 && normalized_ordinal_month > 0); match normalized_ordinal_month { TISHREI => TISHREI_LEN, - ḤESVAN => self.year_type().ḥesvan_length(), + ḤESHVAN => self.year_type().ḥeshvan_length(), KISLEV => self.year_type().kislev_length(), TEVET => TEVET_LEN, SHEVAT => SHEVAT_LEN, ADAR => ADAR_LEN, NISAN => NISAN_LEN, - IYAR => IYAR_LEN, + IYYAR => IYYAR_LEN, SIVAN => SIVAN_LEN, TAMMUZ => TAMMUZ_LEN, AV => AV_LEN, @@ -524,7 +524,7 @@ impl Keviyah { // convenience constant to keep the additions smallish // Number of days before (any) Adar in a regular year const BEFORE_ADAR_DEFAULT_LEN: u16 = u16_cvt!( - TISHREI_LEN + ḤESVAN_DEFAULT_LEN + KISLEV_DEFAULT_LEN + TEVET_LEN + SHEVAT_LEN + TISHREI_LEN + ḤESHVAN_DEFAULT_LEN + KISLEV_DEFAULT_LEN + TEVET_LEN + SHEVAT_LEN ); let Some(normalized_ordinal_month) = self.normalized_ordinal_month(ordinal_month) else { @@ -539,26 +539,26 @@ impl Keviyah { let mut days = match normalized_ordinal_month { TISHREI => 0, - ḤESVAN => u16_cvt!(TISHREI_LEN), - KISLEV => u16_cvt!(TISHREI_LEN) + u16::from(year_type.ḥesvan_length()), + ḤESHVAN => u16_cvt!(TISHREI_LEN), + KISLEV => u16_cvt!(TISHREI_LEN) + u16::from(year_type.ḥeshvan_length()), // Use default lengths after this, we'll apply the correction later // (This helps optimize this into a simple jump table) - TEVET => u16_cvt!(TISHREI_LEN + ḤESVAN_DEFAULT_LEN + KISLEV_DEFAULT_LEN), - SHEVAT => u16_cvt!(TISHREI_LEN + ḤESVAN_DEFAULT_LEN + KISLEV_DEFAULT_LEN + TEVET_LEN), + TEVET => u16_cvt!(TISHREI_LEN + ḤESHVAN_DEFAULT_LEN + KISLEV_DEFAULT_LEN), + SHEVAT => u16_cvt!(TISHREI_LEN + ḤESHVAN_DEFAULT_LEN + KISLEV_DEFAULT_LEN + TEVET_LEN), ADAR => BEFORE_ADAR_DEFAULT_LEN, NISAN => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN), - IYAR => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN), - SIVAN => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYAR_LEN), + IYYAR => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN), + SIVAN => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYYAR_LEN), TAMMUZ => { - u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYAR_LEN + SIVAN_LEN) + u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYYAR_LEN + SIVAN_LEN) } #[rustfmt::skip] - AV => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYAR_LEN + SIVAN_LEN + TAMMUZ_LEN), + AV => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYYAR_LEN + SIVAN_LEN + TAMMUZ_LEN), #[rustfmt::skip] - _ => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYAR_LEN + SIVAN_LEN + TAMMUZ_LEN + AV_LEN), + _ => u16_cvt!(BEFORE_ADAR_DEFAULT_LEN + ADAR_LEN + NISAN_LEN + IYYAR_LEN + SIVAN_LEN + TAMMUZ_LEN + AV_LEN), }; - // If it is after Kislev and Ḥesvan, we should add the year correction + // If it is after Kislev and Ḥeshvan, we should add the year correction if normalized_ordinal_month > KISLEV { // Ensure the casts are fine debug_assert!(days > 1 && year_type.length_correction().abs() <= 1); From c3aefa5e09872a5376c94e5696d2caab2bf0c862 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 4 Jan 2024 18:33:39 -0800 Subject: [PATCH 20/24] merge table --- components/calendar/src/hebrew.rs | 170 +++++++++++------------------- 1 file changed, 60 insertions(+), 110 deletions(-) diff --git a/components/calendar/src/hebrew.rs b/components/calendar/src/hebrew.rs index 7b07cb1728a..5624ece17f6 100644 --- a/components/calendar/src/hebrew.rs +++ b/components/calendar/src/hebrew.rs @@ -438,117 +438,67 @@ mod tests { use crate::types::MonthCode; use calendrical_calculations::hebrew_keviyah::*; + // Sentinel value for Adar I + // We're using normalized month values here so that we can use constants. These do not + // distinguish between the different Adars. We add an out-of-range sentinel value of 13 to + // specifically talk about Adar I in a leap year + const ADARI: u8 = 13; + + const ISO_HEBREW_DATE_PAIRS: [((i32, u8, u8), (u8, u8, i32)); 48] = [ + ((2021, 1, 10), (26, TEVET, 5781)), + ((2021, 1, 25), (12, SHEVAT, 5781)), + ((2021, 2, 10), (28, SHEVAT, 5781)), + ((2021, 2, 25), (13, ADAR, 5781)), + ((2021, 3, 10), (26, ADAR, 5781)), + ((2021, 3, 25), (12, NISAN, 5781)), + ((2021, 4, 10), (28, NISAN, 5781)), + ((2021, 4, 25), (13, IYYAR, 5781)), + ((2021, 5, 10), (28, IYYAR, 5781)), + ((2021, 5, 25), (14, SIVAN, 5781)), + ((2021, 6, 10), (30, SIVAN, 5781)), + ((2021, 6, 25), (15, TAMMUZ, 5781)), + ((2021, 7, 10), (1, AV, 5781)), + ((2021, 7, 25), (16, AV, 5781)), + ((2021, 8, 10), (2, ELUL, 5781)), + ((2021, 8, 25), (17, ELUL, 5781)), + ((2021, 9, 10), (4, TISHREI, 5782)), + ((2021, 9, 25), (19, TISHREI, 5782)), + ((2021, 10, 10), (4, ḤESHVAN, 5782)), + ((2021, 10, 25), (19, ḤESHVAN, 5782)), + ((2021, 11, 10), (6, KISLEV, 5782)), + ((2021, 11, 25), (21, KISLEV, 5782)), + ((2021, 12, 10), (6, TEVET, 5782)), + ((2021, 12, 25), (21, TEVET, 5782)), + ((2022, 1, 10), (8, SHEVAT, 5782)), + ((2022, 1, 25), (23, SHEVAT, 5782)), + ((2022, 2, 10), (9, ADARI, 5782)), + ((2022, 2, 25), (24, ADARI, 5782)), + ((2022, 3, 10), (7, ADAR, 5782)), + ((2022, 3, 25), (22, ADAR, 5782)), + ((2022, 4, 10), (9, NISAN, 5782)), + ((2022, 4, 25), (24, NISAN, 5782)), + ((2022, 5, 10), (9, IYYAR, 5782)), + ((2022, 5, 25), (24, IYYAR, 5782)), + ((2022, 6, 10), (11, SIVAN, 5782)), + ((2022, 6, 25), (26, SIVAN, 5782)), + ((2022, 7, 10), (11, TAMMUZ, 5782)), + ((2022, 7, 25), (26, TAMMUZ, 5782)), + ((2022, 8, 10), (13, AV, 5782)), + ((2022, 8, 25), (28, AV, 5782)), + ((2022, 9, 10), (14, ELUL, 5782)), + ((2022, 9, 25), (29, ELUL, 5782)), + ((2022, 10, 10), (15, TISHREI, 5783)), + ((2022, 10, 25), (30, TISHREI, 5783)), + ((2022, 11, 10), (16, ḤESHVAN, 5783)), + ((2022, 11, 25), (1, KISLEV, 5783)), + ((2022, 12, 10), (16, KISLEV, 5783)), + ((2022, 12, 25), (1, TEVET, 5783)), + ]; + #[test] fn test_conversions() { - let iso_dates: [Date; 48] = [ - Date::try_new_iso_date(2021, 1, 10).unwrap(), - Date::try_new_iso_date(2021, 1, 25).unwrap(), - Date::try_new_iso_date(2021, 2, 10).unwrap(), - Date::try_new_iso_date(2021, 2, 25).unwrap(), - Date::try_new_iso_date(2021, 3, 10).unwrap(), - Date::try_new_iso_date(2021, 3, 25).unwrap(), - Date::try_new_iso_date(2021, 4, 10).unwrap(), - Date::try_new_iso_date(2021, 4, 25).unwrap(), - Date::try_new_iso_date(2021, 5, 10).unwrap(), - Date::try_new_iso_date(2021, 5, 25).unwrap(), - Date::try_new_iso_date(2021, 6, 10).unwrap(), - Date::try_new_iso_date(2021, 6, 25).unwrap(), - Date::try_new_iso_date(2021, 7, 10).unwrap(), - Date::try_new_iso_date(2021, 7, 25).unwrap(), - Date::try_new_iso_date(2021, 8, 10).unwrap(), - Date::try_new_iso_date(2021, 8, 25).unwrap(), - Date::try_new_iso_date(2021, 9, 10).unwrap(), - Date::try_new_iso_date(2021, 9, 25).unwrap(), - Date::try_new_iso_date(2021, 10, 10).unwrap(), - Date::try_new_iso_date(2021, 10, 25).unwrap(), - Date::try_new_iso_date(2021, 11, 10).unwrap(), - Date::try_new_iso_date(2021, 11, 25).unwrap(), - Date::try_new_iso_date(2021, 12, 10).unwrap(), - Date::try_new_iso_date(2021, 12, 25).unwrap(), - Date::try_new_iso_date(2022, 1, 10).unwrap(), - Date::try_new_iso_date(2022, 1, 25).unwrap(), - Date::try_new_iso_date(2022, 2, 10).unwrap(), - Date::try_new_iso_date(2022, 2, 25).unwrap(), - Date::try_new_iso_date(2022, 3, 10).unwrap(), - Date::try_new_iso_date(2022, 3, 25).unwrap(), - Date::try_new_iso_date(2022, 4, 10).unwrap(), - Date::try_new_iso_date(2022, 4, 25).unwrap(), - Date::try_new_iso_date(2022, 5, 10).unwrap(), - Date::try_new_iso_date(2022, 5, 25).unwrap(), - Date::try_new_iso_date(2022, 6, 10).unwrap(), - Date::try_new_iso_date(2022, 6, 25).unwrap(), - Date::try_new_iso_date(2022, 7, 10).unwrap(), - Date::try_new_iso_date(2022, 7, 25).unwrap(), - Date::try_new_iso_date(2022, 8, 10).unwrap(), - Date::try_new_iso_date(2022, 8, 25).unwrap(), - Date::try_new_iso_date(2022, 9, 10).unwrap(), - Date::try_new_iso_date(2022, 9, 25).unwrap(), - Date::try_new_iso_date(2022, 10, 10).unwrap(), - Date::try_new_iso_date(2022, 10, 25).unwrap(), - Date::try_new_iso_date(2022, 11, 10).unwrap(), - Date::try_new_iso_date(2022, 11, 25).unwrap(), - Date::try_new_iso_date(2022, 12, 10).unwrap(), - Date::try_new_iso_date(2022, 12, 25).unwrap(), - ]; - - // Sentinel value for Adar I - // We're using normalized month values here so that we can use constants. These do not - // distinguish between the different Adars. We add an out-of-range sentinel value of 13 to - // specifically talk about Adar I in a leap year - const ADARI: u8 = 13; - - let hebrew_dates: [(u8, u8, i32); 48] = [ - (26, TEVET, 5781), - (12, SHEVAT, 5781), - (28, SHEVAT, 5781), - (13, ADAR, 5781), - (26, ADAR, 5781), - (12, NISAN, 5781), - (28, NISAN, 5781), - (13, IYYAR, 5781), - (28, IYYAR, 5781), - (14, SIVAN, 5781), - (30, SIVAN, 5781), - (15, TAMMUZ, 5781), - (1, AV, 5781), - (16, AV, 5781), - (2, ELUL, 5781), - (17, ELUL, 5781), - (4, TISHREI, 5782), - (19, TISHREI, 5782), - (4, ḤESHVAN, 5782), - (19, ḤESHVAN, 5782), - (6, KISLEV, 5782), - (21, KISLEV, 5782), - (6, TEVET, 5782), - (21, TEVET, 5782), - (8, SHEVAT, 5782), - (23, SHEVAT, 5782), - (9, ADARI, 5782), - (24, ADARI, 5782), - (7, ADAR, 5782), - (22, ADAR, 5782), - (9, NISAN, 5782), - (24, NISAN, 5782), - (9, IYYAR, 5782), - (24, IYYAR, 5782), - (11, SIVAN, 5782), - (26, SIVAN, 5782), - (11, TAMMUZ, 5782), - (26, TAMMUZ, 5782), - (13, AV, 5782), - (28, AV, 5782), - (14, ELUL, 5782), - (29, ELUL, 5782), - (15, TISHREI, 5783), - (30, TISHREI, 5783), - (16, ḤESHVAN, 5783), - (1, KISLEV, 5783), - (16, KISLEV, 5783), - (1, TEVET, 5783), - ]; - - for (iso_date, (d, m, y)) in iso_dates.iter().zip(hebrew_dates.into_iter()) { + for ((iso_y, iso_m, iso_d), (d, m, y)) in ISO_HEBREW_DATE_PAIRS.into_iter() { + let iso_date = Date::try_new_iso_date(iso_y, iso_m, iso_d).unwrap(); let m = if m == ADARI { MonthCode(tinystr!(4, "M05L")) } else { @@ -562,7 +512,7 @@ mod tests { let hebrew_to_iso = hebrew_date.to_calendar(Iso); assert_eq!( - hebrew_to_iso, *iso_date, + hebrew_to_iso, iso_date, "Failed comparing to-ISO value for {hebrew_date:?} => {iso_date:?}" ); assert_eq!( From 0325fb7c8793d7238f71f45acffeae57fbc77604 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 4 Jan 2024 18:37:48 -0800 Subject: [PATCH 21/24] test ordinal construction --- components/calendar/src/hebrew.rs | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/components/calendar/src/hebrew.rs b/components/calendar/src/hebrew.rs index 5624ece17f6..e888499567c 100644 --- a/components/calendar/src/hebrew.rs +++ b/components/calendar/src/hebrew.rs @@ -444,6 +444,8 @@ mod tests { // specifically talk about Adar I in a leap year const ADARI: u8 = 13; + // The leap years used in the tests below + const LEAP_YEARS_IN_TESTS: [i32; 1] = [5782]; const ISO_HEBREW_DATE_PAIRS: [((i32, u8, u8), (u8, u8, i32)); 48] = [ ((2021, 1, 10), (26, TEVET, 5781)), ((2021, 1, 25), (12, SHEVAT, 5781)), @@ -499,13 +501,14 @@ mod tests { fn test_conversions() { for ((iso_y, iso_m, iso_d), (d, m, y)) in ISO_HEBREW_DATE_PAIRS.into_iter() { let iso_date = Date::try_new_iso_date(iso_y, iso_m, iso_d).unwrap(); - let m = if m == ADARI { + let month_code = if m == ADARI { MonthCode(tinystr!(4, "M05L")) } else { MonthCode::new_normal(m).unwrap() }; - let hebrew_date = Date::try_new_from_codes(tinystr!(16, "am").into(), y, m, d, Hebrew) - .expect("Date should parse"); + let hebrew_date = + Date::try_new_from_codes(tinystr!(16, "am").into(), y, month_code, d, Hebrew) + .expect("Date should parse"); let iso_to_hebrew = iso_date.to_calendar(Hebrew); @@ -519,6 +522,25 @@ mod tests { iso_to_hebrew, hebrew_date, "Failed comparing to-hebrew value for {iso_date:?} => {hebrew_date:?}" ); + + let ordinal_month = if LEAP_YEARS_IN_TESTS.contains(&y) { + if m == ADARI { + ADAR + } else if m >= ADAR { + m + 1 + } else { + m + } + } else { + assert!(m != ADARI); + m + }; + + let ordinal_hebrew_date = + Date::try_new_hebrew_date_with_calendar(y, ordinal_month, d, Hebrew) + .expect("Construction of date must succeed"); + + assert_eq!(ordinal_hebrew_date, hebrew_date, "Hebrew date construction from codes and ordinals should work the same for {hebrew_date:?}"); } } From 6b63138a99f45f7d6d11504a70897da54511814d Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 4 Jan 2024 18:39:42 -0800 Subject: [PATCH 22/24] make table uniform --- components/calendar/src/hebrew.rs | 105 +++++++++++++++--------------- 1 file changed, 54 insertions(+), 51 deletions(-) diff --git a/components/calendar/src/hebrew.rs b/components/calendar/src/hebrew.rs index e888499567c..2d895d27221 100644 --- a/components/calendar/src/hebrew.rs +++ b/components/calendar/src/hebrew.rs @@ -444,62 +444,65 @@ mod tests { // specifically talk about Adar I in a leap year const ADARI: u8 = 13; - // The leap years used in the tests below + /// The leap years used in the tests below const LEAP_YEARS_IN_TESTS: [i32; 1] = [5782]; - const ISO_HEBREW_DATE_PAIRS: [((i32, u8, u8), (u8, u8, i32)); 48] = [ - ((2021, 1, 10), (26, TEVET, 5781)), - ((2021, 1, 25), (12, SHEVAT, 5781)), - ((2021, 2, 10), (28, SHEVAT, 5781)), - ((2021, 2, 25), (13, ADAR, 5781)), - ((2021, 3, 10), (26, ADAR, 5781)), - ((2021, 3, 25), (12, NISAN, 5781)), - ((2021, 4, 10), (28, NISAN, 5781)), - ((2021, 4, 25), (13, IYYAR, 5781)), - ((2021, 5, 10), (28, IYYAR, 5781)), - ((2021, 5, 25), (14, SIVAN, 5781)), - ((2021, 6, 10), (30, SIVAN, 5781)), - ((2021, 6, 25), (15, TAMMUZ, 5781)), - ((2021, 7, 10), (1, AV, 5781)), - ((2021, 7, 25), (16, AV, 5781)), - ((2021, 8, 10), (2, ELUL, 5781)), - ((2021, 8, 25), (17, ELUL, 5781)), - ((2021, 9, 10), (4, TISHREI, 5782)), - ((2021, 9, 25), (19, TISHREI, 5782)), - ((2021, 10, 10), (4, ḤESHVAN, 5782)), - ((2021, 10, 25), (19, ḤESHVAN, 5782)), - ((2021, 11, 10), (6, KISLEV, 5782)), - ((2021, 11, 25), (21, KISLEV, 5782)), - ((2021, 12, 10), (6, TEVET, 5782)), - ((2021, 12, 25), (21, TEVET, 5782)), - ((2022, 1, 10), (8, SHEVAT, 5782)), - ((2022, 1, 25), (23, SHEVAT, 5782)), - ((2022, 2, 10), (9, ADARI, 5782)), - ((2022, 2, 25), (24, ADARI, 5782)), - ((2022, 3, 10), (7, ADAR, 5782)), - ((2022, 3, 25), (22, ADAR, 5782)), - ((2022, 4, 10), (9, NISAN, 5782)), - ((2022, 4, 25), (24, NISAN, 5782)), - ((2022, 5, 10), (9, IYYAR, 5782)), - ((2022, 5, 25), (24, IYYAR, 5782)), - ((2022, 6, 10), (11, SIVAN, 5782)), - ((2022, 6, 25), (26, SIVAN, 5782)), - ((2022, 7, 10), (11, TAMMUZ, 5782)), - ((2022, 7, 25), (26, TAMMUZ, 5782)), - ((2022, 8, 10), (13, AV, 5782)), - ((2022, 8, 25), (28, AV, 5782)), - ((2022, 9, 10), (14, ELUL, 5782)), - ((2022, 9, 25), (29, ELUL, 5782)), - ((2022, 10, 10), (15, TISHREI, 5783)), - ((2022, 10, 25), (30, TISHREI, 5783)), - ((2022, 11, 10), (16, ḤESHVAN, 5783)), - ((2022, 11, 25), (1, KISLEV, 5783)), - ((2022, 12, 10), (16, KISLEV, 5783)), - ((2022, 12, 25), (1, TEVET, 5783)), + /// (iso, hebrew) pairs of testcases. If any of the years here + /// are leap years please add them to LEAP_YEARS_IN_TESTS (we have this manually + /// so we don't end up exercising potentially buggy codepaths to test this) + const ISO_HEBREW_DATE_PAIRS: [((i32, u8, u8), (i32, u8, u8)); 48] = [ + ((2021, 1, 10), (5781, TEVET, 26)), + ((2021, 1, 25), (5781, SHEVAT, 12)), + ((2021, 2, 10), (5781, SHEVAT, 28)), + ((2021, 2, 25), (5781, ADAR, 13)), + ((2021, 3, 10), (5781, ADAR, 26)), + ((2021, 3, 25), (5781, NISAN, 12)), + ((2021, 4, 10), (5781, NISAN, 28)), + ((2021, 4, 25), (5781, IYYAR, 13)), + ((2021, 5, 10), (5781, IYYAR, 28)), + ((2021, 5, 25), (5781, SIVAN, 14)), + ((2021, 6, 10), (5781, SIVAN, 30)), + ((2021, 6, 25), (5781, TAMMUZ, 15)), + ((2021, 7, 10), (5781, AV, 1)), + ((2021, 7, 25), (5781, AV, 16)), + ((2021, 8, 10), (5781, ELUL, 2)), + ((2021, 8, 25), (5781, ELUL, 17)), + ((2021, 9, 10), (5782, TISHREI, 4)), + ((2021, 9, 25), (5782, TISHREI, 19)), + ((2021, 10, 10), (5782, ḤESHVAN, 4)), + ((2021, 10, 25), (5782, ḤESHVAN, 19)), + ((2021, 11, 10), (5782, KISLEV, 6)), + ((2021, 11, 25), (5782, KISLEV, 21)), + ((2021, 12, 10), (5782, TEVET, 6)), + ((2021, 12, 25), (5782, TEVET, 21)), + ((2022, 1, 10), (5782, SHEVAT, 8)), + ((2022, 1, 25), (5782, SHEVAT, 23)), + ((2022, 2, 10), (5782, ADARI, 9)), + ((2022, 2, 25), (5782, ADARI, 24)), + ((2022, 3, 10), (5782, ADAR, 7)), + ((2022, 3, 25), (5782, ADAR, 22)), + ((2022, 4, 10), (5782, NISAN, 9)), + ((2022, 4, 25), (5782, NISAN, 24)), + ((2022, 5, 10), (5782, IYYAR, 9)), + ((2022, 5, 25), (5782, IYYAR, 24)), + ((2022, 6, 10), (5782, SIVAN, 11)), + ((2022, 6, 25), (5782, SIVAN, 26)), + ((2022, 7, 10), (5782, TAMMUZ, 11)), + ((2022, 7, 25), (5782, TAMMUZ, 26)), + ((2022, 8, 10), (5782, AV, 13)), + ((2022, 8, 25), (5782, AV, 28)), + ((2022, 9, 10), (5782, ELUL, 14)), + ((2022, 9, 25), (5782, ELUL, 29)), + ((2022, 10, 10), (5783, TISHREI, 15)), + ((2022, 10, 25), (5783, TISHREI, 30)), + ((2022, 11, 10), (5783, ḤESHVAN, 16)), + ((2022, 11, 25), (5783, KISLEV, 1)), + ((2022, 12, 10), (5783, KISLEV, 16)), + ((2022, 12, 25), (5783, TEVET, 1)), ]; #[test] fn test_conversions() { - for ((iso_y, iso_m, iso_d), (d, m, y)) in ISO_HEBREW_DATE_PAIRS.into_iter() { + for ((iso_y, iso_m, iso_d), (y, m, d)) in ISO_HEBREW_DATE_PAIRS.into_iter() { let iso_date = Date::try_new_iso_date(iso_y, iso_m, iso_d).unwrap(); let month_code = if m == ADARI { MonthCode(tinystr!(4, "M05L")) From 81c1e700c652ed36086897ac26e3c97cb74108ba Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 4 Jan 2024 21:35:10 -0800 Subject: [PATCH 23/24] clippy --- components/calendar/src/hebrew.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/components/calendar/src/hebrew.rs b/components/calendar/src/hebrew.rs index 2d895d27221..2382b3d1c87 100644 --- a/components/calendar/src/hebrew.rs +++ b/components/calendar/src/hebrew.rs @@ -449,6 +449,7 @@ mod tests { /// (iso, hebrew) pairs of testcases. If any of the years here /// are leap years please add them to LEAP_YEARS_IN_TESTS (we have this manually /// so we don't end up exercising potentially buggy codepaths to test this) + #[allow(clippy::type_complexity)] const ISO_HEBREW_DATE_PAIRS: [((i32, u8, u8), (i32, u8, u8)); 48] = [ ((2021, 1, 10), (5781, TEVET, 26)), ((2021, 1, 25), (5781, SHEVAT, 12)), From fb1ce603922789cfe886ce8fe217827b31f45d78 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 4 Jan 2024 21:36:21 -0800 Subject: [PATCH 24/24] pubcrate --- components/calendar/src/types.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/calendar/src/types.rs b/components/calendar/src/types.rs index a70b9b0e397..1b0b4547b79 100644 --- a/components/calendar/src/types.rs +++ b/components/calendar/src/types.rs @@ -141,7 +141,8 @@ impl MonthCode { /// Construct a "normal" month code given a number ("Mxx"). /// /// Returns an error for months greater than 99 - pub fn new_normal(number: u8) -> Option { + #[cfg(test)] // Only used in tests for now. Could be made public if people need it. + pub(crate) fn new_normal(number: u8) -> Option { let tens = number / 10; let ones = number % 10; if tens > 9 {