Skip to content

Commit 08d214e

Browse files
committed
Support Node 20 and work around ICU calendar bug
Node 20 (ICU 73.1, actually) introduced a bug that broke some tests. This PR ensures that our tests will continue to run after other users update their CI to Node 20. See https://bugs.chromium.org/p/chromium/issues/detail?id=1451943 for the bug. https://bugs.chromium.org/p/chromium/issues/detail?id=1173158 is the root cause issue that was made worse in ICU 73.1. The fix was to avoid using Intl.DateTimeFormat in the polyfill for Gregorian-based calendars. A nice side effect of this is that these calendars now run tests faster and no longer fail for dates before 1582-10-15.
1 parent 8b94fd6 commit 08d214e

File tree

5 files changed

+52
-59
lines changed

5 files changed

+52
-59
lines changed

.github/workflows/deploy.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ jobs:
1010
- uses: actions/checkout@v3
1111
with:
1212
persist-credentials: false
13-
- name: use node.js v19.x
13+
- name: use node.js v20.x
1414
uses: actions/setup-node@v3
1515
with:
16-
node-version: 19.x
16+
node-version: 20.x
1717
- run: npm ci
1818
- run: npm run build
1919
- uses: JamesIves/[email protected]

.github/workflows/lint.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ jobs:
55
runs-on: ubuntu-latest
66
steps:
77
- uses: actions/checkout@v3
8-
- name: use node.js v19.x
8+
- name: use node.js v20.x
99
uses: actions/setup-node@v3
1010
with:
11-
node-version: 19.x
11+
node-version: 20.x
1212
- run: npm ci
1313
- run: npm run lint
1414
- run: npm run build:spec

.github/workflows/test.yml

+8-8
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ jobs:
99
runs-on: ubuntu-latest
1010
steps:
1111
- uses: actions/checkout@v3
12-
- name: use node.js v19.x
12+
- name: use node.js v20.x
1313
uses: actions/setup-node@v3
1414
with:
15-
node-version: 19.x
15+
node-version: 20.x
1616
- run: npm ci
1717
- run: npm run test-demitasse
1818
env:
@@ -23,10 +23,10 @@ jobs:
2323
- uses: actions/checkout@v3
2424
with:
2525
submodules: true
26-
- name: use node.js v19.x
26+
- name: use node.js v20.x
2727
uses: actions/setup-node@v3
2828
with:
29-
node-version: 19.x
29+
node-version: 20.x
3030
- run: npm ci
3131
- run: npm run codecov:test262
3232
env:
@@ -35,20 +35,20 @@ jobs:
3535
runs-on: ubuntu-latest
3636
steps:
3737
- uses: actions/checkout@v3
38-
- name: use node.js v19.x
38+
- name: use node.js v20.x
3939
uses: actions/setup-node@v3
4040
with:
41-
node-version: 19.x
41+
node-version: 20.x
4242
- run: npm ci
4343
- run: npm run test-cookbook
4444
test-validstrings:
4545
runs-on: ubuntu-latest
4646
steps:
4747
- uses: actions/checkout@v3
48-
- name: use node.js v19.x
48+
- name: use node.js v20.x
4949
uses: actions/setup-node@v3
5050
with:
51-
node-version: 19.x
51+
node-version: 20.x
5252
- run: npm ci
5353
- run: |
5454
cd polyfill

polyfill/lib/calendar.mjs

