Skip to content

Commit c8e240e

Browse files
committed
Implement stateful surpasses() to improve year/month until() performance
1 parent f3800d1 commit c8e240e

File tree

1 file changed

+205
-105
lines changed

1 file changed

+205
-105
lines changed

components/calendar/src/calendar_arithmetic.rs

Lines changed: 205 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -736,87 +736,117 @@ impl<C: DateFieldsResolver> ArithmeticDate<C> {
736736
sign: i64,
737737
cal: &C,
738738
) -> bool {
739-
// 1. Let _parts_ be CalendarISOToDate(_calendar_, _fromIsoDate_).
739+
// NOTE: Numbered comments refer to the Temporal `NonISODateSurpasses` spec.
740+
// Because this implementation refactors the algorithm into stateful components
741+
// to reduce redundant calculations, references to numbered lines of the spec are
742+
// distributed across multiple functions. This is intentional and meaningful.
743+
let sign_mul = if duration.is_negative { -1i64 } else { 1i64 };
744+
let years = i64::from(duration.years) * sign_mul;
745+
let months = i64::from(duration.months) * sign_mul;
746+
747+
let month_checker = self.surpasses_month_checker(other, years, sign, cal);
748+
if month_checker.surpasses_month(months) {
749+
return true;
750+
}
751+
752+
// 8. If weeks = 0 and days = 0, return false.
753+
if duration.weeks == 0 && duration.days == 0 {
754+
return false;
755+
}
756+
757+
let months_added =
758+
Self::new_balanced(month_checker.y0, months + i64::from(month_checker.m0), 1, cal);
759+
let week_day_checker =
760+
self.surpasses_week_day_checker_from_months_added(&month_checker, months_added);
761+
week_day_checker.surpasses_week_day(duration)
762+
}
763+
764+
/// Prepares a stateful checker for month iteration in surpasses().
765+
fn surpasses_month_checker<'a>(
766+
&'a self,
767+
other: &'a Self,
768+
years: i64,
769+
sign: i64,
770+
cal: &'a C,
771+
) -> SurpassesMonthChecker<'a, C> {
772+
// 1. Let parts be CalendarISOToDate(calendar, fromIsoDate).
740773
let parts = self;
741-
// 1. Let _calDate2_ be CalendarISOToDate(_calendar_, _toIsoDate_).
774+
// 2. Let calDate2 be CalendarISOToDate(calendar, toIsoDate).
742775
let cal_date_2 = other;
743-
// 1. Let _y0_ be _parts_.[[Year]] + _years_.
744-
let y0 =
745-
cal.year_info_from_extended(duration.add_years_to(parts.year().to_extended_year()));
746-
// 1. If CompareSurpasses(_sign_, _y0_, _parts_.[[MonthCode]], _parts_.[[Day]], _calDate2_) is *true*, return *true*.
776+
// 3. Let y0 be parts.[[Year]] + years.
777+
let y0 = cal.year_info_from_extended(parts.year().to_extended_year() + years as i32);
747778
let base_month = cal.month_from_ordinal(parts.year(), parts.month());
748-
if Self::compare_surpasses_lexicographic(sign, y0, base_month, parts.day(), cal_date_2, cal)
749-
{
750-
return true;
751-
}
752-
// 1. Let _m0_ be MonthCodeToOrdinal(_calendar_, _y0_, ! ConstrainMonthCode(_calendar_, _y0_, _parts_.[[MonthCode]], ~constrain~)).
753779
let constrain = DateFromFieldsOptions {
754780
overflow: Some(Overflow::Constrain),
755781
..Default::default()
756782
};
757-
let m0_result = cal.ordinal_from_month(y0, base_month, constrain);
758-
let m0 = match m0_result {
759-
Ok(m0) => m0,
760-
Err(_) => {
761-
debug_assert!(
762-
false,
763-
"valid month code for calendar, and constrained to the year"
764-
);
765-
1
766-
}
767-
};
768-
// 1. Let _monthsAdded_ be BalanceNonISODate(_calendar_, _y0_, _m0_ + _months_, 1).
769-
let months_added = Self::new_balanced(y0, duration.add_months_to(m0), 1, cal);
783+
// 5. Let m0 be MonthCodeToOrdinal(calendar, y0, ! ConstrainMonthCode(calendar, y0, parts.[[MonthCode]], constrain)).
784+
let m0 = cal
785+
.ordinal_from_month(y0, base_month, constrain)
786+
.unwrap_or(1);
770787

