Skip to content

Start the time zone transition search before the first transition in 1844 - #372

Closed
craig-o-curtis wants to merge 1 commit into
js-temporal:mainfrom
craig-o-curtis:fix-transitions-before-1847
Closed

craig-o-curtis wants to merge 1 commit into
js-temporal:mainfrom
craig-o-curtis:fix-transitions-before-1847

Conversation

@craig-o-curtis

Copy link
Copy Markdown

This is not a port: the fix isn't in tc39/proposal-temporal yet. The same bug is there, and I reported it as tc39/proposal-temporal#3330 with the same change, so a later sync can pick it up. It doesn't depend on #367#371 or #361. It touches the same file as #371 but a different part of it, and the two apply together cleanly.

What was wrong

GetNamedTimeZoneNextTransition() jumps forward to BEFORE_FIRST_DST (1847-01-01) and GetNamedTimeZonePreviousTransition() stops there, because 1847 was taken to be the year of the first transition in the TZDB (e70d632). It no longer is. Five zones change sides of the date line on 1844-12-31 and skip that local day: Asia/Manila (-15:56+08:04), Pacific/Guam and Pacific/Saipan (-14:21+09:39), Pacific/Kosrae (-13:08+10:52) and Pacific/Palau (-15:02+08:58). On main (production build):

Call main Expected
Temporal.Instant.from('1800-01-01T00:00:00Z').toZonedDateTimeISO('Asia/Manila').getTimeZoneTransition('next') 1899-09-06T12:00:00+08:00[Asia/Manila] 1845-01-01T00:00:00+08:04[Asia/Manila]
Temporal.Instant.from('1846-06-01T00:00:00Z').toZonedDateTimeISO('Asia/Manila').getTimeZoneTransition('previous') null 1845-01-01T00:00:00+08:04[Asia/Manila]
Temporal.PlainDate.from('1844-12-31').toZonedDateTime('Asia/Manila') 1899-09-06T12:00:00+08:00[Asia/Manila] 1845-01-01T00:00:00+08:04[Asia/Manila]
Temporal.ZonedDateTime.from('1844-12-30T12:00[Asia/Manila]').hoursInDay 479340.06444444443 24
Temporal.ZonedDateTime.from('1844-12-30T12:00[Asia/Manila]').round({ smallestUnit: 'day' }) 1844-12-30T00:00:00-15:56[Asia/Manila] 1845-01-01T00:00:00+08:04[Asia/Manila]
control: Temporal.Instant.from('1800-01-01T00:00:00Z').toZonedDateTimeISO('Europe/London').getTimeZoneTransition('next') 1847-12-01T00:01:15+00:00[Europe/London] same

The expected values are Chrome 153's native Temporal. The same calls in Guam, Saipan, Kosrae and Palau are wrong in the same way, with their 1901 transition in place of Manila's 1899 one. The offsets themselves were right, because they come from Intl.DateTimeFormat. Local 1844-12-31 doesn't exist in these zones, so GetStartOfDay() searches for the next transition after the day before. That search jumped to 1847 and found the next transition after it. Development builds give the same results.

What this changes

BEFORE_FIRST_DST moves to 1844-01-01, with a comment saying where the first transition is (lib/ecmascript.ts, one line plus a comment). That is almost a year before the first transition, as 1847-01-01 was before Europe/London's on 1847-12-01. So the early return null in GetNamedTimeZonePreviousTransition() still can't cut off a search window, which is 19 days at most. The only cost is up to three more years of 19-day steps when a search reaches the constant. Antarctica/Troll is the worst case (first transition in 2005): previous from just before that transition took 4.2–4.3 ms both before and after the change.

Not fixed by this PR

  • The constant is still a hard-coded assumption about the TZDB. If a future TZDB release adds an earlier transition, it will be missed the same way. In Chrome 153, getTimeZoneTransition('previous') from 1844-12-31T00:00Z is null in all 418 zones, so there is none today.
  • I didn't add a jest test. The expected values depend on the time zone data of the runtime's ICU, and older data doesn't have the 1844 transitions. Search the last partial window when looking for the next time zone transition #371 didn't add one either, for the same reason.

Testing

Node 24.21.0 (ICU 78.3, tzdata 2026c): jest 607 passed; eslint, prettier and tscheck clean. test262 at the pinned version: intl402/Temporal 330 passed and 8 expected failures, built-ins/Temporal 4330 passed.

I compared these rows with Chrome 153:

main this PR
The five zones: next from 1800, previous from 1846, 1848, 1900 and from 1 ns after the transition; toZonedDateTime, hoursInDay and startOfDay on 1844-12-29 … 1845-01-02; the skipped day with each disambiguation; add/subtract/round/until across it; controls in Europe/London and America/New_York, and 2026 lookups (157 rows) 36 wrong 0 wrong
All 418 zones in Intl.supportedValuesOf('timeZone'): first transition after 1800, previous from 1 ns after it, from 1845-06-01 and from 1850-01-01 (1,672 rows) 20 wrong (all in the five zones) 0 wrong

Every row outside the five zones (1,652 zone-scan rows and 20 controls) is byte-identical with and without this PR. So are next and previous from the first of every month of 2020–2030 in 24 zones (6,336 lookups). With #371 applied as well, all 1,829 rows still match Chrome 153.

I made the same change in tc39/proposal-temporal at e8cc03fc97 and got the same results there.

…1844

GetNamedTimeZoneNextTransition() jumps forward to BEFORE_FIRST_DST
(1847-01-01) and GetNamedTimeZonePreviousTransition() stops there,
because 1847 was taken to be the year of the first transition in the
TZDB. It no longer is: Asia/Manila, Pacific/Guam, Pacific/Saipan,
Pacific/Kosrae and Pacific/Palau change sides of the date line on
1844-12-31, skipping that local day. Those transitions were not found:

  Temporal.Instant.from('1800-01-01T00:00:00Z')
    .toZonedDateTimeISO('Asia/Manila')
    .getTimeZoneTransition('next');
  // 1899-09-06T12:00:00+08:00[Asia/Manila]
  // (expected 1845-01-01T00:00:00+08:04[Asia/Manila])

GetStartOfDay() uses this function for the skipped day, so
Temporal.PlainDate.from('1844-12-31').toZonedDateTime('Asia/Manila')
also gave the 1899 transition, and hoursInDay on 1844-12-30 was
479340.06444444443 instead of 24.

Move the constant to 1844-01-01. That is almost a year before the first
transition, as 1847-01-01 was before Europe/London's on 1847-12-01, so
the early return in GetNamedTimeZonePreviousTransition() still can't
cut off a search window (19 days at most).

Also present in tc39/proposal-temporal; reported as
tc39/proposal-temporal#3330.
@ptomato

ptomato commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Ditto, will apply it in proposal-temporal and pull it in later. Thanks 😄

@ptomato ptomato closed this Sep 21, 2026
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