Skip to content

Commit 36c370c

Browse files
authored
Polyfill: fix Chinese/Dangi calendar at extreme date ranges (#3277)
* Polyfill: fix Chinese/Dangi calendar at extreme date ranges Use Metonic cycle (19-year) offset to shift extreme ISO years into ICU4C's safe range before calling Intl.DateTimeFormat, then adjust the calendar year back. Fixes #3081. * Add runtime detection of ICU-23286 bug in Chinese/Dangi calendar * Add chinese and dangi to thorough calendar day math tests * Polyfill: make chineseMetonicOffset a method on helperChinese Move the standalone chineseMetonicOffset function into helperChinese as a metonicOffset method, using `this` instead of the `helper` parameter. helperDangi inherits it via ObjectAssign spread. Thanks to ptomato for the suggestion.
1 parent 86a7b46 commit 36c370c

4 files changed

Lines changed: 52 additions & 10 deletions

File tree

‎polyfill/lib/calendar.mjs‎

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
MapPrototypeSet,
2828
MathAbs,
2929
MathFloor,
30+
MathRound,
3031
MathTrunc,
3132
MathMax,
3233
MathSign,
@@ -1913,9 +1914,51 @@ const helperJapanese = ObjectAssign(
19131914
}
19141915
);
19151916

1917+
// ICU4C's Chinese/Dangi calendar fails for ISO years outside ~-29688 to +70368.
1918+
// Work around this using the Metonic cycle (see also Hebrew cycleInfo above):
1919+
// shift by a multiple of 19 years into the safe range, then adjust back.
1920+
const CHINESE_ICU_SAFE_LOW = -29000;
1921+
const CHINESE_ICU_SAFE_HIGH = 70000;
1922+
19161923
const helperChinese = ObjectAssign({}, nonIsoHelperBase, {
19171924
id: 'chinese',
19181925
calendarType: 'lunisolar',
1926+
isVulnerableTo70000Bug() {
1927+
// https://unicode-org.atlassian.net/browse/ICU-23286
1928+
if (this.vulnerableTo70000Bug === undefined) {
1929+
const formatter = this.getFormatter();
1930+
try {
1931+
Call(IntlDateTimeFormatPrototypeFormatToParts, formatter, [2146851043199999 + 1]);
1932+
this.vulnerableTo70000Bug = false;
1933+
} catch {
1934+
this.vulnerableTo70000Bug = true;
1935+
}
1936+
}
1937+
return this.vulnerableTo70000Bug;
1938+
},
1939+
metonicOffset(year) {
1940+
if (!this.isVulnerableTo70000Bug()) return 0;
1941+
if (year >= CHINESE_ICU_SAFE_LOW && year <= CHINESE_ICU_SAFE_HIGH) return 0;
1942+
return MathRound((year - 2000) / 19) * 19;
1943+
},
1944+
isoToCalendarDate(isoDate, cache) {
1945+
const offset = this.metonicOffset(isoDate.year);
1946+
if (offset === 0) {
1947+
return nonIsoHelperBase.isoToCalendarDate.call(this, isoDate, cache);
1948+
}
1949+
const safeIsoDate = { ...isoDate, year: isoDate.year - offset };
1950+
const result = nonIsoHelperBase.isoToCalendarDate.call(this, safeIsoDate, cache);
1951+
const adjusted = { ...result, year: result.year + offset };
1952+
// Cache both directions with the original (not shifted) date keys
1953+
const key = OneObjectCache.generateISOToCalendarKey(isoDate);
1954+
cache.set(key, adjusted);
1955+
const cacheReverse = (overflow) => {
1956+
const keyReverse = OneObjectCache.generateCalendarToISOKey(adjusted, overflow);
1957+
cache.set(keyReverse, isoDate);
1958+
};
1959+
Call(ArrayPrototypeForEach, ['constrain', 'reject'], [cacheReverse]);
1960+
return adjusted;
1961+
},
19191962
inLeapYear(calendarDate, cache) {
19201963
return this.getMonthList(calendarDate.year, cache).monthsInYear === 13;
19211964
},
@@ -1986,6 +2029,9 @@ const helperChinese = ObjectAssign({}, nonIsoHelperBase, {
19862029
const cached = cache.get(key);
19872030
if (cached) return cached;
19882031

2032+
const offset = this.metonicOffset(calendarYear);
2033+
const effectiveYear = calendarYear - offset;
2034+
19892035
// Reuse the same local object for calendar-specific results, starting with
19902036
// a date close to Chinese New Year. Feb 17 will either be in the new year
19912037
// or near the end of the previous year's final month.
@@ -1994,7 +2040,7 @@ const helperChinese = ObjectAssign({}, nonIsoHelperBase, {
19942040
const dateTimeFormat = this.getFormatter();
19952041
const updateCalendarFields = () => {
19962042
// Abuse GetUTCEpochMilliseconds for automatic rebalancing.
1997-
const isoNumbers = { year: calendarYear, month: 2, day: daysPastJan31 };
2043+
const isoNumbers = { year: effectiveYear, month: 2, day: daysPastJan31 };
19982044
const ms = ES.GetUTCEpochMilliseconds(isoNumbers, midnightTimeRecord);
19992045
const fieldEntries = Call(IntlDateTimeFormatPrototypeFormatToParts, dateTimeFormat, [ms]);
20002046
for (let i = 0; i < fieldEntries.length; i++) {
@@ -2047,7 +2093,7 @@ const helperChinese = ObjectAssign({}, nonIsoHelperBase, {
20472093
}
20482094
oldDay = day;
20492095

2050-
if (relatedYear !== calendarYear) break;
2096+
if (relatedYear !== effectiveYear) break;
20512097

20522098
monthList[monthIndex] = { monthCode };
20532099
monthList[monthCode] = monthIndex++;

‎polyfill/lib/primordials.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export const {
111111
log10: MathLog10,
112112
max: MathMax,
113113
min: MathMin,
114+
round: MathRound,
114115
sign: MathSign,
115116
trunc: MathTrunc
116117
} = Math;

‎polyfill/test/expected-failures.txt‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,8 @@ intl402/Temporal/ZonedDateTime/prototype/daysInYear/basic-chinese.js
3434
intl402/DateTimeFormat/prototype/resolvedOptions/resolved-calendar-unicode-extensions-and-options.js
3535
intl402/DateTimeFormat/prototype/resolvedOptions/resolved-numbering-system-unicode-extensions-and-options.js
3636

37-
# ICU4C internal error in extreme dates in chinese/dangi calendars
38-
intl402/Temporal/PlainDate/from/extreme-dates.js
39-
intl402/Temporal/PlainDateTime/from/extreme-dates.js
40-
# Also fails because of https://github.com/tc39/proposal-temporal/issues/3251
37+
# https://github.com/tc39/proposal-temporal/issues/3251
4138
intl402/Temporal/PlainYearMonth/from/extreme-dates.js
42-
intl402/Temporal/ZonedDateTime/from/extreme-dates.js
4339

4440
# https://issues.chromium.org/issues/481634945
4541
intl402/DateTimeFormat/prototype/formatToParts/era.js

‎polyfill/test/thorough/calendardaymath.mjs‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@ import {
88
} from './support.mjs';
99

1010
const largestUnits = [{ largestUnit: 'weeks' }, { largestUnit: 'days' }];
11-
// 'chinese' and 'dangi' are temporarily omitted because there are some dates
12-
// that the current version of ICU4C can't handle well enough to calculate the
13-
// snapshots
1411
const calendars = [
1512
'buddhist',
13+
'chinese',
1614
'coptic',
15+
'dangi',
1716
'ethioaa',
1817
'ethiopic',
1918
'gregory',

0 commit comments

Comments
 (0)