Polyfill: GetNamedTimeZoneNextTransition misses a transition in the last search window before the max Instant
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from tc39/proposal-temporal
-
Difficulty 1/5 Under an hour Newbie friendliness 92/100
tc39/proposal-temporal#3330 · 1 comment ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
tc39/proposal-temporal#3329 ·
-
Difficulty 3/5 1-2 days Newbie friendliness 76/100
tc39/proposal-temporal#3327 · 1 comment · 1 reaction ·
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
tc39/proposal-temporal#3324 ·
-
Difficulty 3/5 1-2 days Newbie friendliness 55/100
tc39/proposal-temporal#3322 ·
All issues in tc39/proposal-temporal
Similar issues
-
clawsweeper:fix-shape-clear clawsweeper:queueable-fix clawsweeper:source-repro impact:ux-friction issue-rating: 🦞 diamond lobster no-stale P3
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
tvOS
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
bvaughn/react-resizable-panels#751 · 1 comment ·
-
www.wiwo.de OpenN: AdGuard Browser Extension P3: Medium T: Annoyance
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
AdguardTeam/AdguardFilters#242026 ·
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
Difficulty 1/5 Under an hour Newbie friendliness 92/100