Skip to content

Replace BalanceISODate(Time) and rearrange time zone offset checks #3014

Closed
anba wants to merge 15 commits intotc39:mainfrom
anba:balance-iso-date
Closed

Replace BalanceISODate(Time) and rearrange time zone offset checks #3014
anba wants to merge 15 commits intotc39:mainfrom
anba:balance-iso-date

Conversation

@anba
Copy link
Contributor

@anba anba commented Oct 17, 2024

Disclaimer: I haven't yet implemented these changes, so it's possible that there are still some bugs in the proposed changes.

  1. The first three commits replace BalanceISODateTime and BalanceISODate with less general operations.
    • BalanceISODateTime is only used to add a time zone offset to a date-time. It seems like all but one occurrence can be replaced by plain subtraction. For that one caller where subtraction isn't possible, either rename BalanceISODateTime to AddOffsetNanosecondsToISODateTime or alternatively just inline it into GetISODateTimeFor. Commits for both alternatives are prepared.
    • And BalanceISODate is only used to add a days amount to an ISO Date record. Replace it with AddDaysToISODate, so it's easier to see what this operation does.
  2. The remaining commits reorder CheckISODaysRange calls to happen earlier, so it's easier to see (and implement) when ISO Date-Time records contain out-of-range values.
    • CheckISODaysRange currently allows inputs from nsMinInstant (inclusive) to nsMaxInstant + nsPerDay (exclusive). That looks like a bug, I think the lower limit should be nsMinInstant - nsPerDay (exclusive). This range can be checked through ISODateTimeWithinLimits.
    • In two places ISODateTimeWithinLimits is used to check GetUTCEpochNanoseconds inputs instead of CheckISODaysRange. Either use CheckISODaysRange consistently or inline CheckISODaysRange everywhere. Commits for both alternative are also prepared here.
    • All changes are editorial, because they only reorder when RangeError exceptions are thrown, but this isn't visible from user-code, because no other side-effects can happen.

@codecov
Copy link

codecov bot commented Oct 17, 2024

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.96%. Comparing base (53731c2) to head (014eb8b).
⚠️ Report is 32 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3014   +/-   ##
=======================================
  Coverage   96.96%   96.96%           
=======================================
  Files          22       22           
  Lines       10219    10219           
  Branches     1841     1841           
=======================================
  Hits         9909     9909           
  Misses        261      261           
  Partials       49       49           

☔ 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.

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.

Thanks for the PR! I like most of the changes but some of the stuff around CheckISODaysRange I'm not sure is correct. I implemented it in this repo's reference code and I get different behaviour around the edges of the representable range.

1. Let _epochNanoseconds_ be GetUTCEpochNanoseconds(_balanced_).
1. Let _isoDate_ be CreateISODateRecord(_parsed_.[[Year]], _parsed_.[[Month]], _parsed_.[[Day]]).
1. Let _isoDateTime_ be CombineISODateAndTimeRecord(_isoDate_, _time_).
1. Perform ? CheckISODaysRange(_isoDate_).
Copy link
Collaborator

Choose a reason for hiding this comment

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

In some cases, this will be incorrect, because the parsed YMD may be different from the actual YMD after balancing. e.g. Temporal.Instant.from("-271821-04-19T23:00-01:00"), the parsed YMD is out of range, but the instant itself is in range.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This shouldn't be a problem after 77bef95 when the allowed input range is changed from [nsMinInstant, nsMaxInstant + nsPerDay) to (nsMinInstant - nsPerDay, nsMaxInstant + nsPerDay).

1. If _offsetBehaviour_ is ~wall~, or _offsetBehaviour_ is ~option~ and _offsetOption_ is ~ignore~, then
1. Return ? GetEpochNanosecondsFor(_timeZone_, _isoDateTime_, _disambiguation_).
1. If _offsetBehaviour_ is ~exact~, or _offsetBehaviour_ is ~option~ and _offsetOption_ is ~use~, then
1. Let _balanced_ be BalanceISODateTime(_isoDate_.[[Year]], _isoDate_.[[Month]], _isoDate_.[[Day]], _time_.[[Hour]], _time_.[[Minute]], _time_.[[Second]], _time_.[[Millisecond]], _time_.[[Microsecond]], _time_.[[Nanosecond]] - _offsetNanoseconds_).
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ditto.

Comment on lines +122 to +123
1. If abs(ISODateToEpochDays(_isoDate_.[[Year]], _isoDate_.[[Month]] - 1, _isoDate_.[[Day]])) > 10<sup>8</sup>, then
1. Let _dateTime_ be CombineISODateAndTimeRecord(_isoDate_, MidnightTimeRecord()).
1. If ISODateTimeWithinLimits(_dateTime_) is *false*, then
Copy link
Collaborator

Choose a reason for hiding this comment

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

This isn't equivalent. (I know that's intentional from your description, but I'm not sure it's correct.) CheckISODaysRange has that weird range because that's the range that will avoid running into the undefined behaviour from tc39/ecma262#1087.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The current CheckISODaysRange definition effectively allows inputs in the range [nsMinInstant, nsMaxInstant + nsPerDay), but I think the allowed range should instead be (nsMinInstant - nsPerDay, nsMaxInstant + nsPerDay).

Confusingly the ECMA-262 spec sometimes mentions that inputs resp. outputs are time values (i.e. times within the [nsMinInstant, nsMaxInstant] range), but the actually allowed times are with the local time zone offset applied. For example https://tc39.es/ecma262/#sec-date.prototype.getdate:

  1. Return DateFromTime(LocalTime(t)).

LocalTime(t) returns a local time zone adjusted value, which can be outside the valid time value bounds, but per the DateFromTime description, DateFromTime should only be called with time values.

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.

I took another look at this today and tried to implement it in JS. I do get some differing test results, though that may be my mistake. If you implement it like this, do you pass e.g. all of the relativeto-string-limits.js tests?

I think the one thing that I am hesitating about, is that I prefer to keep CheckISODaysRange separate from ISODateTimeWithinLimits, so that we can later remove CheckISODaysRange when tc39/ecma262#1087 is fixed. (Depending on what behaviour is chosen for what is currently unspecified behaviour.)

A more minor point is that I'd prefer, in a series of refactor commits, that every commit leaves the whole in a correct state. This is not the case for the first commit "Replace BalanceISODateTime with subtraction" which gives different behaviour on strings such as Temporal.Instant.from("-271821-04-19T23:00-01:00"). Is there any way to arrange the order of the commits so that this doesn't happen?

As I said in the previous review, I think a number of these refactors are worth having, so it would be good to get this in. The two main sticking points for me are that I'd like to be more certain that the refactors are not changing any behaviour, and I'd prefer to keep CheckISODaysRange.

1. Let _adjustedDate_ be _isoDateTime2_.[[ISODate]].
1. If _timeSign_ = _dateSign_, then
1. Set _adjustedDate_ to BalanceISODate(_adjustedDate_.[[Year]], _adjustedDate_.[[Month]], _adjustedDate_.[[Day]] + _timeSign_).
1. If _timeSign_ = -_dateSign_, then
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure this negation is correct.

@anba
Copy link
Contributor Author

anba commented Feb 20, 2025

I think the one thing that I am hesitating about, is that I prefer to keep CheckISODaysRange separate from ISODateTimeWithinLimits, so that we can later remove CheckISODaysRange when tc39/ecma262#1087 is fixed. (Depending on what behaviour is chosen for what is currently unspecified behaviour.)

The proposed changes are unrelated to tc39/ecma262#1087. Instead they are related to tc39/ecma262#3464.

@ptomato
Copy link
Collaborator

ptomato commented Nov 6, 2025

I rebased this and will take another look in the context of https://bugzilla.mozilla.org/show_bug.cgi?id=1998020

@catamorphism
Copy link
Contributor

I went through and implemented the changes in the polyfill. Some tests failed and necessitated spec changes. #3184 shows what had to be changed.

@ptomato
Copy link
Collaborator

ptomato commented Dec 11, 2025

We discussed this in the Temporal meeting of 2025-12-11. First of all, thanks for bearing with us for this long. The conclusion is that we'll adopt some of the editorial refactors from this PR (which @catamorphism has split out in #3204) but we would prefer to keep CheckISODaysRange as it is.

@anba If you still feel it is necessary to make a normative change, please open a narrower PR that just contains the normative change.

