Skip to content

Polyfill: Fix extreme PlainYearMonth crash in non-ISO calendars - #369

Open
craig-o-curtis wants to merge 2 commits into
js-temporal:mainfrom
craig-o-curtis:port-3279-calendar-limits-clamp
Open

craig-o-curtis wants to merge 2 commits into
js-temporal:mainfrom
craig-o-curtis:port-3279-calendar-limits-clamp

Conversation

@craig-o-curtis

Copy link
Copy Markdown

Ports two tc39/proposal-temporal commits, one commit each:

What was wrong

To convert a non-ISO calendar date to an ISO date, calendarToIsoDate() guesses an ISO date, converts the guess back to the calendar with Intl.DateTimeFormat, and corrects it. Near either end of the supported range, a guess can fall outside the range of the JavaScript Date object. Intl.DateTimeFormat can't format such a date, so the conversion threw RangeError: Invalid ISO date, even though the requested date itself is in range. On main, all of these throw:

Temporal.PlainDate.from({ calendar: 'hebrew', year: 279517, month: 10, day: 11 }, { overflow: 'reject' });
Temporal.PlainDate.from({ calendar: 'islamic-civil', year: -280804, month: 3, day: 21 }, { overflow: 'reject' });
Temporal.PlainYearMonth.from({ calendar: 'islamic-civil', year: 283583, monthCode: 'M06' });
Temporal.PlainYearMonth.from({ calendar: 'ethioaa', year: 281247, monthCode: 'M06' });
Temporal.PlainYearMonth.from({ calendar: 'indian', year: 275682, monthCode: 'M07' });

What this changes

  • First commit: the first guess, and the correction after it, are clamped to the supported range (-271821-04-19 to +275760-09-13). This fixes the first two lines above.
  • Second commit: for the islamic calendars, coptic/ethiopic/ethioaa, and indian, a guess outside the Date range is moved into range by whole calendar cycles (islamic 10631 days = 30 years, coptic/ethiopic/ethioaa 1461 days = 4 years, indian 4 years), converted, and the year moved back. This fixes the last three lines. It also fixes islamic-civil until() near both ends of the range, which threw on main. Dates inside the range take the same code path as before.

Not fixed by this PR

These behave the same on main and on this branch:

Testing

Node 24.21.0 (ICU 78.3, CLDR 48): jest passes, and test262 at the pinned version passes with no new failures or unexpected passes (intl402/Temporal 330 passed, 8 expected failures; built-ins/Temporal 4330 passed).

The test262 tests for this fix, intl402/Temporal/{PlainDate,PlainDateTime,PlainYearMonth,ZonedDateTime}/from/extreme-dates.js and .../prototype/withCalendar/extreme-dates.js, are newer than our pinned test262. I ran them by hand, at the test262 commit proposal-temporal used for af0cb4b, and counted each calendar row separately:

Build Failing rows (of 112)
main 59
this PR 42
this PR with #361 applied on top 8

With this PR, every remaining failure is one of the "Not fixed by this PR" items or is fixed by #361: indian before year 1, hebrew negative leap years, the coptic/ethiopic eras, and chinese/dangi under ICU 78. With #361 on top (it applies cleanly), only the chinese/dangi far-future and far-past rows fail. Upstream's removal of PlainYearMonth/from/extreme-dates.js from its expected failures needs no change here, because that test isn't in our pinned test262.

Differences from upstream (TypeScript)

  • The shifted isoToCalendarDate functions call HelperBase.prototype.isoToCalendarDate, where upstream calls nonIsoHelperBase.isoToCalendarDate.
  • They are installed as override class fields, the same way OrthodoxBaseHelper already installs OrthodoxOps.
  • ES.AddDaysToISODate is this file's addDaysISO.

ptomato and others added 2 commits September 18, 2026 10:33
…sion

In the iterative guessing process of converting a calendar date to an
ISO date, we can run afoul of the supported range when either of two
things happen:

- A calendar helper's estimateISODate() returns an estimate of 01-01 of
  whatever year, which may be -271821
- diffTotalDaysEstimate is positive and takes us past the end of the
  range

In the places where either of these two things happen, clamp the ISO
date to the supported range.

It's possible there are other places that could error, but I haven't
been able to find cases that would make them do so.

UPSTREAM_COMMIT=a41eb67481d51cc5e0fe3f95136c978126a6c9c6
* Polyfill: Fix extreme PlainYearMonth crash in non-ISO calendars

Fixes #3251.

* Deduplicate legacy Date range boundary logic

Replace isOutOfLegacyDateRange() with compareISODateToLegacyDateRange(),
a three-way comparison returning -1/0/1. Rewrite clampISODate() to use
it, removing the duplicated boundary conditions.

Thanks to ptomato for the suggestion.

NOTE: (this info below wasn't in the upstream commit)
"Fixes #3251" above refers to tc39/proposal-temporal#3251, not to an
issue in this repository.

What this changes: converting a calendar date to an ISO date searches
by trial ISO dates, and near the ends of the supported range some of
those trial dates are outside the range of the JavaScript Date object.
Intl.DateTimeFormat cannot format those, so the conversion threw
"RangeError: Invalid ISO date". In the islamic calendars, the coptic,
ethiopic and ethioaa calendars and the indian calendar, such a date is
now moved into range by a whole number of calendar cycles (islamic:
10631 days = 30 years; coptic/ethiopic/ethioaa: 1461 days = 4 years;
indian: 4 years), converted, and the calendar year moved back by the
same number of years. Dates inside the range are converted as before.

Still broken after this commit (unchanged, not part of the port):
- hebrew and persian: until()/since() and Duration round() with
  relativeTo can still throw "Invalid ISO date" near the end of the
  range, e.g. persian +275760-07-01 until +275760-09-13 by months.
- chinese and dangi: years far from the present (e.g. year 250000)
  throw "TypeError: Internal error. Icu error." from ICU. That is
  fixed in proposal-temporal by tc39/proposal-temporal#3277, which is
  not ported here.

TypeScript port: the shifted functions call
HelperBase.prototype.isoToCalendarDate where upstream calls
nonIsoHelperBase.isoToCalendarDate, are installed as override class
fields, and use this file's addDaysISO for ES.AddDaysToISODate.

UPSTREAM_COMMIT=af0cb4b17fad3bafbe905892abda5fb91054a7ea
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.

Polyfill: Exception incorrectly thrown with maximum allowed PlainYearMonth in some non-ISO calendars

3 participants