From ce13e6a7937d97792e60f7a93e66faa844df9a9e Mon Sep 17 00:00:00 2001 From: Alan Liu Date: Thu, 5 Mar 2026 19:28:19 -0500 Subject: [PATCH 01/10] Implement stateful `surpasses()` to improve year/month `until()` performance --- .../calendar/src/calendar_arithmetic.rs | 320 ++++++++++++------ 1 file changed, 217 insertions(+), 103 deletions(-) diff --git a/components/calendar/src/calendar_arithmetic.rs b/components/calendar/src/calendar_arithmetic.rs index 4c4e4f981a9..883396a90a9 100644 --- a/components/calendar/src/calendar_arithmetic.rs +++ b/components/calendar/src/calendar_arithmetic.rs @@ -752,87 +752,131 @@ impl ArithmeticDate { sign: i64, cal: &C, ) -> bool { - // 1. Let _parts_ be CalendarISOToDate(_calendar_, _fromIsoDate_). + // NOTE: Numbered comments refer to the Temporal `NonISODateSurpasses` spec. + // Because this implementation refactors the algorithm into stateful components + // to reduce redundant calculations, references to numbered lines of the spec are + // distributed across multiple functions. This is intentional and meaningful. + let sign_mul = if duration.is_negative { -1i64 } else { 1i64 }; + let years = i64::from(duration.years) * sign_mul; + let months = i64::from(duration.months) * sign_mul; + + let month_checker = self.surpasses_month_checker(other, years, sign, cal); + if month_checker.surpasses_month(months) { + return true; + } + + // 8. If weeks = 0 and days = 0, return false. + if duration.weeks == 0 && duration.days == 0 { + return false; + } + + let months_added = Self::new_balanced( + month_checker.y0, + months + i64::from(month_checker.m0), + 1, + cal, + ); + let week_day_checker = + self.surpasses_week_day_checker_from_months_added(&month_checker, months_added); + week_day_checker.surpasses_week_day(duration) + } + + /// Prepares a stateful checker for month iteration in surpasses(). + fn surpasses_month_checker<'a>( + &'a self, + other: &'a Self, + years: i64, + sign: i64, + cal: &'a C, + ) -> SurpassesMonthChecker<'a, C> { + // 1. Let parts be CalendarISOToDate(calendar, fromIsoDate). let parts = self; - // 1. Let _calDate2_ be CalendarISOToDate(_calendar_, _toIsoDate_). + // 2. Let calDate2 be CalendarISOToDate(calendar, toIsoDate). let cal_date_2 = other; - // 1. Let _y0_ be _parts_.[[Year]] + _years_. - let y0 = - cal.year_info_from_extended(duration.add_years_to(parts.year().to_extended_year())); - // 1. If CompareSurpasses(_sign_, _y0_, _parts_.[[MonthCode]], _parts_.[[Day]], _calDate2_) is *true*, return *true*. + // 3. Let y0 be parts.[[Year]] + years. + let y0 = cal.year_info_from_extended(parts.year().to_extended_year() + years as i32); let base_month = cal.month_from_ordinal(parts.year(), parts.month()); - if Self::compare_surpasses_lexicographic(sign, y0, base_month, parts.day(), cal_date_2, cal) - { - return true; - } - // 1. Let _m0_ be MonthCodeToOrdinal(_calendar_, _y0_, ! ConstrainMonthCode(_calendar_, _y0_, _parts_.[[MonthCode]], ~constrain~)). let constrain = DateFromFieldsOptions { overflow: Some(Overflow::Constrain), ..Default::default() }; - let m0_result = cal.ordinal_from_month(y0, base_month, constrain); - let m0 = match m0_result { - Ok(m0) => m0, - Err(_) => { - debug_assert!( - false, - "valid month code for calendar, and constrained to the year" - ); - 1 - } - }; - // 1. Let _monthsAdded_ be BalanceNonISODate(_calendar_, _y0_, _m0_ + _months_, 1). - let months_added = Self::new_balanced(y0, duration.add_months_to(m0), 1, cal); + // 5. Let m0 be MonthCodeToOrdinal(calendar, y0, ! ConstrainMonthCode(calendar, y0, parts.[[MonthCode]], constrain)). + let m0 = cal + .ordinal_from_month(y0, base_month, constrain) + .unwrap_or(1); - // 1. If CompareSurpasses(_sign_, _monthsAdded_.[[Year]], _monthsAdded_.[[Month]], _parts_.[[Day]], _calDate2_) is *true*, return *true*. - if Self::compare_surpasses_ordinal( + // 7. If CompareSurpasses(sign, monthsAdded.[[Year]], monthsAdded.[[Month]], parts.[[Day]], calDate2) is true, return true. + let lexicographic_surpasses = Self::compare_surpasses_lexicographic( sign, - months_added.year, - months_added.ordinal_month, + y0, + base_month, parts.day(), cal_date_2, - ) { - return true; - } - // 1. If _weeks_ = 0 and _days_ = 0, return *false*. - if duration.weeks == 0 && duration.days == 0 { - return false; + cal, + ); + + SurpassesMonthChecker { + parts, + cal_date_2, + y0, + m0, + lexicographic_surpasses, + sign, + cal, } - // 1. Let _endOfMonth_ be BalanceNonISODate(_calendar_, _monthsAdded_.[[Year]], _monthsAdded_.[[Month]] + 1, 0). + } + + /// Prepares a stateful checker for week and day iteration in surpasses(). + fn surpasses_week_day_checker<'a>( + &'a self, + other: &'a Self, + years: i64, + months: i64, + sign: i64, + cal: &'a C, + ) -> SurpassesWeekDayChecker<'a, C> { + let month_checker = self.surpasses_month_checker(other, years, sign, cal); + // 6. Let monthsAdded be BalanceNonISODate(calendar, y0, m0 + months, 1). + let months_added = Self::new_balanced( + month_checker.y0, + months + i64::from(month_checker.m0), + 1, + cal, + ); + self.surpasses_week_day_checker_from_months_added(&month_checker, months_added) + } + + /// Prepares a checker for week and day iteration from an existing months_added date. + fn surpasses_week_day_checker_from_months_added<'a>( + &'a self, + month_checker: &SurpassesMonthChecker<'a, C>, + months_added: UncheckedArithmeticDate, + ) -> SurpassesWeekDayChecker<'a, C> { + // 9. Let endOfMonth be BalanceNonISODate(calendar, monthsAdded.[[Year]], monthsAdded.[[Month]] + 1, 0). let end_of_month = Self::new_balanced( months_added.year, i64::from(months_added.ordinal_month) + 1, 0, - cal, + month_checker.cal, ); - // 1. Let _baseDay_ be _parts_.[[Day]]. - let base_day = parts.day(); - // 1. If _baseDay_ ≤ _endOfMonth_.[[Day]], then - // 1. Let _regulatedDay_ be _baseDay_. - // 1. Else, - // 1. Let _regulatedDay_ be _endOfMonth_.[[Day]]. + // 10. Let baseDay be parts.[[Day]]. + let base_day = self.day(); + // 11. If baseDay ≤ endOfMonth.[[Day]], then + // a. Let regulatedDay be baseDay. + // 12. Else, + // a. Let regulatedDay be endOfMonth.[[Day]]. let regulated_day = if base_day < end_of_month.day { base_day } else { end_of_month.day }; - // 1. Let _daysInWeek_ be 7 (the number of days in a week for all supported calendars). - // 1. Let _balancedDate_ be BalanceNonISODate(_calendar_, _endOfMonth_.[[Year]], _endOfMonth_.[[Month]], _regulatedDay_ + _daysInWeek_ * _weeks_ + _days_). - // 1. Return CompareSurpasses(_sign_, _balancedDate_.[[Year]], _balancedDate_.[[Month]], _balancedDate_.[[Day]], _calDate2_). - let balanced_date = Self::new_balanced( - end_of_month.year, - i64::from(end_of_month.ordinal_month), - duration.add_weeks_and_days_to(regulated_day), - cal, - ); - - Self::compare_surpasses_ordinal( - sign, - balanced_date.year, - balanced_date.ordinal_month, - balanced_date.day, - cal_date_2, - ) + SurpassesWeekDayChecker { + cal_date_2: month_checker.cal_date_2, + end_of_month, + regulated_day, + sign: month_checker.sign, + cal: month_checker.cal, + } } /// Implements the Temporal abstract operation `NonISODateAdd`. @@ -950,8 +994,8 @@ impl ArithmeticDate { } } - // 1. Let _sign_ be -1 × CompareISODate(_one_, _two_). - // 1. If _sign_ = 0, return ZeroDateDuration(). + // 1. Let sign be -1 × CompareISODate(one, two). + // 2. If sign = 0, return ZeroDateDuration(). let sign = match other.cmp(self) { Ordering::Greater => 1i64, Ordering::Equal => return DateDuration::default(), @@ -976,12 +1020,12 @@ impl ArithmeticDate { cal, )); - // 1. Let _years_ be 0. - // 1. If _largestUnit_ is ~year~, then - // 1. Let _candidateYears_ be _sign_. - // 1. Repeat, while NonISODateSurpasses(_calendar_, _sign_, _one_, _two_, _candidateYears_, 0, 0, 0) is *false*, - // 1. Set _years_ to _candidateYears_. - // 1. Set _candidateYears_ to _candidateYears_ + _sign_. + // 3. Let years be 0. + // 4. If largestUnit is year, then + // a. Let candidateYears be sign. + // b. Repeat, while NonISODateSurpasses(calendar, sign, one, two, candidateYears, 0, 0, 0) is false, + // i. Set years to candidateYears. + // ii. Set candidateYears to candidateYears + sign. let mut years = 0; if matches!(options.largest_unit, Some(DateDurationUnit::Years)) { @@ -1004,12 +1048,12 @@ impl ArithmeticDate { } } - // 1. Let _months_ be 0. - // 1. If _largestUnit_ is ~year~ or _largestUnit_ is ~month~, then - // 1. Let _candidateMonths_ be _sign_. - // 1. Repeat, while NonISODateSurpasses(_calendar_, _sign_, _one_, _two_, _years_, _candidateMonths_, 0, 0) is *false*, - // 1. Set _months_ to _candidateMonths_. - // 1. Set _candidateMonths_ to _candidateMonths_ + _sign_. + // 5. Let months be 0. + // 6. If largestUnit is year or largestUnit is month, then + // a. Let candidateMonths be sign. + // b. Repeat, while NonISODateSurpasses(calendar, sign, one, two, years, candidateMonths, 0, 0) is false, + // i. Set months to candidateMonths. + // ii. Set candidateMonths to candidateMonths + sign. let mut months = 0; if matches!( options.largest_unit, @@ -1032,61 +1076,131 @@ impl ArithmeticDate { candidate_months = min_months } - while !self.surpasses( - other, - DateDuration::from_signed_ymwd(years, candidate_months, 0, 0), - sign, - cal, - ) { + let checker = self.surpasses_month_checker(other, years, sign, cal); + while !checker.surpasses_month(candidate_months) { months = candidate_months; candidate_months += sign; } } - // 1. Let _weeks_ be 0. - // 1. If _largestUnit_ is ~week~, then - // 1. Let _candidateWeeks_ be _sign_. - // 1. Repeat, while NonISODateSurpasses(_calendar_, _sign_, _one_, _two_, _years_, _months_, _candidateWeeks_, 0) is *false*, - // 1. Set _weeks_ to _candidateWeeks_. - // 1. Set _candidateWeeks_ to _candidateWeeks_ + sign. + // 7. Let weeks be 0. + // 8. If largestUnit is week, then + // a. Let candidateWeeks be sign. + // b. Repeat, while NonISODateSurpasses(calendar, sign, one, two, years, months, candidateWeeks, 0) is false, + // i. Set weeks to candidateWeeks. + // ii. Set candidateWeeks to candidateWeeks + sign. let mut weeks = 0; + let checker = self.surpasses_week_day_checker(other, years, months, sign, cal); if matches!(options.largest_unit, Some(DateDurationUnit::Weeks)) { let mut candidate_weeks = sign; - while !self.surpasses( - other, - DateDuration::from_signed_ymwd(years, months, candidate_weeks, 0), - sign, - cal, - ) { + while !checker.surpasses_week(candidate_weeks) { weeks = candidate_weeks; candidate_weeks += sign; } } - // 1. Let _days_ be 0. - // 1. Let _candidateDays_ be _sign_. - // 1. Repeat, while NonISODateSurpasses(_calendar_, _sign_, _one_, _two_, _years_, _months_, _weeks_, _candidateDays_) is *false*, - // 1. Set _days_ to _candidateDays_. - // 1. Set _candidateDays_ to _candidateDays_ + _sign_. + // 9. Let days be 0. + // 10. Let candidateDays be sign. + // 11. Repeat, while NonISODateSurpasses(calendar, sign, one, two, years, months, weeks, candidateDays) is false, + // a. Set days to candidateDays. + // b. Set candidateDays to candidateDays + sign. let mut days = 0; // There is no pressing need to optimize candidate_days here: the early-return RD arithmetic // optimization will be hit if the largest_unit is weeks/days, and if it is months or years we will // arrive here with a candidate date that is at most 31 days off. We can run this loop 31 times. let mut candidate_days = sign; - while !self.surpasses( - other, - DateDuration::from_signed_ymwd(years, months, weeks, candidate_days), - sign, - cal, - ) { + while !checker.surpasses_day(weeks, candidate_days) { days = candidate_days; candidate_days += sign; } - // 1. Return ! CreateDateDurationRecord(_years_, _months_, _weeks_, _days_). + // 12. Return ! CreateDateDurationRecord(years, months, weeks, days). DateDuration::from_signed_ymwd(years, months, weeks, days) } } +/// Stateful checker for month iteration in surpasses(). +/// +/// By saving intermediary computations based on a fixed year, +/// only the computations relating to the month are done. The week +/// and day are expected to be zero. +struct SurpassesMonthChecker<'a, C: DateFieldsResolver> { + parts: &'a ArithmeticDate, + cal_date_2: &'a ArithmeticDate, + y0: C::YearInfo, + m0: u8, + lexicographic_surpasses: bool, + sign: i64, + cal: &'a C, +} + +impl<'a, C: DateFieldsResolver> SurpassesMonthChecker<'a, C> { + fn surpasses_month(&self, months: i64) -> bool { + // 4. If CompareSurpasses(sign, y0, parts.[[MonthCode]], parts.[[Day]], calDate2) is true, return true. + if self.lexicographic_surpasses { + return true; + } + // 6. Let monthsAdded be BalanceNonISODate(calendar, y0, m0 + months, 1). + let months_added = + ArithmeticDate::::new_balanced(self.y0, months + i64::from(self.m0), 1, self.cal); + + // 7. If CompareSurpasses(sign, monthsAdded.[[Year]], monthsAdded.[[Month]], parts.[[Day]], calDate2) is true, return true. + ArithmeticDate::::compare_surpasses_ordinal( + self.sign, + months_added.year, + months_added.ordinal_month, + self.parts.day(), + self.cal_date_2, + ) + } +} + +/// Stateful checker for week and day iteration in surpasses(). +/// +/// By saving intermediary computations based on a fixed year and month, +/// only the computations relating to the week and day are done. +struct SurpassesWeekDayChecker<'a, C: DateFieldsResolver> { + cal_date_2: &'a ArithmeticDate, + end_of_month: UncheckedArithmeticDate, + regulated_day: u8, + sign: i64, + cal: &'a C, +} + +impl<'a, C: DateFieldsResolver> SurpassesWeekDayChecker<'a, C> { + fn surpasses_balanced_day(&self, balanced_day: i64) -> bool { + // 14. Let balancedDate be BalanceNonISODate(calendar, endOfMonth.[[Year]], endOfMonth.[[Month]], regulatedDay + daysInWeek * weeks + days). + let balanced_date = ArithmeticDate::::new_balanced( + self.end_of_month.year, + i64::from(self.end_of_month.ordinal_month), + balanced_day, + self.cal, + ); + + // 15. Return CompareSurpasses(sign, balancedDate.[[Year]], balancedDate.[[Month]], balancedDate.[[Day]], calDate2). + ArithmeticDate::::compare_surpasses_ordinal( + self.sign, + balanced_date.year, + balanced_date.ordinal_month, + balanced_date.day, + self.cal_date_2, + ) + } + + fn surpasses_week(&self, weeks: i64) -> bool { + // 13. Let daysInWeek be 7 (the number of days in a week for all supported calendars). + self.surpasses_balanced_day(7 * weeks + i64::from(self.regulated_day)) + } + + fn surpasses_day(&self, weeks: i64, days: i64) -> bool { + // 13. Let daysInWeek be 7 (the number of days in a week for all supported calendars). + self.surpasses_balanced_day(7 * weeks + days + i64::from(self.regulated_day)) + } + + fn surpasses_week_day(&self, duration: DateDuration) -> bool { + self.surpasses_balanced_day(duration.add_weeks_and_days_to(self.regulated_day)) + } +} + #[cfg(test)] mod tests { use super::*; From 64b3d140b2792c0a0552cb7d10aa16544bb68ce4 Mon Sep 17 00:00:00 2001 From: Alan Liu Date: Thu, 5 Mar 2026 20:21:14 -0500 Subject: [PATCH 02/10] Clippy insisted on backticks (take 2) --- components/calendar/src/calendar_arithmetic.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/components/calendar/src/calendar_arithmetic.rs b/components/calendar/src/calendar_arithmetic.rs index 883396a90a9..94d0eaeba41 100644 --- a/components/calendar/src/calendar_arithmetic.rs +++ b/components/calendar/src/calendar_arithmetic.rs @@ -781,7 +781,7 @@ impl ArithmeticDate { week_day_checker.surpasses_week_day(duration) } - /// Prepares a stateful checker for month iteration in surpasses(). + /// Prepares a stateful checker for month iteration in `surpasses()`. fn surpasses_month_checker<'a>( &'a self, other: &'a Self, @@ -826,7 +826,7 @@ impl ArithmeticDate { } } - /// Prepares a stateful checker for week and day iteration in surpasses(). + /// Prepares a stateful checker for week and day iteration in `surpasses()`. fn surpasses_week_day_checker<'a>( &'a self, other: &'a Self, @@ -846,7 +846,7 @@ impl ArithmeticDate { self.surpasses_week_day_checker_from_months_added(&month_checker, months_added) } - /// Prepares a checker for week and day iteration from an existing months_added date. + /// Prepares a checker for week and day iteration from an existing `months_added` date. fn surpasses_week_day_checker_from_months_added<'a>( &'a self, month_checker: &SurpassesMonthChecker<'a, C>, @@ -1118,7 +1118,7 @@ impl ArithmeticDate { } } -/// Stateful checker for month iteration in surpasses(). +/// Stateful checker for month iteration in `surpasses()`. /// /// By saving intermediary computations based on a fixed year, /// only the computations relating to the month are done. The week @@ -1154,7 +1154,7 @@ impl<'a, C: DateFieldsResolver> SurpassesMonthChecker<'a, C> { } } -/// Stateful checker for week and day iteration in surpasses(). +/// Stateful checker for week and day iteration in `surpasses()`. /// /// By saving intermediary computations based on a fixed year and month, /// only the computations relating to the week and day are done. From 004128c9cb5da2e07f6bb69dcdae185e2067d998 Mon Sep 17 00:00:00 2001 From: Alan Liu Date: Tue, 10 Mar 2026 20:21:48 -0400 Subject: [PATCH 03/10] Cleanup: Add closing backticks in deprecated messages --- .../calendar/src/cal/east_asian_traditional.rs | 12 ++++++------ components/calendar/src/cal/hijri.rs | 4 ++-- components/calendar/src/date.rs | 6 +++--- components/calendar/src/types.rs | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/components/calendar/src/cal/east_asian_traditional.rs b/components/calendar/src/cal/east_asian_traditional.rs index cee3d958f59..71faf953063 100644 --- a/components/calendar/src/cal/east_asian_traditional.rs +++ b/components/calendar/src/cal/east_asian_traditional.rs @@ -403,7 +403,7 @@ impl KoreanTraditional { /// Use [`Self::new`]. #[cfg(feature = "serde")] #[doc = icu_provider::gen_buffer_unstable_docs!(BUFFER,Self::new)] - #[deprecated(since = "2.1.0", note = "use `Self::new()")] + #[deprecated(since = "2.1.0", note = "use `Self::new()`")] pub fn try_new_with_buffer_provider( _provider: &(impl BufferProvider + ?Sized), ) -> Result { @@ -412,13 +412,13 @@ impl KoreanTraditional { /// Use [`Self::new`]. #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new)] - #[deprecated(since = "2.1.0", note = "use `Self::new()")] + #[deprecated(since = "2.1.0", note = "use `Self::new()`")] pub fn try_new_unstable(_provider: &D) -> Result { Ok(Self::new()) } /// Use [`Self::new`]. - #[deprecated(since = "2.1.0", note = "use `Self::new()")] + #[deprecated(since = "2.1.0", note = "use `Self::new()`")] pub fn new_always_calculating() -> Self { Self::new() } @@ -550,7 +550,7 @@ impl ChineseTraditional { #[cfg(feature = "serde")] #[doc = icu_provider::gen_buffer_unstable_docs!(BUFFER,Self::new)] - #[deprecated(since = "2.1.0", note = "use `Self::new()")] + #[deprecated(since = "2.1.0", note = "use `Self::new()`")] pub fn try_new_with_buffer_provider( _provider: &(impl BufferProvider + ?Sized), ) -> Result { @@ -558,13 +558,13 @@ impl ChineseTraditional { } #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new)] - #[deprecated(since = "2.1.0", note = "use `Self::new()")] + #[deprecated(since = "2.1.0", note = "use `Self::new()`")] pub fn try_new_unstable(_provider: &D) -> Result { Ok(Self::new()) } /// Use [`Self::new()`]. - #[deprecated(since = "2.1.0", note = "use `Self::new()")] + #[deprecated(since = "2.1.0", note = "use `Self::new()`")] pub fn new_always_calculating() -> Self { Self::new() } diff --git a/components/calendar/src/cal/hijri.rs b/components/calendar/src/cal/hijri.rs index bfe12911ec0..a9e2ca14c61 100644 --- a/components/calendar/src/cal/hijri.rs +++ b/components/calendar/src/cal/hijri.rs @@ -1070,7 +1070,7 @@ impl>, R: Rules> Date { impl Date> { /// Deprecated - #[deprecated(since = "2.1.0", note = "use `Date::try_new_hijri_with_calendar")] + #[deprecated(since = "2.1.0", note = "use `Date::try_new_hijri_with_calendar`")] pub fn try_new_ummalqura(year: i32, month: u8, day: u8) -> Result { Date::try_new_hijri_with_calendar(year, month, day, Hijri::new_umm_al_qura()) } @@ -1078,7 +1078,7 @@ impl Date> { impl>> Date { /// Deprecated - #[deprecated(since = "2.1.0", note = "use `Date::try_new_hijri_with_calendar")] + #[deprecated(since = "2.1.0", note = "use `Date::try_new_hijri_with_calendar`")] pub fn try_new_hijri_tabular_with_calendar( year: i32, month: u8, diff --git a/components/calendar/src/date.rs b/components/calendar/src/date.rs index 708dc84cdc2..2e7f2ff1a33 100644 --- a/components/calendar/src/date.rs +++ b/components/calendar/src/date.rs @@ -271,14 +271,14 @@ impl Date { /// Construct a [`Date`] from an ISO date and a calendar. #[inline] - #[deprecated(since = "2.2.0", note = "use `iso.to_calendar(calendar)")] + #[deprecated(since = "2.2.0", note = "use `iso.to_calendar(calendar)`")] pub fn new_from_iso(iso: Date, calendar: A) -> Self { iso.to_calendar(calendar) } /// Convert the [`Date`] to an ISO Date #[inline] - #[deprecated(since = "2.2.0", note = "use `date.to_calendar(Iso)")] + #[deprecated(since = "2.2.0", note = "use `date.to_calendar(Iso)`")] pub fn to_iso(&self) -> Date { self.to_calendar(Iso) } @@ -320,7 +320,7 @@ impl Date { /// /// This is *not* the day of the week, an ordinal number that is locale /// dependent. - #[deprecated(since = "2.2.0", note = "use `Date::weekday")] + #[deprecated(since = "2.2.0", note = "use `Date::weekday`")] pub fn day_of_week(&self) -> types::Weekday { self.to_rata_die().into() } diff --git a/components/calendar/src/types.rs b/components/calendar/src/types.rs index 3c434185215..6eb0c0da3f4 100644 --- a/components/calendar/src/types.rs +++ b/components/calendar/src/types.rs @@ -676,7 +676,7 @@ pub struct MonthInfo { pub(crate) leap_status: LeapStatus, /// The [`Month::code()`] of [`Self::to_input`]. - #[deprecated(since = "2.2.0", note = "use `to_input().code()")] + #[deprecated(since = "2.2.0", note = "use `to_input().code()`")] pub standard_code: MonthCode, /// Deprecated From f7dccb3ef16c9c114331a3660a55e91bd099cc0e Mon Sep 17 00:00:00 2001 From: Alan Liu Date: Tue, 10 Mar 2026 18:51:17 -0400 Subject: [PATCH 04/10] Improved stateful surpasses() impl that hews closely to the NonISODateSurpasses spec This is not ready to go, but I wanted to get some feedback. Most of the following will not make sense until one has seen the code. - The priority here is to stick to the spec, and avoid recalculation. There are some clumsy aspects, like the need for the caller to invoke the checker in a specific way: 1. Large fields to small fields. 2. Before moving from field A to field B, make a final call with the computed value of field A, to set the internal state correctly. 3. Caller maintains their own y/m/w/d. I tried to work around #2 and #3, but couldn't come up with anything clean. It seems like the current approach is a good compromise. - For the debug assertions, clippy complained that I was calling a mut function. This is actually ok, because the object is reset to a new state immediately afterwards. The workaround to the clippy warning is expensive, but should be ok because it's just for debug. I tried other approaches but didn't get anything else to work. Adding an "allow" directive didn't work because debug_assert! is a macro. - There is now no in-library usage of surpasses(). All the testing for that was via until() as far as I can tell. The work item is to add test coverage for surpasses(), and once that looks good, I will refactor surpasses() to use the stateful checker, to optimize performance and eliminate duplicate code. --- .../calendar/src/calendar_arithmetic.rs | 405 ++++++++++-------- 1 file changed, 235 insertions(+), 170 deletions(-) diff --git a/components/calendar/src/calendar_arithmetic.rs b/components/calendar/src/calendar_arithmetic.rs index 94d0eaeba41..b553cef9e24 100644 --- a/components/calendar/src/calendar_arithmetic.rs +++ b/components/calendar/src/calendar_arithmetic.rs @@ -244,8 +244,7 @@ pub(crate) trait DateFieldsResolver: Calendar { /// The default impl is for non-lunisolar calendars with 12 months! /// /// `until()` will debug assert if this ever returns a value greater than the - /// month diff betweeen two dates as a Guarantee. If such a value is returned as a Guess, - /// it will simply be slow + /// month diff betweeen two dates. #[inline] fn min_months_from(_start: Self::YearInfo, years: i64) -> i64 { 12 * years @@ -752,131 +751,87 @@ impl ArithmeticDate { sign: i64, cal: &C, ) -> bool { - // NOTE: Numbered comments refer to the Temporal `NonISODateSurpasses` spec. - // Because this implementation refactors the algorithm into stateful components - // to reduce redundant calculations, references to numbered lines of the spec are - // distributed across multiple functions. This is intentional and meaningful. - let sign_mul = if duration.is_negative { -1i64 } else { 1i64 }; - let years = i64::from(duration.years) * sign_mul; - let months = i64::from(duration.months) * sign_mul; - - let month_checker = self.surpasses_month_checker(other, years, sign, cal); - if month_checker.surpasses_month(months) { - return true; - } - - // 8. If weeks = 0 and days = 0, return false. - if duration.weeks == 0 && duration.days == 0 { - return false; - } - - let months_added = Self::new_balanced( - month_checker.y0, - months + i64::from(month_checker.m0), - 1, - cal, - ); - let week_day_checker = - self.surpasses_week_day_checker_from_months_added(&month_checker, months_added); - week_day_checker.surpasses_week_day(duration) - } - - /// Prepares a stateful checker for month iteration in `surpasses()`. - fn surpasses_month_checker<'a>( - &'a self, - other: &'a Self, - years: i64, - sign: i64, - cal: &'a C, - ) -> SurpassesMonthChecker<'a, C> { - // 1. Let parts be CalendarISOToDate(calendar, fromIsoDate). + // 1. Let _parts_ be CalendarISOToDate(_calendar_, _fromIsoDate_). let parts = self; - // 2. Let calDate2 be CalendarISOToDate(calendar, toIsoDate). + // 1. Let _calDate2_ be CalendarISOToDate(_calendar_, _toIsoDate_). let cal_date_2 = other; - // 3. Let y0 be parts.[[Year]] + years. - let y0 = cal.year_info_from_extended(parts.year().to_extended_year() + years as i32); + // 1. Let _y0_ be _parts_.[[Year]] + _years_. + let y0 = + cal.year_info_from_extended(duration.add_years_to(parts.year().to_extended_year())); + // 1. If CompareSurpasses(_sign_, _y0_, _parts_.[[MonthCode]], _parts_.[[Day]], _calDate2_) is *true*, return *true*. let base_month = cal.month_from_ordinal(parts.year(), parts.month()); + if Self::compare_surpasses_lexicographic(sign, y0, base_month, parts.day(), cal_date_2, cal) + { + return true; + } + // 1. Let _m0_ be MonthCodeToOrdinal(_calendar_, _y0_, ! ConstrainMonthCode(_calendar_, _y0_, _parts_.[[MonthCode]], ~constrain~)). let constrain = DateFromFieldsOptions { overflow: Some(Overflow::Constrain), ..Default::default() }; - // 5. Let m0 be MonthCodeToOrdinal(calendar, y0, ! ConstrainMonthCode(calendar, y0, parts.[[MonthCode]], constrain)). - let m0 = cal - .ordinal_from_month(y0, base_month, constrain) - .unwrap_or(1); + let m0_result = cal.ordinal_from_month(y0, base_month, constrain); + let m0 = match m0_result { + Ok(m0) => m0, + Err(_) => { + debug_assert!( + false, + "valid month code for calendar, and constrained to the year" + ); + 1 + } + }; + // 1. Let _monthsAdded_ be BalanceNonISODate(_calendar_, _y0_, _m0_ + _months_, 1). + let months_added = Self::new_balanced(y0, duration.add_months_to(m0), 1, cal); - // 7. If CompareSurpasses(sign, monthsAdded.[[Year]], monthsAdded.[[Month]], parts.[[Day]], calDate2) is true, return true. - let lexicographic_surpasses = Self::compare_surpasses_lexicographic( + // 1. If CompareSurpasses(_sign_, _monthsAdded_.[[Year]], _monthsAdded_.[[Month]], _parts_.[[Day]], _calDate2_) is *true*, return *true*. + if Self::compare_surpasses_ordinal( sign, - y0, - base_month, + months_added.year, + months_added.ordinal_month, parts.day(), cal_date_2, - cal, - ); - - SurpassesMonthChecker { - parts, - cal_date_2, - y0, - m0, - lexicographic_surpasses, - sign, - cal, + ) { + return true; } - } - - /// Prepares a stateful checker for week and day iteration in `surpasses()`. - fn surpasses_week_day_checker<'a>( - &'a self, - other: &'a Self, - years: i64, - months: i64, - sign: i64, - cal: &'a C, - ) -> SurpassesWeekDayChecker<'a, C> { - let month_checker = self.surpasses_month_checker(other, years, sign, cal); - // 6. Let monthsAdded be BalanceNonISODate(calendar, y0, m0 + months, 1). - let months_added = Self::new_balanced( - month_checker.y0, - months + i64::from(month_checker.m0), - 1, - cal, - ); - self.surpasses_week_day_checker_from_months_added(&month_checker, months_added) - } - - /// Prepares a checker for week and day iteration from an existing `months_added` date. - fn surpasses_week_day_checker_from_months_added<'a>( - &'a self, - month_checker: &SurpassesMonthChecker<'a, C>, - months_added: UncheckedArithmeticDate, - ) -> SurpassesWeekDayChecker<'a, C> { - // 9. Let endOfMonth be BalanceNonISODate(calendar, monthsAdded.[[Year]], monthsAdded.[[Month]] + 1, 0). + // 1. If _weeks_ = 0 and _days_ = 0, return *false*. + if duration.weeks == 0 && duration.days == 0 { + return false; + } + // 1. Let _endOfMonth_ be BalanceNonISODate(_calendar_, _monthsAdded_.[[Year]], _monthsAdded_.[[Month]] + 1, 0). let end_of_month = Self::new_balanced( months_added.year, i64::from(months_added.ordinal_month) + 1, 0, - month_checker.cal, + cal, ); - // 10. Let baseDay be parts.[[Day]]. - let base_day = self.day(); - // 11. If baseDay ≤ endOfMonth.[[Day]], then - // a. Let regulatedDay be baseDay. - // 12. Else, - // a. Let regulatedDay be endOfMonth.[[Day]]. + // 1. Let _baseDay_ be _parts_.[[Day]]. + let base_day = parts.day(); + // 1. If _baseDay_ ≤ _endOfMonth_.[[Day]], then + // 1. Let _regulatedDay_ be _baseDay_. + // 1. Else, + // 1. Let _regulatedDay_ be _endOfMonth_.[[Day]]. let regulated_day = if base_day < end_of_month.day { base_day } else { end_of_month.day }; - SurpassesWeekDayChecker { - cal_date_2: month_checker.cal_date_2, - end_of_month, - regulated_day, - sign: month_checker.sign, - cal: month_checker.cal, - } + // 1. Let _daysInWeek_ be 7 (the number of days in a week for all supported calendars). + // 1. Let _balancedDate_ be BalanceNonISODate(_calendar_, _endOfMonth_.[[Year]], _endOfMonth_.[[Month]], _regulatedDay_ + _daysInWeek_ * _weeks_ + _days_). + // 1. Return CompareSurpasses(_sign_, _balancedDate_.[[Year]], _balancedDate_.[[Month]], _balancedDate_.[[Day]], _calDate2_). + let balanced_date = Self::new_balanced( + end_of_month.year, + i64::from(end_of_month.ordinal_month), + duration.add_weeks_and_days_to(regulated_day), + cal, + ); + + Self::compare_surpasses_ordinal( + sign, + balanced_date.year, + balanced_date.ordinal_month, + balanced_date.day, + cal_date_2, + ) } /// Implements the Temporal abstract operation `NonISODateAdd`. @@ -1002,6 +957,13 @@ impl ArithmeticDate { Ordering::Less => -1i64, }; + // Non-spec optimization: Instead of calling a stateless + // NonISODateSurpasses implementation that repeatedly recomputes many + // values, we use a stateful implementation. It caches intermediate + // produced by NonISODateSurpasses and reuses them in subsequent calls, + // to avoid unnecessary recalculations. + let mut surpasses_checker: SurpassesChecker<'_, C> = SurpassesChecker::new(self, other, sign, cal); + // Preparation for non-specced optimization: // We don't want to spend time incrementally bumping it up one year // at a time, so let's pre-guess a year delta that is guaranteed to not @@ -1013,12 +975,13 @@ impl ArithmeticDate { i64::from(year_diff) - sign }; - debug_assert!(!self.surpasses( - other, - DateDuration::from_signed_ymwd(min_years, 0, 0, 0), - sign, - cal, - )); + // clippy rejects: debug_assert!(!surpasses_checker.surpasses_months(min_months)); + #[cfg(debug_assertions)] + { + let mut debug_checker = SurpassesChecker::new(self, other, sign, cal); + let min_years_valid = !debug_checker.surpasses_years(min_years); + debug_assert!(min_years_valid); + } // 3. Let years be 0. // 4. If largestUnit is year, then @@ -1026,7 +989,6 @@ impl ArithmeticDate { // b. Repeat, while NonISODateSurpasses(calendar, sign, one, two, candidateYears, 0, 0, 0) is false, // i. Set years to candidateYears. // ii. Set candidateYears to candidateYears + sign. - let mut years = 0; if matches!(options.largest_unit, Some(DateDurationUnit::Years)) { let mut candidate_years = sign; @@ -1037,16 +999,13 @@ impl ArithmeticDate { candidate_years = min_years }; - while !self.surpasses( - other, - DateDuration::from_signed_ymwd(candidate_years, 0, 0, 0), - sign, - cal, - ) { + while !surpasses_checker.surpasses_years(candidate_years) { years = candidate_years; candidate_years += sign; } } + // Final (or only) call to freeze the checker's Years field state. + surpasses_checker.surpasses_years(years); // 5. Let months be 0. // 6. If largestUnit is year or largestUnit is month, then @@ -1067,21 +1026,25 @@ impl ArithmeticDate { // If largest_unit = Months, then compute the calendar-specific minimum number of // months corresponding to min_years. let min_months = C::min_months_from(self.year(), min_years); - debug_assert!(!self.surpasses( - other, - DateDuration::from_signed_ymwd(years, min_months, 0, 0), - sign, - cal, - )); + // clippy rejects: debug_assert!(!surpasses_checker.surpasses_months(min_months)); + #[cfg(debug_assertions)] + { + let mut debug_checker = SurpassesChecker::new(self, other, sign, cal); + debug_checker.surpasses_years(years); + let min_months_valid = !debug_checker.surpasses_months(min_months); + debug_assert!(min_months_valid); + } candidate_months = min_months } - let checker = self.surpasses_month_checker(other, years, sign, cal); - while !checker.surpasses_month(candidate_months) { + while !surpasses_checker.surpasses_months(candidate_months) { months = candidate_months; candidate_months += sign; } } + // Final (or only) call to freeze the checker's Months field state. + surpasses_checker.surpasses_months(months); + // 7. Let weeks be 0. // 8. If largestUnit is week, then // a. Let candidateWeeks be sign. @@ -1089,14 +1052,15 @@ impl ArithmeticDate { // i. Set weeks to candidateWeeks. // ii. Set candidateWeeks to candidateWeeks + sign. let mut weeks = 0; - let checker = self.surpasses_week_day_checker(other, years, months, sign, cal); if matches!(options.largest_unit, Some(DateDurationUnit::Weeks)) { let mut candidate_weeks = sign; - while !checker.surpasses_week(candidate_weeks) { + while !surpasses_checker.surpasses_weeks(candidate_weeks) { weeks = candidate_weeks; candidate_weeks += sign; } } + // Final (or only) call to freeze the checker's Weeks field state. + surpasses_checker.surpasses_weeks(weeks); // 9. Let days be 0. // 10. Let candidateDays be sign. @@ -1108,75 +1072,165 @@ impl ArithmeticDate { // optimization will be hit if the largest_unit is weeks/days, and if it is months or years we will // arrive here with a candidate date that is at most 31 days off. We can run this loop 31 times. let mut candidate_days = sign; - while !checker.surpasses_day(weeks, candidate_days) { + while !surpasses_checker.surpasses_days(candidate_days) { days = candidate_days; candidate_days += sign; } - // 12. Return ! CreateDateDurationRecord(years, months, weeks, days). + // // 12. Return ! CreateDateDurationRecord(years, months, weeks, days). DateDuration::from_signed_ymwd(years, months, weeks, days) } } -/// Stateful checker for month iteration in `surpasses()`. +/// Stateful checker for iterative per-field `surpasses()` checks /// -/// By saving intermediary computations based on a fixed year, -/// only the computations relating to the month are done. The week -/// and day are expected to be zero. -struct SurpassesMonthChecker<'a, C: DateFieldsResolver> { +/// This checker for `surpasses()` is designed to iteratively evaluate one field +/// at a time, from largest to smallest. At each point, the caller must cache +/// the computed state of a larger field by calling +/// `surpasses_()` before moving on to the next smaller +/// field. +struct SurpassesChecker<'a, C: DateFieldsResolver> { parts: &'a ArithmeticDate, cal_date_2: &'a ArithmeticDate, - y0: C::YearInfo, - m0: u8, - lexicographic_surpasses: bool, sign: i64, cal: &'a C, + y0: ::YearInfo, + m0: u8, + end_of_month: UncheckedArithmeticDate, + regulated_day: u8, + weeks: i64, } -impl<'a, C: DateFieldsResolver> SurpassesMonthChecker<'a, C> { - fn surpasses_month(&self, months: i64) -> bool { - // 4. If CompareSurpasses(sign, y0, parts.[[MonthCode]], parts.[[Day]], calDate2) is true, return true. - if self.lexicographic_surpasses { - return true; +impl<'a, C: DateFieldsResolver> SurpassesChecker<'a, C> { + fn new( + parts: &'a ArithmeticDate, + cal_date_2: &'a ArithmeticDate, + sign: i64, + cal: &'a C, + ) -> Self { + Self { + parts, + cal_date_2, + sign, + cal, + y0: cal.year_info_from_extended(0), + m0: 0, + end_of_month: UncheckedArithmeticDate { + year: cal.year_info_from_extended(0), + ordinal_month: 0, + day: 0, + }, + regulated_day: 0, + weeks: 0, } + } + + // NOTE: Numbered comments refer to the Temporal `NonISODateSurpasses` + // spec. A design goal of this code is to mirror the spec as closely as + // possible. This dictates the names of variables and the sequence of + // operations. + + fn surpasses_years(&mut self, years: i64) -> bool { + // 1. Let parts be CalendarISOToDate(calendar, fromIsoDate). + // 2. Let calDate2 be CalendarISOToDate(calendar, toIsoDate). + // 3. Let y0 be parts.[[Year]] + years. + self.y0 = self + .cal + .year_info_from_extended(self.parts.year().to_extended_year() + years as i32); + // 4. If CompareSurpasses(sign, y0, parts.[[MonthCode]], parts.[[Day]], calDate2) is true, return true. + let base_month = self + .cal + .month_from_ordinal(self.parts.year(), self.parts.month()); + let constrain = DateFromFieldsOptions { + overflow: Some(Overflow::Constrain), + ..Default::default() + }; + // 5. Let m0 be MonthCodeToOrdinal(calendar, y0, ! ConstrainMonthCode(calendar, y0, parts.[[MonthCode]], constrain)). + let m0_result = self.cal.ordinal_from_month(self.y0, base_month, constrain); + self.m0 = match m0_result { + Ok(m0) => m0, + Err(_) => { + debug_assert!( + false, + "valid month code for calendar, and constrained to the year" + ); + 1 + } + }; + + let surpasses_years = ArithmeticDate::::compare_surpasses_lexicographic( + self.sign, + self.y0, + base_month, + self.parts.day(), + self.cal_date_2, + self.cal, + ); + // Because of leap months in the Hebrew calendar, year checks must also include + // an ordinal test of the month. This handles cases where a date in Adar I (M05L) + // in a leap year (e.g., 5784) is compared to a date in Adar (M06) in a + // non-leap year (e.g., 5785). Since lexicographical order (M05L < M06) differs + // from temporal order when constrained to a non-leap year (where both map to + // ordinal month 6), a lexicographical-only check fails to detect that a later + // day in Adar I actually surpasses an earlier day in Adar. This ordinal check + // ensures the year counter correctly stays at 0 instead of over-incrementing to 1. + surpasses_years || self.surpasses_months(0) + } + + fn surpasses_months(&mut self, months: i64) -> bool { // 6. Let monthsAdded be BalanceNonISODate(calendar, y0, m0 + months, 1). let months_added = ArithmeticDate::::new_balanced(self.y0, months + i64::from(self.m0), 1, self.cal); // 7. If CompareSurpasses(sign, monthsAdded.[[Year]], monthsAdded.[[Month]], parts.[[Day]], calDate2) is true, return true. - ArithmeticDate::::compare_surpasses_ordinal( + let surpasses = ArithmeticDate::::compare_surpasses_ordinal( self.sign, months_added.year, months_added.ordinal_month, self.parts.day(), self.cal_date_2, - ) + ); + + // 9. Let endOfMonth be BalanceNonISODate(calendar, monthsAdded.[[Year]], monthsAdded.[[Month]] + 1, 0). + self.end_of_month = ArithmeticDate::::new_balanced( + months_added.year, + i64::from(months_added.ordinal_month) + 1, + 0, + self.cal, + ); + // 10. Let baseDay be parts.[[Day]]. + let base_day = self.parts.day(); + // 11. If baseDay ≤ endOfMonth.[[Day]], then + // a. Let regulatedDay be baseDay. + // 12. Else, + // a. Let regulatedDay be endOfMonth.[[Day]]. + self.regulated_day = if base_day < self.end_of_month.day { + base_day + } else { + self.end_of_month.day + }; + + surpasses } -} -/// Stateful checker for week and day iteration in `surpasses()`. -/// -/// By saving intermediary computations based on a fixed year and month, -/// only the computations relating to the week and day are done. -struct SurpassesWeekDayChecker<'a, C: DateFieldsResolver> { - cal_date_2: &'a ArithmeticDate, - end_of_month: UncheckedArithmeticDate, - regulated_day: u8, - sign: i64, - cal: &'a C, -} + fn surpasses_weeks(&mut self, weeks: i64) -> bool { + // 8. If weeks = 0 (*and days = 0*), return false. + if weeks == 0 { + return false; + } -impl<'a, C: DateFieldsResolver> SurpassesWeekDayChecker<'a, C> { - fn surpasses_balanced_day(&self, balanced_day: i64) -> bool { + // 13. Let daysInWeek be 7 (the number of days in a week for all supported calendars). // 14. Let balancedDate be BalanceNonISODate(calendar, endOfMonth.[[Year]], endOfMonth.[[Month]], regulatedDay + daysInWeek * weeks + days). + // 15. Return CompareSurpasses(sign, balancedDate.[[Year]], balancedDate.[[Month]], balancedDate.[[Day]], calDate2). let balanced_date = ArithmeticDate::::new_balanced( self.end_of_month.year, i64::from(self.end_of_month.ordinal_month), - balanced_day, + 7 * weeks + i64::from(self.regulated_day), self.cal, ); - // 15. Return CompareSurpasses(sign, balancedDate.[[Year]], balancedDate.[[Month]], balancedDate.[[Day]], calDate2). + self.weeks = weeks; + ArithmeticDate::::compare_surpasses_ordinal( self.sign, balanced_date.year, @@ -1186,18 +1240,29 @@ impl<'a, C: DateFieldsResolver> SurpassesWeekDayChecker<'a, C> { ) } - fn surpasses_week(&self, weeks: i64) -> bool { - // 13. Let daysInWeek be 7 (the number of days in a week for all supported calendars). - self.surpasses_balanced_day(7 * weeks + i64::from(self.regulated_day)) - } + fn surpasses_days(&mut self, days: i64) -> bool { + // 8. If weeks = 0 and days = 0, return false. + if self.weeks == 0 && days == 0 { + return false; + } - fn surpasses_day(&self, weeks: i64, days: i64) -> bool { // 13. Let daysInWeek be 7 (the number of days in a week for all supported calendars). - self.surpasses_balanced_day(7 * weeks + days + i64::from(self.regulated_day)) - } + // 14. Let balancedDate be BalanceNonISODate(calendar, endOfMonth.[[Year]], endOfMonth.[[Month]], regulatedDay + daysInWeek * weeks + days). + // 15. Return CompareSurpasses(sign, balancedDate.[[Year]], balancedDate.[[Month]], balancedDate.[[Day]], calDate2). + let balanced_date = ArithmeticDate::::new_balanced( + self.end_of_month.year, + i64::from(self.end_of_month.ordinal_month), + 7 * self.weeks + days + i64::from(self.regulated_day), + self.cal, + ); - fn surpasses_week_day(&self, duration: DateDuration) -> bool { - self.surpasses_balanced_day(duration.add_weeks_and_days_to(self.regulated_day)) + ArithmeticDate::::compare_surpasses_ordinal( + self.sign, + balanced_date.year, + balanced_date.ordinal_month, + balanced_date.day, + self.cal_date_2, + ) } } From 2cb49757ab7dae3ef6e435bd80c44446a8a7037e Mon Sep 17 00:00:00 2001 From: Alan Liu Date: Wed, 11 Mar 2026 17:55:20 -0400 Subject: [PATCH 05/10] Allow dead code for surpasses() method --- components/calendar/src/calendar_arithmetic.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/components/calendar/src/calendar_arithmetic.rs b/components/calendar/src/calendar_arithmetic.rs index b553cef9e24..5ba73ec1f3a 100644 --- a/components/calendar/src/calendar_arithmetic.rs +++ b/components/calendar/src/calendar_arithmetic.rs @@ -744,6 +744,7 @@ impl ArithmeticDate { /// This takes two dates (`self` and `other`), `duration`, and `sign` (either -1 or 1), then /// returns whether adding the duration to `self` results in a year/month/day that exceeds /// `other` in the direction indicated by `sign`, constraining the month but not the day. + #[allow(dead_code)] // TODO: remove surpasses() method when no longer needed pub(crate) fn surpasses( &self, other: &Self, From d3c693f9946dc3429b69459b9f5d728794f52476 Mon Sep 17 00:00:00 2001 From: Alan Liu Date: Fri, 13 Mar 2026 11:36:53 -0400 Subject: [PATCH 06/10] Update components/calendar/src/calendar_arithmetic.rs Co-authored-by: Shane F. Carr --- components/calendar/src/calendar_arithmetic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/calendar/src/calendar_arithmetic.rs b/components/calendar/src/calendar_arithmetic.rs index 5ba73ec1f3a..ceefb5d74cb 100644 --- a/components/calendar/src/calendar_arithmetic.rs +++ b/components/calendar/src/calendar_arithmetic.rs @@ -1078,7 +1078,7 @@ impl ArithmeticDate { candidate_days += sign; } - // // 12. Return ! CreateDateDurationRecord(years, months, weeks, days). + // 12. Return ! CreateDateDurationRecord(years, months, weeks, days). DateDuration::from_signed_ymwd(years, months, weeks, days) } } From 7dc646948fdc50091f541d55821734173fc94a03 Mon Sep 17 00:00:00 2001 From: Alan Liu Date: Fri, 13 Mar 2026 13:19:13 -0400 Subject: [PATCH 07/10] Incorporate feedback from sffc, clarify `SurpassesChecker` API - Move some code around to more closely match the spec - Improve performance of month iteration by minimizing calls to `new_balanced` - Add explicit `set_` methods to `SurpassesChecker`, to be clearer semantically. This is a place to "freeze" the internal state of the checker for a given field value. These usually inline to call `surpasses_`, but in the case of months, they do the expensive `new_balanced` call. Future optimizations can move more stuff into `set_`. --- .../calendar/src/calendar_arithmetic.rs | 75 +++++++++++-------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/components/calendar/src/calendar_arithmetic.rs b/components/calendar/src/calendar_arithmetic.rs index ceefb5d74cb..d0a95d8b5fa 100644 --- a/components/calendar/src/calendar_arithmetic.rs +++ b/components/calendar/src/calendar_arithmetic.rs @@ -744,6 +744,10 @@ impl ArithmeticDate { /// This takes two dates (`self` and `other`), `duration`, and `sign` (either -1 or 1), then /// returns whether adding the duration to `self` results in a year/month/day that exceeds /// `other` in the direction indicated by `sign`, constraining the month but not the day. + /// + /// Note: This function is no longer used, but the code is retained for + /// reference. The `until()` implementation uses `SurpassesChecker`, which + /// implements the same spec, but with better performance. #[allow(dead_code)] // TODO: remove surpasses() method when no longer needed pub(crate) fn surpasses( &self, @@ -1005,8 +1009,8 @@ impl ArithmeticDate { candidate_years += sign; } } - // Final (or only) call to freeze the checker's Years field state. - surpasses_checker.surpasses_years(years); + // Freeze the checker's Years field state. + surpasses_checker.set_years(years); // 5. Let months be 0. // 6. If largestUnit is year or largestUnit is month, then @@ -1043,8 +1047,8 @@ impl ArithmeticDate { candidate_months += sign; } } - // Final (or only) call to freeze the checker's Months field state. - surpasses_checker.surpasses_months(months); + // Freeze the checker's Months field state. + surpasses_checker.set_months(months); // 7. Let weeks be 0. // 8. If largestUnit is week, then @@ -1060,8 +1064,8 @@ impl ArithmeticDate { candidate_weeks += sign; } } - // Final (or only) call to freeze the checker's Weeks field state. - surpasses_checker.surpasses_weeks(weeks); + // Freeze the checker's Weeks field state. + surpasses_checker.set_weeks(weeks); // 9. Let days be 0. // 10. Let candidateDays be sign. @@ -1086,10 +1090,9 @@ impl ArithmeticDate { /// Stateful checker for iterative per-field `surpasses()` checks /// /// This checker for `surpasses()` is designed to iteratively evaluate one field -/// at a time, from largest to smallest. At each point, the caller must cache -/// the computed state of a larger field by calling -/// `surpasses_()` before moving on to the next smaller -/// field. +/// at a time, from largest to smallest. After each field value is determined, +/// the caller must cache the computed state of the field's value by calling +/// `set_()` before moving on to the next smaller field. struct SurpassesChecker<'a, C: DateFieldsResolver> { parts: &'a ArithmeticDate, cal_date_2: &'a ArithmeticDate, @@ -1142,11 +1145,19 @@ impl<'a, C: DateFieldsResolver> SurpassesChecker<'a, C> { let base_month = self .cal .month_from_ordinal(self.parts.year(), self.parts.month()); + let surpasses_years = ArithmeticDate::::compare_surpasses_lexicographic( + self.sign, + self.y0, + base_month, + self.parts.day(), + self.cal_date_2, + self.cal, + ); + // 5. Let m0 be MonthCodeToOrdinal(calendar, y0, ! ConstrainMonthCode(calendar, y0, parts.[[MonthCode]], constrain)). let constrain = DateFromFieldsOptions { overflow: Some(Overflow::Constrain), ..Default::default() }; - // 5. Let m0 be MonthCodeToOrdinal(calendar, y0, ! ConstrainMonthCode(calendar, y0, parts.[[MonthCode]], constrain)). let m0_result = self.cal.ordinal_from_month(self.y0, base_month, constrain); self.m0 = match m0_result { Ok(m0) => m0, @@ -1159,38 +1170,37 @@ impl<'a, C: DateFieldsResolver> SurpassesChecker<'a, C> { } }; - let surpasses_years = ArithmeticDate::::compare_surpasses_lexicographic( - self.sign, - self.y0, - base_month, - self.parts.day(), - self.cal_date_2, - self.cal, - ); - // Because of leap months in the Hebrew calendar, year checks must also include - // an ordinal test of the month. This handles cases where a date in Adar I (M05L) - // in a leap year (e.g., 5784) is compared to a date in Adar (M06) in a - // non-leap year (e.g., 5785). Since lexicographical order (M05L < M06) differs - // from temporal order when constrained to a non-leap year (where both map to - // ordinal month 6), a lexicographical-only check fails to detect that a later - // day in Adar I actually surpasses an earlier day in Adar. This ordinal check - // ensures the year counter correctly stays at 0 instead of over-incrementing to 1. + // If the lexicographic check returns false, the spec continues to the + // ordinal check, which is implemented in `surpasses_months`. surpasses_years || self.surpasses_months(0) } + #[inline] + fn set_years(&mut self, years: i64) { + self.surpasses_years(years); + } + fn surpasses_months(&mut self, months: i64) -> bool { // 6. Let monthsAdded be BalanceNonISODate(calendar, y0, m0 + months, 1). let months_added = ArithmeticDate::::new_balanced(self.y0, months + i64::from(self.m0), 1, self.cal); // 7. If CompareSurpasses(sign, monthsAdded.[[Year]], monthsAdded.[[Month]], parts.[[Day]], calDate2) is true, return true. - let surpasses = ArithmeticDate::::compare_surpasses_ordinal( + // We do not need to perform any other checks because step 8 of the spec + // guarantees an early return if weeks and days are 0. + ArithmeticDate::::compare_surpasses_ordinal( self.sign, months_added.year, months_added.ordinal_month, self.parts.day(), self.cal_date_2, - ); + ) + } + + fn set_months(&mut self, months: i64) { + // 6. Let monthsAdded be BalanceNonISODate(calendar, y0, m0 + months, 1). + let months_added = + ArithmeticDate::::new_balanced(self.y0, months + i64::from(self.m0), 1, self.cal); // 9. Let endOfMonth be BalanceNonISODate(calendar, monthsAdded.[[Year]], monthsAdded.[[Month]] + 1, 0). self.end_of_month = ArithmeticDate::::new_balanced( @@ -1210,8 +1220,6 @@ impl<'a, C: DateFieldsResolver> SurpassesChecker<'a, C> { } else { self.end_of_month.day }; - - surpasses } fn surpasses_weeks(&mut self, weeks: i64) -> bool { @@ -1241,6 +1249,11 @@ impl<'a, C: DateFieldsResolver> SurpassesChecker<'a, C> { ) } + #[inline] + fn set_weeks(&mut self, weeks: i64) { + self.surpasses_weeks(weeks); + } + fn surpasses_days(&mut self, days: i64) -> bool { // 8. If weeks = 0 and days = 0, return false. if self.weeks == 0 && days == 0 { From a01d17d642bf279d08241426d8fbcd240c63e4c6 Mon Sep 17 00:00:00 2001 From: Alan Liu Date: Fri, 13 Mar 2026 13:33:09 -0400 Subject: [PATCH 08/10] fmt --- components/calendar/src/calendar_arithmetic.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/calendar/src/calendar_arithmetic.rs b/components/calendar/src/calendar_arithmetic.rs index d0a95d8b5fa..91d67488e0b 100644 --- a/components/calendar/src/calendar_arithmetic.rs +++ b/components/calendar/src/calendar_arithmetic.rs @@ -967,7 +967,8 @@ impl ArithmeticDate { // values, we use a stateful implementation. It caches intermediate // produced by NonISODateSurpasses and reuses them in subsequent calls, // to avoid unnecessary recalculations. - let mut surpasses_checker: SurpassesChecker<'_, C> = SurpassesChecker::new(self, other, sign, cal); + let mut surpasses_checker: SurpassesChecker<'_, C> = + SurpassesChecker::new(self, other, sign, cal); // Preparation for non-specced optimization: // We don't want to spend time incrementally bumping it up one year From f58a9dac473ac701b2e7a09af4fb0a976e93e360 Mon Sep 17 00:00:00 2001 From: Alan Liu Date: Fri, 13 Mar 2026 13:51:59 -0400 Subject: [PATCH 09/10] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ef0d5d6329..68a9a276d0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Fully filled in up to 30c187f4b7 - Optimize Hebrew and Julian calendars (unicode-org#7213) - Optimize day/week diffing to use RDs (unicode-org#7308) - Optimize `until` month and day calculation performance + - Speed up `until` year and month field handling by 75% on average by optimizing `surpasses` calculation (unicode-org#7745) - `icu_casemap` - General changes only - `icu_collections` From c3b11ee14696e9cd442005cdab86cb9d40f2d4c8 Mon Sep 17 00:00:00 2001 From: Alan Liu Date: Fri, 13 Mar 2026 14:01:41 -0400 Subject: [PATCH 10/10] revert accidental CHANGELOG.md change --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68a9a276d0c..3ef0d5d6329 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,6 @@ Fully filled in up to 30c187f4b7 - Optimize Hebrew and Julian calendars (unicode-org#7213) - Optimize day/week diffing to use RDs (unicode-org#7308) - Optimize `until` month and day calculation performance - - Speed up `until` year and month field handling by 75% on average by optimizing `surpasses` calculation (unicode-org#7745) - `icu_casemap` - General changes only - `icu_collections`