+39-46
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ const nonIsoHelperBase = {
544544
day: 'numeric',
545545
month: 'numeric',
546546
year: 'numeric',
547-
era: this.eraLength,
547+
era: 'short',
548548
timeZone: 'UTC'
549549
});
550550
}
@@ -763,11 +763,12 @@ const nonIsoHelperBase = {
763763
// If the estimate is in the same year & month as the target, then we can
764764
// calculate the result exactly and short-circuit any additional logic.
765765
// This optimization assumes that months are continuous. It would break if
766-
// a calendar skipped days, like the Julian->Gregorian switchover. But the
767-
// only ICU calendars that currently skip days (japanese/roc/buddhist) is
766+
// a calendar skipped days, like the Julian->Gregorian switchover. But
767+
// current ICU calendars only skip days (japanese/roc/buddhist) because of
768768
// a bug (https://bugs.chromium.org/p/chromium/issues/detail?id=1173158)
769-
// that's currently detected by `checkIcuBugs()` which will throw. So
770-
// this optimization should be safe for all ICU calendars.
769+
// that's currently worked around by a custom calendarToIsoDate
770+
// implementation in those calendars. So this optimization should be safe
771+
// for all ICU calendars.
771772
let testIsoEstimate = this.addDaysIso(isoEstimate, diffDays);
772773
if (date.day > this.minimumMonthLength(date)) {
773774
// There's a chance that the calendar date is out of range. Throw or
@@ -981,11 +982,11 @@ const nonIsoHelperBase = {
981982
// Add enough days to roll over to the next month. One we're in the next
982983
// month, we can calculate the length of the current month. NOTE: This
983984
// algorithm assumes that months are continuous. It would break if a
984-
// calendar skipped days, like the Julian->Gregorian switchover. But the
985-
// only ICU calendars that currently skip days (japanese/roc/buddhist) is a
986-
// bug (https://bugs.chromium.org/p/chromium/issues/detail?id=1173158)
987-
// that's currently detected by `checkIcuBugs()` which will throw. So this
988-
// code should be safe for all ICU calendars.
985+
// calendar skipped days, like the Julian->Gregorian switchover. But current
986+
// ICU calendars only skip days (japanese/roc/buddhist) because of a bug
987+
// (https://bugs.chromium.org/p/chromium/issues/detail?id=1173158) that's
988+
// currently worked around by a custom calendarToIsoDate implementation in
989+
// those calendars. So this code should be safe for all ICU calendars.
989990
const { day } = calendarDate;
990991
const max = this.maximumMonthLength(calendarDate);
991992
const min = this.minimumMonthLength(calendarDate);
@@ -1043,9 +1044,6 @@ const nonIsoHelperBase = {
10431044
);
10441045
return duration.days;
10451046
},
1046-
// The short era format works for all calendars except Japanese, which will
1047-
// override.
1048-
eraLength: 'short',
10491047
// All built-in calendars except Chinese/Dangi and Hebrew use an era
10501048
hasEra: true,
10511049
monthDayFromFields(fields, overflow, cache) {
@@ -1575,24 +1573,27 @@ const makeHelperGregorian = (id, originalEras) => {
15751573
const { anchorEra } = this;
15761574
const isoYearEstimate = year + anchorEra.isoEpoch.year - (anchorEra.hasYearZero ? 0 : 1);
15771575
return ES.RegulateISODate(isoYearEstimate, month, day, 'constrain');
1578-
},
1579-
// Several calendars based on the Gregorian calendar use Julian dates (not
1580-
// proleptic Gregorian dates) before the Julian switchover in Oct 1582. See
1581-
// https://bugs.chromium.org/p/chromium/issues/detail?id=1173158.
1582-
v8IsVulnerableToJulianBug: new Date('+001001-01-01T00:00Z')
1583-
.toLocaleDateString('en-US-u-ca-japanese', { timeZone: 'UTC' })
1584-
.startsWith('12'),
1585-
calendarIsVulnerableToJulianBug: false,
1586-
checkIcuBugs(isoDate) {
1587-
if (this.calendarIsVulnerableToJulianBug && this.v8IsVulnerableToJulianBug) {
1588-
const beforeJulianSwitch = ES.CompareISODate(isoDate.year, isoDate.month, isoDate.day, 1582, 10, 15) < 0;
1589-
if (beforeJulianSwitch) {
1590-
throw new RangeError(
1591-
`calendar '${this.id}' is broken for ISO dates before 1582-10-15` +
1592-
' (see https://bugs.chromium.org/p/chromium/issues/detail?id=1173158)'
1593-
);
1594-
}
1595-
}
1576+
}
1577+
});
1578+
};
1579+
1580+
/**
1581+
* Some calendars are identical to Gregorian except era and year. For these
1582+
* calendars, we can avoid using Intl.DateTimeFormat and just calculate the
1583+
* year, era, and eraYear. This is faster (because Intl.DateTimeFormat is slow
1584+
* and uses a huge amount of RAM), and it avoids ICU bugs like
1585+
* https://bugs.chromium.org/p/chromium/issues/detail?id=1173158.
1586+
*/
1587+
const makeHelperSameMonthDayAsGregorian = (id, originalEras) => {
1588+
const base = makeHelperGregorian(id, originalEras);
1589+
return ObjectAssign(base, {
1590+
isoToCalendarDate(isoDate) {
1591+
// Month and day are same as ISO, so bypass Intl.DateTimeFormat and
1592+
// calculate the year, era, and eraYear here.
1593+
const { year: isoYear, month, day } = isoDate;
1594+
const monthCode = buildMonthCode(month);
1595+
const year = isoYear - this.anchorEra.isoEpoch.year + 1;
1596+
return this.completeEraYear({ year, month, monthCode, day });
15961597
}
15971598
});
15981599
};
@@ -1648,26 +1649,22 @@ const helperEthiopic = makeHelperOrthodox('ethiopic', [
16481649

16491650
const helperRoc = ObjectAssign(
16501651
{},
1651-
makeHelperGregorian('roc', [
1652+
makeHelperSameMonthDayAsGregorian('roc', [
16521653
{ name: 'minguo', isoEpoch: { year: 1912, month: 1, day: 1 } },
16531654
{ name: 'before-roc', reverseOf: 'minguo' }
1654-
]),
1655-
{
1656-
calendarIsVulnerableToJulianBug: true
1657-
}
1655+
])
16581656
);
16591657

16601658
const helperBuddhist = ObjectAssign(
16611659
{},
1662-
makeHelperGregorian('buddhist', [{ name: 'be', hasYearZero: true, isoEpoch: { year: -543, month: 1, day: 1 } }]),
1663-
{
1664-
calendarIsVulnerableToJulianBug: true
1665-
}
1660+
makeHelperSameMonthDayAsGregorian('buddhist', [
1661+
{ name: 'be', hasYearZero: true, isoEpoch: { year: -543, month: 1, day: 1 } }
1662+
])
16661663
);
16671664

16681665
const helperGregory = ObjectAssign(
16691666
{},
1670-
makeHelperGregorian('gregory', [
1667+
makeHelperSameMonthDayAsGregorian('gregory', [
16711668
{ name: 'ce', isoEpoch: { year: 1, month: 1, day: 1 } },
16721669
{ name: 'bce', reverseOf: 'ce' }
16731670
]),
@@ -1716,7 +1713,7 @@ const helperJapanese = ObjectAssign(
17161713
// '1 1, 6 Meiji, 12:00:00 PM'
17171714
// > new Date('1872-12-31T12:00').toLocaleString(...args)
17181715
// '12 31, 5 Meiji, 12:00:00 PM'
1719-
makeHelperGregorian('japanese', [
1716+
makeHelperSameMonthDayAsGregorian('japanese', [
17201717
// The Japanese calendar `year` is just the ISO year, because (unlike other
17211718
// ICU calendars) there's no obvious "default era", we use the ISO year.
17221719
{ name: 'reiwa', isoEpoch: { year: 2019, month: 5, day: 1 }, anchorEpoch: { year: 2019, month: 5, day: 1 } },
@@ -1728,11 +1725,7 @@ const helperJapanese = ObjectAssign(
17281725
{ name: 'bce', reverseOf: 'ce' }
17291726
]),
17301727
{
1731-
// The last 3 Japanese eras confusingly return only one character in the
1732-
// default "short" era, so need to use the long format.
1733-
eraLength: 'long',
17341728
erasBeginMidYear: true,
1735-
calendarIsVulnerableToJulianBug: true,
17361729
reviseIntlEra(calendarDate, isoDate) {
17371730
const { era, eraYear } = calendarDate;
17381731
const { year: isoYear } = isoDate;

polyfill/test262

Submodule test262 updated 88 files

0 commit comments

Comments
 (0)