Skip to content

Conversation

sffc
Copy link
Collaborator

@sffc sffc commented Jul 19, 2025

This changes CalendarDateUntil so that it is based purely on CalendarDateAdd instead of using ISO-specific AOs like ISODateSurpasses and RegulateISODate. It means that the same CalendarDateUntil can be used for both ISO and non-ISO calendars.

Please double- and triple-check my work. I haven't come up with a counter-example where behavior differs, but that doesn't mean there isn't one that I didn't think of. I have not implemented this in code.

@sffc sffc requested a review from ptomato July 19, 2025 02:48
Copy link

codecov bot commented Jul 19, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.85%. Comparing base (4b83ba3) to head (9004121).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3136   +/-   ##
=======================================
  Coverage   96.85%   96.85%           
=======================================
  Files          21       21           
  Lines        9983     9983           
  Branches     1829     1829           
=======================================
  Hits         9669     9669           
  Misses        268      268           
  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.

1. If _sign_ = 0, return ZeroDateDuration().
1. Let _duration_ be CreateDateDurationRecord(0, 0, 0, 0).
1. If _largestUnit_ is ~year~, then
1. Let _intermediate_ be _one_.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

_intermediate_ needs to be initialized to something that is on the same side of _two_ as _one_ is, so that the body of the loop runs at least once. I pick _one_ since it always satisfies that criterion.

Comment on lines 532 to 536
1. Let _intermediate_ be _one_.
1. Repeat, while CompareISODate(_intermediate_, _two_) != _sign_,
1. Set _duration_.[[Years]] to _duration_.[[Years]] + _sign_.
1. Set _intermediate_ to CalendarDateAdd(_calendar_, _one_, _duration_, ~constrain~).
1. Set _duration_.[[Years]] to _duration_.[[Years]] - _sign_.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This loop is better written as:

loop {
    duration.years += sign;
    let intermediate = CalendarDateAdd(calendar, one, duration, "constrain");
    if CompareISODate(intermediate, two) == sign {
        duration.years -= sign;
        break;
    }
}

but I wrote it like I did since we don't have do-while loops in ecmarkup.

Copy link
Member

Choose a reason for hiding this comment

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

Sure we do:

1. Let _done_ be *false*.
1. Assert: The following loop will terminate.
1. Repeat, while _done_ is *false*,
  1. Set _duration_.[[Years]] to _duration_.[[Years]] + _sign_.
  1. Let _intermediate_ be CalendarDateAdd(_calendar_, _one_, _duration_, ~constrain~).
  1. If CompareISODate(_intermediate_, _two_) is _sign_, then
    1. Set _duration_.[[Years]] to _duration_.[[Years]] - _sign_.
    1. Set _done_ to *true*.

Some examples:

1. Return ! CreateDateDurationRecord(_years_, _months_, _weeks_, _days_).
1. Return an implementation-defined Date Duration Record as described above.
1. Let _sign_ be -CompareISODate(_one_, _two_).
1. If _sign_ = 0, return ZeroDateDuration().
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Double-check that my signs are correct and not negated.

  1. If one < two:
    1. CompareISODate(one, two) == -1, so sign = 1
    2. Inside the loop, we increment the duration by 1
    3. The loop condition checks CompareISODate(intermediate, two):
      1. The first time, intermediate is one, so the return value is -1, which does not equal sign, so the loop runs
      2. At some point after that, intermediate will be greater than two, at which point the return value is 1, the loop terminates, and the duration is decremented back to the value before the excess occurred
      3. Note: If intermediate and two are ever equal, the loop will run one last time, and the value of duration that caused the two values to be equal will become the resolved duration
  2. If one > two:
    1. CompareISODate(one, two) == 1, so sign == -1
    2. (all other steps should still work, except with signs flipped)

1. Let _intermediate_ be _one_.
1. Repeat, while CompareISODate(_intermediate_, _two_) != _sign_,
1. Set _duration_.[[Years]] to _duration_.[[Years]] + _sign_.
1. Set _intermediate_ to ! CalendarDateAdd(_calendar_, _one_, _duration_, ~constrain~).
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I might need to refactor CalendarDateAdd in order to make it infallible. I guess it's possible that some dates really close to the boundary would resolve to intermediates that are out of bounds, but I don't want those to cause exceptions to be thrown.

@gibson042
Copy link
Member

@sffc Please review #2535 (expand the hidden content and search the page for "exhaust", because @ptomato wrote some great exhaustive scripts at e.g. #2535 (comment) ) and maybe also the resulting test262 coverage at tc39/test262#4004 . Simply put, guessing about the absence of a counterexample is unacceptable vs. taking a full calendar cycle such as the 1461 inclusive days from Gregorian 1970-01-01 to 1974-01-01 (inclusive) and evaluating a full cross product for each largestUnit of ['month', 'year', 'week', 'day'].

@sffc
Copy link
Collaborator Author

sffc commented Jul 19, 2025

Ok, this PR isn't editorial then. ☹️ I wish we had written it to be calendar-agnostic because it is currently ISO-specific and I'm struggling to figure out a more general formulation, especially one that works with leap months.

@sffc sffc marked this pull request as draft July 19, 2025 18:27
@sffc
Copy link
Collaborator Author

sffc commented Jul 19, 2025

I think the key AO ISODateSurpasses can likely be re-written in a calendar-agnostic way, but needs some more time to do it carefully. It can still work in terms of years and days, but it needs to use month codes instead of months.

I also still want to explore whether I can make CalendarDateUntil sit on top of CalendarDateAdd and be functionally equivalent. I need to write a program like @ptomato did.

@sffc
Copy link
Collaborator Author

sffc commented Jul 21, 2025

OK, I think I managed to get this working. See #3138 for the first incremental editorial change.

@sffc
Copy link
Collaborator Author

sffc commented Aug 4, 2025

I think I'm able to achieve my goal using the framework in tc39/proposal-intl-era-monthcode#69 and #3138. So, I will close this PR. I may come back with more smaller editorial PRs as necessary.

@sffc sffc closed this Aug 4, 2025
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