diff --git a/CHANGELOG.md b/CHANGELOG.md index d098a07ee89..3ef0d5d6329 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Fully filled in up to 30c187f4b7 - Deprecate `Date::new_from_iso`/`Date::to_iso` (unicode-org#7287) - 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 - `icu_casemap` - General changes only - `icu_collections` diff --git a/components/calendar/benches/until.rs b/components/calendar/benches/until.rs index 063de0e4ab3..b07e061d729 100644 --- a/components/calendar/benches/until.rs +++ b/components/calendar/benches/until.rs @@ -37,7 +37,7 @@ fn bench_calendar( } } -fn convert_benches(c: &mut Criterion) { +fn until_benches(c: &mut Criterion) { let mut group = c.benchmark_group("until/years"); bench_all_calendars!(group, bench_calendar, DateDurationUnit::Years); group.finish(); @@ -55,5 +55,5 @@ fn convert_benches(c: &mut Criterion) { group.finish(); } -criterion_group!(benches, convert_benches); +criterion_group!(benches, until_benches); criterion_main!(benches); diff --git a/components/calendar/src/cal/coptic.rs b/components/calendar/src/cal/coptic.rs index a6d57779d0c..021317c2900 100644 --- a/components/calendar/src/cal/coptic.rs +++ b/components/calendar/src/cal/coptic.rs @@ -67,6 +67,11 @@ impl DateFieldsResolver for Coptic { 13 } + #[inline] + fn min_months_from_inner(_start: Self::YearInfo, years: i64) -> i64 { + 13 * years + } + #[inline] fn extended_year_from_era_year_unchecked( &self, diff --git a/components/calendar/src/cal/east_asian_traditional.rs b/components/calendar/src/cal/east_asian_traditional.rs index d1b5a032a0a..6e6d3777995 100644 --- a/components/calendar/src/cal/east_asian_traditional.rs +++ b/components/calendar/src/cal/east_asian_traditional.rs @@ -587,6 +587,13 @@ impl DateFieldsResolver for EastAsianTraditional { 12 + year.packed.leap_month().is_some() as u8 } + #[inline] + fn min_months_from_inner(_start: Self::YearInfo, years: i64) -> i64 { + // A lunisolar leap month is inserted at least every 3 years. Reingold denies + // that the Chinese calendar determines leap years using the 19-year Metonic cycle. + 12 * years + (years / 3) + } + #[inline] fn extended_year_from_era_year_unchecked( &self, diff --git a/components/calendar/src/cal/ethiopian.rs b/components/calendar/src/cal/ethiopian.rs index baa409f496d..39003f85b29 100644 --- a/components/calendar/src/cal/ethiopian.rs +++ b/components/calendar/src/cal/ethiopian.rs @@ -94,6 +94,11 @@ impl DateFieldsResolver for Ethiopian { Coptic::months_in_provided_year(year) } + #[inline] + fn min_months_from_inner(start: Self::YearInfo, years: i64) -> i64 { + Coptic::min_months_from_inner(start, years) + } + #[inline] fn extended_year_from_era_year_unchecked( &self, diff --git a/components/calendar/src/cal/hebrew.rs b/components/calendar/src/cal/hebrew.rs index e78e020e2dd..9fd386d22e5 100644 --- a/components/calendar/src/cal/hebrew.rs +++ b/components/calendar/src/cal/hebrew.rs @@ -134,6 +134,12 @@ impl DateFieldsResolver for Hebrew { 12 + year.keviyah.is_leap() as u8 } + #[inline] + fn min_months_from_inner(_start: HebrewYear, years: i64) -> i64 { + // There are 7 leap years in every 19-year Metonic cycle. + 235 * years / 19 + } + #[inline] fn extended_year_from_era_year_unchecked( &self, diff --git a/components/calendar/src/calendar_arithmetic.rs b/components/calendar/src/calendar_arithmetic.rs index 2f68a5d9ed0..3a9fd3a103c 100644 --- a/components/calendar/src/calendar_arithmetic.rs +++ b/components/calendar/src/calendar_arithmetic.rs @@ -235,6 +235,14 @@ pub(crate) trait DateFieldsResolver: Calendar { 12 } + /// The minimum number of months over `years` years, starting from the given year. + /// + /// The default impl is for non-lunisolar calendars with 12 months! + #[inline] + fn min_months_from_inner(_start: Self::YearInfo, years: i64) -> i64 { + 12 * years + } + /// Calculates the ordinal month for the given year and month code. /// /// The default impl is for non-lunisolar calendars! @@ -902,7 +910,7 @@ impl ArithmeticDate { /// Implements the Temporal abstract operation `NonISODateUntil`. /// - /// This takes a duration (`self`) and a date (`other`), then returns a duration that, when + /// This takes two dates (`self` and `other`), then returns a duration that, when /// added to `self`, results in `other`, with largest unit according to `options`. pub(crate) fn until( &self, @@ -992,14 +1000,12 @@ impl ArithmeticDate { let mut candidate_months = sign; if options.largest_unit == Some(DateDurationUnit::Months) && min_years != 0 { - // Optimization: No current calendar supports years with month length < 12. - // If something is at least N full years away, it is also at least 12*N full months away. - // - // In the future we can introduce per-calendar routines that are better at estimating a month count. - // - // We only need to apply this optimization for largest_unit = Months. If the largest_unit is years then - // our candidate date is already pretty close and won't need more than 12 iterations to get there. - let min_months = min_years * 12; + // If largest_unit = Months, then compute the calendar-specific minimum number of + // months corresponding to min_years. For solar calendars, this is 12 * min_years. + // For the Hebrew calendar, a leap month is added for 7 out of 19 years. East Asian + // Calendars do not provide a specialized implementation of `min_months_from()` + // because it would be too expensive to calculate; they default to 12 * min_years. + let min_months = self.min_months_from(min_years); debug_assert!(!self.surpasses( other, DateDuration::from_signed_ymwd(years, min_months, 0, 0), @@ -1038,6 +1044,7 @@ impl ArithmeticDate { 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*, @@ -1057,9 +1064,15 @@ impl ArithmeticDate { days = candidate_days; candidate_days += sign; } + // 1. Return ! CreateDateDurationRecord(_years_, _months_, _weeks_, _days_). DateDuration::from_signed_ymwd(years, months, weeks, days) } + + /// The minimum number of months over `years` years, starting from `self.year()`. + pub(crate) fn min_months_from(self, years: i64) -> i64 { + C::min_months_from_inner(self.year(), years) + } } #[cfg(test)] diff --git a/components/calendar/src/options.rs b/components/calendar/src/options.rs index 55fa0100220..09751cc607a 100644 --- a/components/calendar/src/options.rs +++ b/components/calendar/src/options.rs @@ -197,7 +197,7 @@ mod unstable { /// let mut options_days = options_default; /// options_days.largest_unit = Some(DateDurationUnit::Days); /// assert_eq!( - /// d1.try_until_with_options(&d2, options_default).unwrap(), + /// d1.try_until_with_options(&d2, options_days).unwrap(), /// DateDuration::for_days(410) /// ); ///