771-
// 1. If CompareSurpasses(_sign_, _monthsAdded_.[[Year]], _monthsAdded_.[[Month]], _parts_.[[Day]], _calDate2_) is *true*, return *true*.
772-
if Self::compare_surpasses_ordinal(
773-
sign,
774-
months_added.year,
775-
months_added.ordinal_month,
776-
parts.day(),
788+
// 7. If CompareSurpasses(sign, monthsAdded.[[Year]], monthsAdded.[[Month]], parts.[[Day]], calDate2) is true, return true.
789+
let lexicographic_surpasses =
790+
Self::compare_surpasses_lexicographic(sign, y0, base_month, parts.day(), cal_date_2, cal);
791+
792+
SurpassesMonthChecker {
793+
parts,
777794
cal_date_2,
778-
) {
779-
return true;
780-
}
781-
// 1. If _weeks_ = 0 and _days_ = 0, return *false*.
782-
if duration.weeks == 0 && duration.days == 0 {
783-
return false;
795+
y0,
796+
m0,
797+
lexicographic_surpasses,
798+
sign,
799+
cal,
784800
}
785-
// 1. Let _endOfMonth_ be BalanceNonISODate(_calendar_, _monthsAdded_.[[Year]], _monthsAdded_.[[Month]] + 1, 0).
801+
}
802+
803+
/// Prepares a stateful checker for week and day iteration in surpasses().
804+
fn surpasses_week_day_checker<'a>(
805+
&'a self,
806+
other: &'a Self,
807+
years: i64,
808+
months: i64,
809+
sign: i64,
810+
cal: &'a C,
811+
) -> SurpassesWeekDayChecker<'a, C> {
812+
let month_checker = self.surpasses_month_checker(other, years, sign, cal);
813+
// 6. Let monthsAdded be BalanceNonISODate(calendar, y0, m0 + months, 1).
814+
let months_added =
815+
Self::new_balanced(month_checker.y0, months + i64::from(month_checker.m0), 1, cal);
816+
self.surpasses_week_day_checker_from_months_added(&month_checker, months_added)
817+
}
818+
819+
/// Prepares a checker for week and day iteration from an existing months_added date.
820+
fn surpasses_week_day_checker_from_months_added<'a>(
821+
&'a self,
822+
month_checker: &SurpassesMonthChecker<'a, C>,
823+
months_added: UncheckedArithmeticDate<C>,
824+
) -> SurpassesWeekDayChecker<'a, C> {
825+
// 9. Let endOfMonth be BalanceNonISODate(calendar, monthsAdded.[[Year]], monthsAdded.[[Month]] + 1, 0).
786826
let end_of_month = Self::new_balanced(
787827
months_added.year,
788828
i64::from(months_added.ordinal_month) + 1,
789829
0,
790-
cal,
830+
month_checker.cal,
791831
);
792-
// 1. Let _baseDay_ be _parts_.[[Day]].
793-
let base_day = parts.day();
794-
// 1. If _baseDay_ &le; _endOfMonth_.[[Day]], then
795-
// 1. Let _regulatedDay_ be _baseDay_.
796-
// 1. Else,
797-
// 1. Let _regulatedDay_ be _endOfMonth_.[[Day]].
832+
// 10. Let baseDay be parts.[[Day]].
833+
let base_day = self.day();
834+
// 11. If baseDay ≤ endOfMonth.[[Day]], then
835+
// a. Let regulatedDay be baseDay.
836+
// 12. Else,
837+
// a. Let regulatedDay be endOfMonth.[[Day]].
798838
let regulated_day = if base_day < end_of_month.day {
799839
base_day
800840
} else {
801841
end_of_month.day
802842
};
803-
// 1. Let _daysInWeek_ be 7 (the number of days in a week for all supported calendars).
804-
// 1. Let _balancedDate_ be BalanceNonISODate(_calendar_, _endOfMonth_.[[Year]], _endOfMonth_.[[Month]], _regulatedDay_ + _daysInWeek_ * _weeks_ + _days_).
805-
// 1. Return CompareSurpasses(_sign_, _balancedDate_.[[Year]], _balancedDate_.[[Month]], _balancedDate_.[[Day]], _calDate2_).
806-
let balanced_date = Self::new_balanced(
807-
end_of_month.year,
808-
i64::from(end_of_month.ordinal_month),
809-
duration.add_weeks_and_days_to(regulated_day),
810-
cal,
811-
);
812-
813-
Self::compare_surpasses_ordinal(
814-
sign,
815-
balanced_date.year,
816-
balanced_date.ordinal_month,
817-
balanced_date.day,
818-
cal_date_2,
819-
)
843+
SurpassesWeekDayChecker {
844+
cal_date_2: month_checker.cal_date_2,
845+
end_of_month,
846+
regulated_day,
847+
sign: month_checker.sign,
848+
cal: month_checker.cal,
849+
}
820850
}
821851

