Skip to content

Conversation

sffc
Copy link
Collaborator

@sffc sffc commented Jul 21, 2025

This is a big step toward making CalendarDateUntil be calendar-agnostic (#3136).

I have done my best to verify that the change is fully editorial. I wrote Rust code for the before and after and ran it for all date pairs between 2022 and 2026, and found no differences. You can find my code here:

https://gist.github.com/sffc/2c7811dab688a3d21c3e8a4478a8d1dc

The function named CalendarDateUntil is the current algorithm, ported line-by-line, and CalendarDateUntil4 is the new one. (Versions 2 and 3 are a sneak-peak of what I want to do in follow-up PRs to the spec)

Copy link

codecov bot commented Jul 21, 2025

Codecov Report

❌ Patch coverage is 50.00000% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.77%. Comparing base (47042f2) to head (52e277c).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
polyfill/lib/calendar.mjs 50.00% 8 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3138      +/-   ##
==========================================
- Coverage   96.85%   96.77%   -0.08%     
==========================================
  Files          21       21              
  Lines        9983     9992       +9     
  Branches     1829     1830       +1     
==========================================
+ Hits         9669     9670       +1     
- Misses        268      276       +8     
  Partials       46       46              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sffc
Copy link
Collaborator Author

sffc commented Jul 21, 2025

Note: the new spec performs operations that may be redundant, like calling BalanceISOYearMonth again and again for every field, but it makes it much more clear what the spec logic is actually doing. Someone implementing this can easily inline the code back into CalendarDateUntil and get back the old spec. Specs should prioritize clarity.

@sffc
Copy link
Collaborator Author

sffc commented Jul 25, 2025

I'm going to prove that this is purely a reverse-inline refactoring.

First, I'll take the new CalendarDateUntil and introduce a surpasses variable, and rewrite the loop using a Goto statement so that the upcoming inlining will be cleaner:

  1. Let _sign_ be -CompareISODate(_one_, _two_).
  1. If _sign_ = 0, return ZeroDateDuration().
  1. Let _years_ be 0.
  1. If _largestUnit_ is ~year~, then
    1. Let _candidateYears_ be _sign_.
    1. Let _surpasses_ be ISODateSurpasses(_sign_, _one_, _two_, _candidateYears_, 0, 0, 0).
    1. If _surpasses_ is *false*,
      1. Set _years_ to _candidateYears_.
      1. Set _candidateYears_ to _candidateYears_ + _sign_.
      1. Goto the previous "Let surpasses be" line.
  1. Let _months_ be 0.
  1. If _largestUnit_ is ~year~ or _largestUnit_ is ~month~, then
    1. Let _candidateMonths_ be _sign_.
    1. Let _surpasses_ be ISODateSurpasses(_sign_, _one_, _two_, _years_, _candidateMonths_, 0, 0).
    1. If _surpasses_ is *false*,
      1. Set _months_ to _candidateMonths_.
      1. Set _candidateMonths_ to _candidateMonths_ + _sign_.
      1. Goto the previous "Let surpasses be" line.
  1. Let _weeks_ be 0.
  1. If _largestUnit_ is ~week~, then
    1. Let _candidateWeeks_ be _sign_.
    1. Let _surpasses_ be ISODateSurpasses(_sign_, _one_, _two_, _years_, _months_, _candidateWeeks_, 0).
    1. If _surpasses_ is *false*,
      1. Set _weeks_ to _candidateWeeks_.
      1. Set _candidateWeeks_ to _candidateWeeks_ + sign.
      1. Goto the previous "Let surpasses be" line.
  1. Let _days_ be 0.
  1. Let _candidateDays_ be _sign_.
  1. Let _surpasses_ be ISODateSurpasses(_sign_, _one_, _two_, _years_, _months_, _weeks_, _candidateDays_).
  1. If _surpasses_ is *false*,
    1. Set _days_ to _candidateDays_.
    1. Set _candidateDays_ to _candidateDays_ + _sign_.
    1. Goto the previous "Let surpasses be" line.
  1. Return ! CreateDateDurationRecord(_years_, _months_, _weeks_, _days_).

Now I will inline the new steps of ISODateSurpasses and return that AO to its old signature. I will pick one or the other branch of the if statement when inlining:

  1. Let _sign_ be -CompareISODate(_one_, _two_).
  1. If _sign_ = 0, return ZeroDateDuration().
  1. Let _years_ be 0.
  1. If _largestUnit_ is ~year~, then
    1. Let _candidateYears_ be _sign_.
    1. Let _yearMonth_ be BalanceISOYearMonth(_one_.[[Year]] + _candidateYears_, _one_.[[Month]]).
    1. Let _y1_ be _yearMonth_.[[Year]].
    1. Let _m1_ be _yearMonth_.[[Month]].
    1. Let _d1_ be _one_.[[Day]].
    1. Let _surpasses_ be ISODateSurpasses(_sign_, _y1_, _m1_, _d1_, _two_).
    1. If _surpasses_ is *false*,
      1. Set _years_ to _candidateYears_.
      1. Set _candidateYears_ to _candidateYears_ + _sign_.
      1. Goto the previous "Let yearMonth be" line.
  1. Let _months_ be 0.
  1. If _largestUnit_ is ~year~ or _largestUnit_ is ~month~, then
    1. Let _candidateMonths_ be _sign_.
    1. Let _yearMonth_ be BalanceISOYearMonth(_one_.[[Year]] + _years_, _one_.[[Month]] + _candidateMonths_).
    1. Let _y1_ be _yearMonth_.[[Year]].
    1. Let _m1_ be _yearMonth_.[[Month]].
    1. Let _d1_ be _one_.[[Day]].
    1. Let _surpasses_ be ISODateSurpasses(_sign_, _y1_, _m1_, _d1_, _two_).
    1. If _surpasses_ is *false*,
      1. Set _months_ to _candidateMonths_.
      1. Set _candidateMonths_ to _candidateMonths_ + _sign_.
      1. Goto the previous "Let yearMonth be" line.
  1. Let _weeks_ be 0.
  1. If _largestUnit_ is ~week~, then
    1. Let _candidateWeeks_ be _sign_.
    1. Let _yearMonth_ be BalanceISOYearMonth(_one_.[[Year]] + _years_, _one_.[[Month]] + _months_).
    1. Let _regulatedDate_ be ! RegulateISODate(_yearMonth_.[[Year]], _yearMonth_.[[Month]], _one_.[[Day]], ~constrain~).
    1. Let _balancedDate_ be BalanceISODate(_regulatedDate_.[[Year]], _regulatedDate_.[[Month]], _regulatedDate_.[[Day]] + 7 × _candidateWeeks_).
    1. Let _y1_ be _balancedDate_.[[Year]].
    1. Let _m1_ be _balancedDate_.[[Month]].
    1. Let _d1_ be _balancedDate_.[[Day]].
    1. Let _surpasses_ be ISODateSurpasses(_sign_, _y1_, _m1_, _d1_, _two_).
    1. If _surpasses_ is *false*,
      1. Set _weeks_ to _candidateWeeks_.
      1. Set _candidateWeeks_ to _candidateWeeks_ + sign.
      1. Goto the previous "Let yearMonth be" line.
  1. Let _days_ be 0.
  1. Let _candidateDays_ be _sign_.
  1. Let _yearMonth_ be BalanceISOYearMonth(_one_.[[Year]] + _years_, _one_.[[Month]] + _months_).
  1. Let _regulatedDate_ be ! RegulateISODate(_yearMonth_.[[Year]], _yearMonth_.[[Month]], _one_.[[Day]], ~constrain~).
  1. Let _balancedDate_ be BalanceISODate(_regulatedDate_.[[Year]], _regulatedDate_.[[Month]], _regulatedDate_.[[Day]] + 7 × _weeks_ + _candidateDays_).
  1. Let _y1_ be _balancedDate_.[[Year]].
  1. Let _m1_ be _balancedDate_.[[Month]].
  1. Let _d1_ be _balancedDate_.[[Day]].
  1. Let _surpasses_ be ISODateSurpasses(_sign_, _y1_, _m1_, _d1_, _two_).
  1. If _surpasses_ is *false*,
    1. Set _days_ to _candidateDays_.
    1. Set _candidateDays_ to _candidateDays_ + _sign_.
    1. Goto the previous "Let surpasses be" line.
  1. Return ! CreateDateDurationRecord(_years_, _months_, _weeks_, _days_).

Hopefully you agree so far: everything I've done is purely mechanical.

Now I will make the following assertions:

  1. BalanceISOYearMonth(_one_.[[Year]] + _candidateYears_, _one_.[[Month]]) is a no-op, because all ISO months are valid in all ISO years. We can remove this function call.
  2. The following operations are equivalent. Since the spec previously was using the first form, I will change to the first form here:
    • _yearMonth_ = BalanceISOYearMonth(_yearMonth_.[[Year]], _yearMonth_.[[Month]] + _sign_) when updating in the loop
    • _yearMonth_ = BalanceISOYearMonth(_one_.[[Year]] + _years_, _one_.[[Month]] + _candidateMonths_) each time separately
  3. Similarly, the following operations are equivalent. Since the spec previously was using the first form, I will change to the first form here:
    • _balancedDate_ be BalanceISODate(_balancedDate_.[[Year]], _balancedDate_.[[Month]], _balancedDate_.[[Day]] + 7 × _sign_) when updating in the loop
    • _balancedDate_ be BalanceISODate(_regulatedDate_.[[Year]], _regulatedDate_.[[Month]], _regulatedDate_.[[Day]] + 7 × _candidateWeeks_) each time separately
  4. Similarly, the following operations are equivalent. Since the spec previously was using the first form, I will change to the first form here:
    • _balancedDate_ = BalanceISODate(_balancedDate_.[[Year]], _balancedDate_.[[Month]], _balancedDate_.[[Day]] + _sign_) when updating in the loop
    • _balancedDate_ = BalanceISODate(_regulatedDate_.[[Year]], _regulatedDate_.[[Month]], _regulatedDate_.[[Day]] + 7 × _weeks_ + _candidateDays_) each time separately
  5. The last two calls to BalanceISOYearMonth(_one_.[[Year]] + _years_, _one_.[[Month]] + _months_) are equivalent: they take the same arguments, which do not change between the call sites. We can remove the second function call.
  6. Likewise, the last two calls to Let _regulatedDate_ be ! RegulateISODate(_yearMonth_.[[Year]], _yearMonth_.[[Month]], _one_.[[Day]], ~constrain~) are also equivalent. We can remove the second function call.
  7. I will lift the first copy of both of those lines out of their If _largestUnit_ is ~weeks block, since they can be equivalently calculated in the outer context.

With these simplifications, we get:

  1. Let _sign_ be -CompareISODate(_one_, _two_).
  1. If _sign_ = 0, return ZeroDateDuration().
  1. Let _years_ be 0.
  1. If _largestUnit_ is ~year~, then
    1. Let _candidateYears_ be _sign_.
    1. Let _y1_ be _one_.[[Year]] + _candidateYears_.
    1. Let _m1_ be _one_.[[Month]].
    1. Let _d1_ be _one_.[[Day]].
    1. Let _surpasses_ be ISODateSurpasses(_sign_, _y1_, _m1_, _d1_, _two_).
    1. If _surpasses_ is *false*,
      1. Set _years_ to _candidateYears_.
      1. Set _candidateYears_ to _candidateYears_ + _sign_.
      1. Goto the previous "Let y1 be" line.
  1. Let _months_ be 0.
  1. If _largestUnit_ is ~year~ or _largestUnit_ is ~month~, then
    1. Let _candidateMonths_ be _sign_.
    1. Let _yearMonth_ be BalanceISOYearMonth(_one_.[[Year]] + _years_, _one_.[[Month]] + _candidateMonths_).
    1. Let _y1_ be _yearMonth_.[[Year]].
    1. Let _m1_ be _yearMonth_.[[Month]].
    1. Let _d1_ be _one_.[[Day]].
    1. Let _surpasses_ be ISODateSurpasses(_sign_, _y1_, _m1_, _d1_, _two_).
    1. If _surpasses_ is *false*,
      1. Set _months_ to _candidateMonths_.
      1. Set _candidateMonths_ to _candidateMonths_ + _sign_.
      1. Set _yearMonth_ to BalanceISOYearMonth(_yearMonth_.[[Year]], _yearMonth_.[[Month]] + _sign_).
      1. Goto the previous "Let y1 be" line.
  1. Let _yearMonth_ be BalanceISOYearMonth(_one_.[[Year]] + _years_, _one_.[[Month]] + _months_).
  1. Let _regulatedDate_ be ! RegulateISODate(_yearMonth_.[[Year]], _yearMonth_.[[Month]], _one_.[[Day]], ~constrain~).
  1. Let _weeks_ be 0.
  1. If _largestUnit_ is ~week~, then
    1. Let _candidateWeeks_ be _sign_.
    1. Let _balancedDate_ be BalanceISODate(_regulatedDate_.[[Year]], _regulatedDate_.[[Month]], _regulatedDate_.[[Day]] + 7 × _candidateWeeks_).
    1. Let _y1_ be _balancedDate_.[[Year]].
    1. Let _m1_ be _balancedDate_.[[Month]].
    1. Let _d1_ be _balancedDate_.[[Day]].
    1. Let _surpasses_ be ISODateSurpasses(_sign_, _y1_, _m1_, _d1_, _two_).
    1. If _surpasses_ is *false*,
      1. Set _weeks_ to _candidateWeeks_.
      1. Set _candidateWeeks_ to _candidateWeeks_ + sign.
      1. Set _balancedDate_ to BalanceISODate(_balancedDate_.[[Year]], _balancedDate_.[[Month]], _balancedDate_.[[Day]] + 7 × _sign_).
      1. Goto the previous "Let y1 be" line.
  1. Let _days_ be 0.
  1. Let _candidateDays_ be _sign_.
  1. Let _balancedDate_ be BalanceISODate(_regulatedDate_.[[Year]], _regulatedDate_.[[Month]], _regulatedDate_.[[Day]] + 7 × _weeks_ + _candidateDays_).
  1. Let _y1_ be _balancedDate_.[[Year]].
  1. Let _m1_ be _balancedDate_.[[Month]].
  1. Let _d1_ be _balancedDate_.[[Day]].
  1. Let _surpasses_ be ISODateSurpasses(_sign_, _y1_, _m1_, _d1_, _two_).
  1. If _surpasses_ is *false*,
    1. Set _days_ to _candidateDays_.
    1. Set _candidateDays_ to _candidateDays_ + _sign_.
    1. Set _balancedDate_ to BalanceISODate(_balancedDate_.[[Year]], _balancedDate_.[[Month]], _balancedDate_.[[Day]] + _sign_).
    1. Goto the previous "Let y1 be" line.
  1. Return ! CreateDateDurationRecord(_years_, _months_, _weeks_, _days_).

Now I will merge lines, rename some variables, and return to regular Repeat statements:

  1. Let _sign_ be -CompareISODate(_one_, _two_).
  1. If _sign_ = 0, return ZeroDateDuration().
  1. Let _years_ be 0.
  1. If _largestUnit_ is ~year~, then
    1. Let _candidateYears_ be _sign_.
    1. Repeat, while ISODateSurpasses(_sign_, _one_.[[Year]] + _candidateYears_, _one_.[[Month]], _one_.[[Day]], _two_) is *false*,
      1. Set _years_ to _candidateYears_.
      1. Set _candidateYears_ to _candidateYears_ + _sign_.
  1. Let _months_ be 0.
  1. If _largestUnit_ is ~year~ or _largestUnit_ is ~month~, then
    1. Let _candidateMonths_ be _sign_.
    1. Let _intermediate_ be BalanceISOYearMonth(_one_.[[Year]] + _years_, _one_.[[Month]] + _candidateMonths_).
    1. Repeat, while ISODateSurpasses(_sign_, _intermediate_.[[Year]], _intermediate_.[[Month]], _one_.[[Day]], _two_) is *false*,
      1. Set _months_ to _candidateMonths_.
      1. Set _candidateMonths_ to _candidateMonths_ + _sign_.
      1. Set _intermediate_ to BalanceISOYearMonth(_intermediate_.[[Year]], _intermediate_.[[Month]] + _sign_).
  1. Set _intermediate_ to BalanceISOYearMonth(_one_.[[Year]] + _years_, _one_.[[Month]] + _months_).
  1. Let _constrained_ be ! RegulateISODate(_intermediate_.[[Year]], _intermediate_.[[Month]], _one_.[[Day]], ~constrain~).
  1. Let _weeks_ be 0.
  1. If _largestUnit_ is ~week~, then
    1. Let _candidateWeeks_ be _sign_.
    1. Set _intermediate_ to BalanceISODate(_constrained_.[[Year]], _constrained_.[[Month]], _constrained_.[[Day]] + 7 × _candidateWeeks_).
    1. Repeat, while ISODateSurpasses(_sign_, _intermediate_.[[Year]], _intermediate_.[[Month]], _intermediate_.[[Day]], _two_) is *false*,
      1. Set _weeks_ to _candidateWeeks_.
      1. Set _candidateWeeks_ to _candidateWeeks_ + sign.
      1. Set _intermediate_ to BalanceISODate(_intermediate_.[[Year]], _intermediate_.[[Month]], _intermediate_.[[Day]] + 7 × _sign_).
  1. Let _days_ be 0.
  1. Let _candidateDays_ be _sign_.
  1. Set _intermediate_ to BalanceISODate(_constrained_.[[Year]], _constrained_.[[Month]], _constrained_.[[Day]] + 7 × _weeks_ + _candidateDays_).
  1. Repeat, while ISODateSurpasses(_sign_, _intermediate_.[[Year]], _intermediate_.[[Month]], _intermediate_.[[Day]], _two_) is *false*,
    1. Set _days_ to _candidateDays_.
    1. Set _candidateDays_ to _candidateDays_ + _sign_.
    1. Set _intermediate_ to BalanceISODate(_intermediate_.[[Year]], _intermediate_.[[Month]], _intermediate_.[[Day]] + _sign_).
  1. Return ! CreateDateDurationRecord(_years_, _months_, _weeks_, _days_).

And voilà! We have returned to the current spec text.

This concludes my proof that this PR is editorial.

sffc and others added 2 commits August 4, 2025 06:32
For the weeks and days part, we already used a shortcut instead of
looping, so we keep it the same as before. For the years and months
calculation, we change to the new definition of ISODateSurpasses.
@ptomato ptomato force-pushed the rewrite-ISODateSurpasses branch from b6df80c to 52e277c Compare August 4, 2025 14:26
Copy link
Collaborator

@ptomato ptomato left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proof seems correct, thanks 😄

I have updated the reference code to use the new definition of ISODateSurpasses.

@ptomato ptomato merged commit f6e78d0 into tc39:main Aug 4, 2025
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants