Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 54 additions & 50 deletions polyfill/lib/calendar.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
ArrayPrototypeIncludes,
ArrayPrototypeIndexOf,
ArrayPrototypeSort,
DatePrototypeSetUTCDate,
DatePrototypeToLocaleDateString,
IntlDateTimeFormat,
IntlDateTimeFormatPrototypeFormatToParts,
Expand Down Expand Up @@ -59,6 +58,8 @@ import * as ES from './ecmascript.mjs';
import { DefineIntrinsic } from './intrinsicclass.mjs';
import { CreateMonthCode, ParseMonthCode } from './monthcode.mjs';

const midnightTimeRecord = ES.MidnightTimeRecord();

function arrayFromSet(src) {
const valuesIterator = Call(SetPrototypeValues, src, []);
return ArrayFrom({
Expand Down Expand Up @@ -507,7 +508,7 @@ OneObjectCache.getCacheForObject = function (obj) {
return cache;
};

function toUtcIsoDateString({ isoYear, isoMonth, isoDay }) {
function toUtcIsoDateString(isoYear, isoMonth, isoDay) {
const yearString = ES.ISOYearString(isoYear);
const monthString = ES.ISODateTimePartString(isoMonth);
const dayString = ES.ISODateTimePartString(isoDay);
Expand Down Expand Up @@ -572,7 +573,7 @@ const nonIsoHelperBase = {
const cached = cache.get(key);
if (cached) return cached;

const isoString = toUtcIsoDateString({ isoYear, isoMonth, isoDay });
const isoString = toUtcIsoDateString(isoYear, isoMonth, isoDay);
const parts = this.getCalendarParts(isoString);
const hasEra = CalendarSupportsEra(this.id);
const result = {};
Expand Down Expand Up @@ -1867,69 +1868,72 @@ const helperChinese = ObjectAssign({}, nonIsoHelperBase, {
const key = JSONStringify({ func: 'getMonthList', calendarYear, id: this.id });
const cached = cache.get(key);
if (cached) return cached;

// Reuse the same local object for calendar-specific results, starting with
// a date close to Chinese New Year. Feb 17 will either be in the new year
// or near the end of the previous year's final month.
let daysPastJan31 = 17;
const calendarFields = { day: undefined, monthString: undefined, relatedYear: undefined };
const dateTimeFormat = this.getFormatter();
const getCalendarDate = (isoYear, daysPastFeb1) => {
const isoStringFeb1 = toUtcIsoDateString({ isoYear, isoMonth: 2, isoDay: 1 });
const legacyDate = new DateCtor(isoStringFeb1);
// Now add the requested number of days, which may wrap to the next month.
Call(DatePrototypeSetUTCDate, legacyDate, [daysPastFeb1 + 1]);
const newYearGuess = Call(IntlDateTimeFormatPrototypeFormatToParts, dateTimeFormat, [legacyDate]);
const calendarMonthString = Call(ArrayPrototypeFind, newYearGuess, [(tv) => tv.type === 'month']).value;
const calendarDay = +Call(ArrayPrototypeFind, newYearGuess, [(tv) => tv.type === 'day']).value;
let calendarYearToVerify = Call(ArrayPrototypeFind, newYearGuess, [(tv) => tv.type === 'relatedYear']);
if (calendarYearToVerify !== undefined) {
calendarYearToVerify = +calendarYearToVerify.value;
} else {
const updateCalendarFields = () => {
// Abuse GetUTCEpochMilliseconds for automatic rebalancing.
const isoNumbers = { year: calendarYear, month: 2, day: daysPastJan31 };
const ms = ES.GetUTCEpochMilliseconds(isoNumbers, midnightTimeRecord);
const fieldEntries = Call(IntlDateTimeFormatPrototypeFormatToParts, dateTimeFormat, [ms]);
for (let i = 0; i < fieldEntries.length; i++) {
const { type, value } = fieldEntries[i];
// day and year should be decimal strings, but month values like "5bis" are not number-coercible.
if (type === 'day' || type === 'relatedYear') {
calendarFields[type] = +value;
} else if (type === 'month') {
calendarFields.monthString = value;
}
}
if (calendarFields.relatedYear === undefined) {
// Node 12 has outdated ICU data that lacks the `relatedYear` field in the
// output of Intl.DateTimeFormat.formatToParts.
throw new RangeErrorCtor(
`Intl.DateTimeFormat.formatToParts lacks relatedYear in ${this.id} calendar. Try Node 14+ or modern browsers.`
);
}
return { calendarMonthString, calendarDay, calendarYearToVerify };
return calendarFields;
};

// First, find a date close to Chinese New Year. Feb 17 will either be in
// the first month or near the end of the last month of the previous year.
let isoDaysDelta = 17;
let { calendarMonthString, calendarDay, calendarYearToVerify } = getCalendarDate(calendarYear, isoDaysDelta);

// If we didn't guess the first month correctly, add (almost in some months)
// a lunar month
if (calendarMonthString !== '1') {
isoDaysDelta += 29;
({ calendarMonthString, calendarDay } = getCalendarDate(calendarYear, isoDaysDelta));
// Ensure that we're in the first month.
updateCalendarFields();
if (calendarFields.monthString !== '1') {
daysPastJan31 += 29;
updateCalendarFields();
}

// Now back up to near the start of the first month, but not too near that
// Now back up to near the start of the first month, but not so near that
// off-by-one issues matter.
isoDaysDelta -= calendarDay - 5;
const result = {};
daysPastJan31 -= calendarFields.day - 5;

const monthList = {};
let monthIndex = 1;
let oldCalendarDay;
let oldDay;
let oldMonthString;
let done = false;
do {
({ calendarMonthString, calendarDay, calendarYearToVerify } = getCalendarDate(calendarYear, isoDaysDelta));
if (oldCalendarDay) {
result[oldMonthString].daysInMonth = oldCalendarDay + 30 - calendarDay;
}
if (calendarYearToVerify !== calendarYear) {
done = true;
} else {
result[calendarMonthString] = { monthIndex: monthIndex++ };
// Move to the next month. Because months are sometimes 29 days, the day of the
// calendar month will move forward slowly but not enough to flip over to a new
// month before the loop ends at 12-13 months.
isoDaysDelta += 30;
for (;;) {
const { day, monthString, relatedYear } = updateCalendarFields();
if (oldDay) {
monthList[oldMonthString].daysInMonth = oldDay + 30 - day;
}
oldCalendarDay = calendarDay;
oldMonthString = calendarMonthString;
} while (!done);
result[oldMonthString].daysInMonth = oldCalendarDay + 30 - calendarDay;
oldDay = day;
oldMonthString = monthString;

cache.set(key, result);
return result;
if (relatedYear !== calendarYear) break;

monthList[monthString] = { monthIndex: monthIndex++ };
// Move to the next month. Because months are sometimes 29 days, the day of the
// calendar month will move forward slowly but not enough to flip over to a new
// month before the loop ends at 12-13 months.
daysPastJan31 += 30;
}
monthList[oldMonthString].daysInMonth = oldDay + 30 - calendarFields.day;

cache.set(key, monthList);
return monthList;
},
estimateIsoDate(calendarDate) {
const { year, month } = calendarDate;
Expand Down
38 changes: 16 additions & 22 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,13 @@ import {
ArrayPrototypeReduce,
ArrayPrototypeSort,
DateNow,
DatePrototypeGetTime,
DatePrototypeGetUTCFullYear,
DatePrototypeGetUTCMonth,
DatePrototypeGetUTCDate,
DatePrototypeGetUTCHours,
DatePrototypeGetUTCMinutes,
DatePrototypeGetUTCSeconds,
DatePrototypeGetUTCMilliseconds,
DatePrototypeSetUTCFullYear,
DatePrototypeSetUTCHours,
DateUTC,
IntlDateTimeFormat,
IntlDateTimeFormatPrototypeGetFormat,
Expand Down Expand Up @@ -2299,7 +2296,7 @@ function GetNamedTimeZoneOffsetNanosecondsImpl(id, epochMilliseconds) {
const { year, month, day, hour, minute, second } = GetFormatterParts(id, epochMilliseconds);
let millisecond = epochMilliseconds % 1000;
if (millisecond < 0) millisecond += 1000;
const utc = GetUTCEpochMilliseconds({ isoDate: { year, month, day }, time: { hour, minute, second, millisecond } });
const utc = GetUTCEpochMilliseconds({ year, month, day }, { hour, minute, second, millisecond });
return (utc - epochMilliseconds) * 1e6;
}

Expand All @@ -2324,29 +2321,29 @@ export function FormatDateTimeUTCOffsetRounded(offsetNanoseconds) {
return FormatOffsetTimeZoneIdentifier(offsetNanoseconds / 60e9);
}

function GetUTCEpochMilliseconds({
isoDate: { year, month, day },
time: { hour, minute, second, millisecond /* ignored: microsecond, nanosecond */ }
}) {
export function GetUTCEpochMilliseconds(isoDate, time) {
const { year, month, day } = isoDate;
const { hour, minute, second, millisecond /* ignored: microsecond, nanosecond */ } = time;

// The pattern of leap years in the ISO 8601 calendar repeats every 400
// years. To avoid overflowing at the edges of the range, we reduce the year
// to the remainder after dividing by 400, and then add back all the
// nanoseconds from the multiples of 400 years at the end.
const reducedYear = year % 400;
const yearCycles = (year - reducedYear) / 400;

// Note: Date.UTC() interprets one and two-digit years as being in the
// 20th century, so don't use it
const legacyDate = new DateCtor();
Call(DatePrototypeSetUTCHours, legacyDate, [hour, minute, second, millisecond]);
Call(DatePrototypeSetUTCFullYear, legacyDate, [reducedYear, month - 1, day]);
const ms = Call(DatePrototypeGetTime, legacyDate, []);
return ms + MS_IN_400_YEAR_CYCLE * yearCycles;
// `Date.UTC(year, monthIndex, days)` maps year [0, 99] to [1900, 1999], so
// avoid that range.
const extraCycles = reducedYear >= 0 ? 5 : 0;
const ms = DateUTC(reducedYear + 400 * extraCycles, month - 1, day, hour, minute, second, millisecond);

return ms + MS_IN_400_YEAR_CYCLE * (yearCycles - extraCycles);
}

function GetUTCEpochNanoseconds(isoDateTime) {
const ms = GetUTCEpochMilliseconds(isoDateTime);
const subMs = isoDateTime.time.microsecond * 1e3 + isoDateTime.time.nanosecond;
export function GetUTCEpochNanoseconds(isoDateTime) {
const { isoDate, time } = isoDateTime;
const ms = GetUTCEpochMilliseconds(isoDate, time);
const subMs = time.microsecond * 1e3 + time.nanosecond;
return bigInt(ms).multiply(1e6).plus(subMs);
}

Expand Down Expand Up @@ -3025,10 +3022,7 @@ export function CombineDateAndTimeDuration(dateDuration, timeDuration) {
// Caution: month is 0-based
export function ISODateToEpochDays(year, month, day) {
return (
GetUTCEpochMilliseconds({
isoDate: { year, month: month + 1, day },
time: { hour: 0, minute: 0, second: 0, millisecond: 0 }
}) / DAY_MS
GetUTCEpochMilliseconds({ year, month: month + 1, day }, { hour: 0, minute: 0, second: 0, millisecond: 0 }) / DAY_MS
);
}

Expand Down