822852
/// Implements the Temporal abstract operation `NonISODateAdd`.
@@ -934,8 +964,8 @@ impl<C: DateFieldsResolver> ArithmeticDate<C> {
934964
}
935965
}
936966

937-
// 1. Let _sign_ be -1 × CompareISODate(_one_, _two_).
938-
// 1. If _sign_ = 0, return ZeroDateDuration().
967+
// 1. Let sign be -1 × CompareISODate(one, two).
968+
// 2. If sign = 0, return ZeroDateDuration().
939969
let sign = match other.cmp(self) {
940970
Ordering::Greater => 1i64,
941971
Ordering::Equal => return DateDuration::default(),
@@ -959,12 +989,12 @@ impl<C: DateFieldsResolver> ArithmeticDate<C> {
959989
cal,
960990
));
961991

962-
// 1. Let _years_ be 0.
963-
// 1. If _largestUnit_ is ~year~, then
964-
// 1. Let _candidateYears_ be _sign_.
965-
// 1. Repeat, while NonISODateSurpasses(_calendar_, _sign_, _one_, _two_, _candidateYears_, 0, 0, 0) is *false*,
966-
// 1. Set _years_ to _candidateYears_.
967-
// 1. Set _candidateYears_ to _candidateYears_ + _sign_.
992+
// 3. Let years be 0.
993+
// 4. If largestUnit is year, then
994+
// a. Let candidateYears be sign.
995+
// b. Repeat, while NonISODateSurpasses(calendar, sign, one, two, candidateYears, 0, 0, 0) is false,
996+
// i. Set years to candidateYears.
997+
// ii. Set candidateYears to candidateYears + sign.
968998

969999
let mut years = 0;
9701000
if matches!(options.largest_unit, Some(DateDurationUnit::Years)) {
@@ -986,12 +1016,12 @@ impl<C: DateFieldsResolver> ArithmeticDate<C> {
9861016
}
9871017
}
9881018