@ptomato ptomato closed this Dec 11, 2025
@fabon-f
Copy link
Contributor

fabon-f commented Jan 22, 2026

Is it planned to fix CheckISODaysRange behavior as a spec bug, or did champions already agree to leave it as-is?

Personally I feel current spec is inconsistent. For example, off-by-one error in CheckISODaysRange breaks the roundtrippability between serialization and deserialization:

const zdt = new Temporal.ZonedDateTime(-8640000000000000000000n, 'America/New_York');
Temporal.ZonedDateTime.from(zdt.toString()); // RangeError!!!

@ptomato
Copy link
Collaborator

ptomato commented Jan 23, 2026

Is it planned to fix CheckISODaysRange behavior as a spec bug, or did champions already agree to leave it as-is?

Personally I feel current spec is inconsistent. For example, off-by-one error in CheckISODaysRange breaks the roundtrippability between serialization and deserialization:

const zdt = new Temporal.ZonedDateTime(-8640000000000000000000n, 'America/New_York');
Temporal.ZonedDateTime.from(zdt.toString()); // RangeError!!!

The bug is in MakeDay which behaves differently in different JS implementations (tc39/ecma262#1087) because of underspecified language. Obviously we'd like this to be fixed, but are not going to block Temporal on it. CheckISODaysRange exists because we think it's preferable that code like the above snippet throws, instead of returning potentially different results in each browser.

@fabon-f
Copy link
Contributor

fabon-f commented Jan 23, 2026

ISODateWithinLimits accepts -271821-04-19 but CheckISODaysRange rejects it. Is such discrepancy also intentional?

The spec says ISODateTimeWithinLimits can call GetUTCEpochNanoseconds with the date time such as -271821-04-19T00:00:01 and expects the correct result. Is there any reason to reject -271821-04-19 in CheckISODaysRange?

@ptomato
Copy link
Collaborator

ptomato commented Feb 5, 2026

@fabon-f I appreciate you digging into this as it helps me put all the information into one place 😄

ISODateWithinLimits accepts -271821-04-19 but CheckISODaysRange rejects it. Is such discrepancy also intentional?

Yes.

The spec says ISODateTimeWithinLimits can call GetUTCEpochNanoseconds with the date time such as -271821-04-19T00:00:01 and expects the correct result.

You are probably right that this is inconsistent, and it would be safer to reject -271821-04-19T00:00:01 in ISODateTimeWithinLimits. But if implementations seem to be doing this correctly, I'd say we probably don't need to fix it at this point. (Technically even a date as late as -271821-04-30 hits underspecified behaviour in MakeDay, so CheckISODaysRange could be even worse...)

The other inconsistency you mentioned

const zdt = new Temporal.ZonedDateTime(-8640000000000000000000n, 'America/New_York');
Temporal.ZonedDateTime.from(zdt.toString()); // RangeError!!!

is unfortunate IMO, but I think it's the best choice given the constraints. If you supply the epoch ns directly, it's in range. If you parse the string, the wall-clock part is out of range for MakeDay, then you add the 4h offset to get back in range. If MakeDay was fixed, this could be changed to not throw.

Here is a list of commits with further context:

@anba
Copy link
Contributor Author

anba commented Mar 3, 2026

As mentioned last year:

The proposed changes are unrelated to tc39/ecma262#1087. Instead they are related to tc39/ecma262#3464.

The current Temporal spec tries to constrain milliseconds to the time value range ±8,640,000,000,000,000, because that's the limit spec'ed in ECMA-262 for some Date abstract operations. But that's a bug in ECMA-262, the actual permissible range is ±8,640,000,086,399,999 milliseconds for some of these operations. (In fact, ECMA-262 is already calling some abstract operations which have parameters defined as time value with values outside the time value range. That's the spec bug tc39/ecma262#3464 tries to fix.) Anyway, Temporal tries to be strict and only pass valid time values, so it imposes on itself the ±8,640,000,000,000,000 limit. I'd say it's now too late to try fixing this in Temporal, instead let's simply wait until Temporal is integrated into ECMA-262, then I (or someone else) can finish tc39/ecma262#3464, and then CheckISODaysRange can be replaced with ISODateTimeWithinLimits.

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.

4 participants