Polyfill: GetNamedTimeZoneNextTransition misses a transition in the last search window before the max Instant

Open Beginner friendly
#3,328 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
84/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
javascript
Domain
web-dev

Research direction

Start in polyfill/lib/ecmascript.mjs at GetNamedTimeZoneNextTransition and reproduce the America/Santiago cases listed in the issue. Verify that transitions near the maximum Instant are found, the listed startOfDay and hoursInDay calls return the expected values, and the 2020–2030 lookup results remain unchanged.

Written by the indexing model from the issue text.

Description

GetNamedTimeZoneNextTransition (polyfill/lib/ecmascript.mjs l.2444–2446 at e8cc03fc970a65a3359e8870e3b35e687ac94e55) gives up when the next search window would pass the maximum instant, without looking at the part of the window that is still in range:

while (leftOffsetNs === rightOffsetNs && leftMs < uppercap) {
  rightMs = leftMs + searchWindow;
  if (rightMs > MS_MAX) return null;

So a transition shortly before the maximum instant is missed. America/Santiago has one at +275760-09-07T04:00:00Z (-04:00-03:00, local midnight is skipped), six days before the maximum:

Temporal.Instant.from('+275760-09-01T00:00:00Z').toZonedDateTimeISO('America/Santiago').getTimeZoneTransition('next');
// null, expected +275760-09-07T01:00:00-03:00[America/Santiago]

GetStartOfDay passes the null on, so the start of that day, and hoursInDay on it and the day before, throw:

Call Polyfill on main Expected
Temporal.Instant.from('+275760-09-01T00:00:00Z').toZonedDateTimeISO('America/Santiago').getTimeZoneTransition('next') null +275760-09-07T01:00:00-03:00[America/Santiago]
Temporal.PlainDate.from('+275760-09-07').toZonedDateTime('America/Santiago') TypeError: Cannot read properties of null (reading 'lesser') +275760-09-07T01:00:00-03:00[America/Santiago]
Temporal.ZonedDateTime.from('+275760-09-07T12:00[America/Santiago]').startOfDay() TypeError: Cannot read properties of null (reading 'lesser') +275760-09-07T01:00:00-03:00[America/Santiago]
Temporal.ZonedDateTime.from('+275760-09-07T12:00[America/Santiago]').hoursInDay TypeError: Cannot read properties of null (reading 'value') 23
Temporal.ZonedDateTime.from('+275760-09-06T12:00[America/Santiago]').hoursInDay TypeError: Cannot read properties of null (reading 'subtract') 24
control: new Temporal.ZonedDateTime(8640000000000000000000n, 'America/Santiago').getTimeZoneTransition('previous') +275760-09-07T01:00:00-03:00[America/Santiago] same

The expected values are Chrome 153's native Temporal. They also follow from the spec: GetNamedTimeZoneNextTransition returns null only if there is no transition t with t ≤ nsMaxInstant, and here there is one. The control row shows that the polyfill finds it searching backwards.

It isn't limited to the last 19 days. Whether the lookup fails depends on where the 19-day steps from the starting point land. Starting from 00:00Z on each of the last 61 days (+275760-07-15+275760-09-13), getTimeZoneTransition('next') in America/Santiago returns null on 39 of them, the earliest being +275760-07-19.

Proposed fix

Clamp the last window to MS_MAX instead of returning:

--- a/polyfill/lib/ecmascript.mjs
+++ b/polyfill/lib/ecmascript.mjs
@@ -2441,9 +2441,10 @@ export function GetNamedTimeZoneNextTransition(id, epochNanoseconds) {
   let rightMs = leftMs;
   let rightOffsetNs = leftOffsetNs;
   const searchWindow = searchWindowForTransitions(id);
-  while (leftOffsetNs === rightOffsetNs && leftMs < uppercap) {
-    rightMs = leftMs + searchWindow;
-    if (rightMs > MS_MAX) return null;
+  // Search the last, partial window up to MS_MAX instead of giving up, so that
+  // a transition at or before nsMaxInstant is still found.
+  while (leftOffsetNs === rightOffsetNs && leftMs < uppercap && leftMs < MS_MAX) {
+    rightMs = MathMin(leftMs + searchWindow, MS_MAX);
     rightOffsetNs = GetNamedTimeZoneOffsetNanosecondsImpl(id, rightMs);
     if (leftOffsetNs === rightOffsetNs) {
       leftMs = rightMs;

leftMs still increases on every pass and stops at MS_MAX, where the existing if (leftOffsetNs === rightOffsetNs) return null; after the loop returns null as before. bisect then runs on a window that ends at or before MS_MAX, so the result is never past the maximum instant. Away from the maximum, MathMin changes nothing.

With this change on e8cc03fc97, all the rows above give the expected values. For nine zones (America/Santiago, America/New_York, Europe/London, Australia/Sydney, Pacific/Chatham, America/Asuncion, Asia/Tokyo, UTC, Asia/Kolkata), I checked getTimeZoneTransition('next') and ('previous'), the start of day and hoursInDay for each of the last 61 days: all of them match Chrome 153, apart from the wording of the RangeError messages on the last two days. next and previous from every day of 2020–2030 in the same zones (36,162 lookups) give the same output before and after the change, and they match Chrome.

GetNamedTimeZonePreviousTransition has the mirror-image early return at BEFORE_FIRST_DST (if (leftMs < BEFORE_FIRST_DST) return null;). It has no visible effect today: the earliest transition after BEFORE_FIRST_DST (1847-01-01) is Europe/London's on 1847-12-01, far more than one window later.

js-temporal/temporal-polyfill has the same code, and its production builds throw TypeError: Cannot read properties of null (reading 'sign') for these rows. I'll open the same change there and link it here.

Dominant language
HTML
Stars
3.7k
Forks
177
PR merge metrics
No merged PRs in 30d

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from tc39/proposal-temporal

All issues in tc39/proposal-temporal

Similar issues

More Web Dev issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.