989-
// 1. Let _months_ be 0.
990-
// 1. If _largestUnit_ is ~year~ or _largestUnit_ is ~month~, then
991-
// 1. Let _candidateMonths_ be _sign_.
992-
// 1. Repeat, while NonISODateSurpasses(_calendar_, _sign_, _one_, _two_, _years_, _candidateMonths_, 0, 0) is *false*,
993-
// 1. Set _months_ to _candidateMonths_.
994-
// 1. Set _candidateMonths_ to _candidateMonths_ + _sign_.
1019+
// 5. Let months be 0.
1020+
// 6. If largestUnit is year or largestUnit is month, then
1021+
// a. Let candidateMonths be sign.
1022+
// b. Repeat, while NonISODateSurpasses(calendar, sign, one, two, years, candidateMonths, 0, 0) is false,
1023+
// i. Set months to candidateMonths.
1024+
// ii. Set candidateMonths to candidateMonths + sign.
9951025
let mut months = 0;
9961026
if matches!(
9971027
options.largest_unit,
@@ -1015,57 +1045,44 @@ impl<C: DateFieldsResolver> ArithmeticDate<C> {
10151045
candidate_months = min_months
10161046
}
10171047

1018-
while !self.surpasses(
1019-
other,
1020-
DateDuration::from_signed_ymwd(years, candidate_months, 0, 0),
1021-
sign,
1022-
cal,
1023-
) {
1048+
let checker = self.surpasses_month_checker(other, years, sign, cal);
1049+
while !checker.surpasses_month(candidate_months) {
10241050
months = candidate_months;
10251051
candidate_months += sign;
10261052
}
10271053
}
1028-
// 1. Let _weeks_ be 0.
1029-
// 1. If _largestUnit_ is ~week~, then
1030-
// 1. Let _candidateWeeks_ be _sign_.
1031-
// 1. Repeat, while NonISODateSurpasses(_calendar_, _sign_, _one_, _two_, _years_, _months_, _candidateWeeks_, 0) is *false*,
1032-
// 1. Set _weeks_ to _candidateWeeks_.
1033-
// 1. Set _candidateWeeks_ to _candidateWeeks_ + sign.
1054+
// 7. Let weeks be 0.
1055+
// 8. If largestUnit is week, then
1056+
// a. Let candidateWeeks be sign.
1057+
// b. Repeat, while NonISODateSurpasses(calendar, sign, one, two, years, months, candidateWeeks, 0) is false,
1058+
// i. Set weeks to candidateWeeks.
1059+
// ii. Set candidateWeeks to candidateWeeks + sign.
10341060
let mut weeks = 0;
1061+
let checker = self.surpasses_week_day_checker(other, years, months, sign, cal);
10351062
if matches!(options.largest_unit, Some(DateDurationUnit::Weeks)) {
10361063
let mut candidate_weeks = sign;
1037-
while !self.surpasses(
1038-
other,
1039-
DateDuration::from_signed_ymwd(years, months, candidate_weeks, 0),
1040-
sign,
1041-
cal,
1042-
) {
1064+
while !checker.surpasses_week(candidate_weeks) {
10431065
weeks = candidate_weeks;
10441066
candidate_weeks += sign;
10451067
}
10461068
}
10471069

1048-
// 1. Let _days_ be 0.
1049-
// 1. Let _candidateDays_ be _sign_.
1050-
// 1. Repeat, while NonISODateSurpasses(_calendar_, _sign_, _one_, _two_, _years_, _months_, _weeks_, _candidateDays_) is *false*,
1051-
// 1. Set _days_ to _candidateDays_.
1052-
// 1. Set _candidateDays_ to _candidateDays_ + _sign_.
1070+
// 9. Let days be 0.
1071+
// 10. Let candidateDays be sign.
1072+
// 11. Repeat, while NonISODateSurpasses(calendar, sign, one, two, years, months, weeks, candidateDays) is false,
1073+
// a. Set days to candidateDays.
1074+
// b. Set candidateDays to candidateDays + sign.
10531075
let mut days = 0;
10541076
// There is no pressing need to optimize candidate_days here: the early-return RD arithmetic
10551077
// optimization will be hit if the largest_unit is weeks/days, and if it is months or years we will
10561078
// arrive here with a candidate date that is at most 31 days off. We can run this loop 31 times.
10571079
let mut candidate_days = sign;
1058-
while !self.surpasses(
1059-
other,
1060-
DateDuration::from_signed_ymwd(years, months, weeks, candidate_days),
1061-
sign,
1062-
cal,
1063-
) {
1080+
while !checker.surpasses_day(weeks, candidate_days) {
10641081
days = candidate_days;
10651082
candidate_days += sign;
10661083
}
10671084

1068-
// 1. Return ! CreateDateDurationRecord(_years_, _months_, _weeks_, _days_).
1085+
// 12. Return ! CreateDateDurationRecord(years, months, weeks, days).
10691086
DateDuration::from_signed_ymwd(years, months, weeks, days)
10701087
}
10711088

@@ -1075,6 +1092,89 @@ impl<C: DateFieldsResolver> ArithmeticDate<C> {
10751092
}
10761093
}
10771094

1095+
/// Stateful checker for month iteration in surpasses().
1096+
///
1097+
/// By saving intermediary computations based on a fixed year,
1098+
/// only the computations relating to the month are done. The week
1099+
/// and day are expected to be zero.
1100+
struct SurpassesMonthChecker<'a, C: DateFieldsResolver> {
1101+
parts: &'a ArithmeticDate<C>,
1102+
cal_date_2: &'a ArithmeticDate<C>,
1103+
y0: C::YearInfo,
1104+
m0: u8,
1105+
lexicographic_surpasses: bool,
1106+
sign: i64,
1107+
cal: &'a C,
1108+
}
1109+
1110+
impl<'a, C: DateFieldsResolver> SurpassesMonthChecker<'a, C> {
1111+
fn surpasses_month(&self, months: i64) -> bool {
1112+
// 4. If CompareSurpasses(sign, y0, parts.[[MonthCode]], parts.[[Day]], calDate2) is true, return true.
1113+
if self.lexicographic_surpasses {
1114+
return true;
1115+
}
1116+
// 6. Let monthsAdded be BalanceNonISODate(calendar, y0, m0 + months, 1).
1117+
let months_added =
1118+
ArithmeticDate::<C>::new_balanced(self.y0, months + i64::from(self.m0), 1, self.cal);
1119+
1120+
// 7. If CompareSurpasses(sign, monthsAdded.[[Year]], monthsAdded.[[Month]], parts.[[Day]], calDate2) is true, return true.
1121+
ArithmeticDate::<C>::compare_surpasses_ordinal(
1122+
self.sign,
1123+
months_added.year,
1124+
months_added.ordinal_month,
1125+
self.parts.day(),
1126+
self.cal_date_2,
1127+
)
1128+
}
1129+
}
1130+
1131+
/// Stateful checker for week and day iteration in surpasses().
1132+
///
1133+
/// By saving intermediary computations based on a fixed year and month,
1134+
/// only the computations relating to the week and day are done.
1135+
struct SurpassesWeekDayChecker<'a, C: DateFieldsResolver> {
1136+
cal_date_2: &'a ArithmeticDate<C>,
1137+
end_of_month: UncheckedArithmeticDate<C>,
1138+
regulated_day: u8,
1139+
sign: i64,
1140+
cal: &'a C,
1141+
}
1142+
1143+
impl<'a, C: DateFieldsResolver> SurpassesWeekDayChecker<'a, C> {
1144+
fn surpasses_balanced_day(&self, balanced_day: i64) -> bool {
1145+
// 14. Let balancedDate be BalanceNonISODate(calendar, endOfMonth.[[Year]], endOfMonth.[[Month]], regulatedDay + daysInWeek * weeks + days).
1146+
let balanced_date = ArithmeticDate::<C>::new_balanced(
1147+
self.end_of_month.year,
1148+
i64::from(self.end_of_month.ordinal_month),
1149+
balanced_day,
1150+
self.cal,
1151+
);
1152+
1153+
// 15. Return CompareSurpasses(sign, balancedDate.[[Year]], balancedDate.[[Month]], balancedDate.[[Day]], calDate2).
1154+
ArithmeticDate::<C>::compare_surpasses_ordinal(
1155+
self.sign,
1156+
balanced_date.year,
1157+
balanced_date.ordinal_month,
1158+
balanced_date.day,
1159+
self.cal_date_2,
1160+
)
1161+
}
1162+
1163+
fn surpasses_week(&self, weeks: i64) -> bool {
1164+
// 13. Let daysInWeek be 7 (the number of days in a week for all supported calendars).
1165+
self.surpasses_balanced_day(7 * weeks + i64::from(self.regulated_day))
1166+
}
1167+
1168+
fn surpasses_day(&self, weeks: i64, days: i64) -> bool {
1169+
// 13. Let daysInWeek be 7 (the number of days in a week for all supported calendars).
1170+
self.surpasses_balanced_day(7 * weeks + days + i64::from(self.regulated_day))
1171+
}
1172+
1173+
fn surpasses_week_day(&self, duration: DateDuration) -> bool {
1174+
self.surpasses_balanced_day(duration.add_weeks_and_days_to(self.regulated_day))
1175+
}
1176+
}
1177+
10781178
#[cfg(test)]
10791179
mod tests {
10801180
use super::*;

0 commit comments

Comments
 (0)