From 59b0f3086f950292f6d4d6acda1175ad8f9a1924 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 15 Dec 2021 17:17:20 -0800 Subject: [PATCH 1/8] Update xxxFromFields public types --- index.d.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/index.d.ts b/index.d.ts index 84cc293a..c228f242 100644 --- a/index.d.ts +++ b/index.d.ts @@ -603,8 +603,9 @@ export namespace Temporal { readonly [Symbol.toStringTag]: 'Temporal.Instant'; } - type EitherYearOrEraAndEraYear = { era: string; eraYear: number } | { year: number }; - type EitherMonthCodeOrMonthAndYear = (EitherYearOrEraAndEraYear & { month: number }) | { monthCode: string }; + type YearOrEraAndEraYear = { era: string; eraYear: number } | { year: number }; + type MonthCodeOrMonthAndYear = (YearOrEraAndEraYear & { month: number }) | { monthCode: string }; + type MonthOrMonthCode = { month: number } | { monthCode: string }; export interface CalendarProtocol { id?: string; @@ -648,15 +649,15 @@ export namespace Temporal { date: Temporal.PlainDate | Temporal.PlainDateTime | Temporal.PlainYearMonth | PlainDateLike | string ): boolean; dateFromFields( - fields: EitherMonthCodeOrMonthAndYear & { day: number }, + fields: YearOrEraAndEraYear & MonthOrMonthCode & { day: number }, options?: AssignmentOptions ): Temporal.PlainDate; yearMonthFromFields( - fields: EitherYearOrEraAndEraYear & ({ month: number } | { monthCode: string }), + fields: YearOrEraAndEraYear & MonthOrMonthCode, options?: AssignmentOptions ): Temporal.PlainYearMonth; monthDayFromFields( - fields: EitherMonthCodeOrMonthAndYear & { day: number }, + fields: MonthCodeOrMonthAndYear & { day: number }, options?: AssignmentOptions ): Temporal.PlainMonthDay; dateAdd( @@ -726,15 +727,15 @@ export namespace Temporal { date: Temporal.PlainDate | Temporal.PlainDateTime | Temporal.PlainYearMonth | PlainDateLike | string ): boolean; dateFromFields( - fields: EitherMonthCodeOrMonthAndYear & { day: number }, + fields: YearOrEraAndEraYear & MonthOrMonthCode & { day: number }, options?: AssignmentOptions ): Temporal.PlainDate; yearMonthFromFields( - fields: EitherYearOrEraAndEraYear & ({ month: number } | { monthCode: string }), + fields: YearOrEraAndEraYear & MonthOrMonthCode, options?: AssignmentOptions ): Temporal.PlainYearMonth; monthDayFromFields( - fields: EitherMonthCodeOrMonthAndYear & { day: number }, + fields: MonthCodeOrMonthAndYear & { day: number }, options?: AssignmentOptions ): Temporal.PlainMonthDay; dateAdd( From b4eb5bfade039de42e5e4cf31b3ef276caba2200 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 15 Dec 2021 17:51:11 -0800 Subject: [PATCH 2/8] Use cached built-in objects instead of runtime prototype lookups --- lib/calendar.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/calendar.ts b/lib/calendar.ts index 185dbfc5..0e937dd4 100644 --- a/lib/calendar.ts +++ b/lib/calendar.ts @@ -32,6 +32,7 @@ import type { const ArrayIncludes = Array.prototype.includes; const ArrayPrototypePush = Array.prototype.push; const IntlDateTimeFormat = globalThis.Intl.DateTimeFormat; +const ArraySort = Array.prototype.sort; const MathAbs = Math.abs; const MathFloor = Math.floor; const ObjectAssign = Object.assign; @@ -1650,7 +1651,7 @@ function adjustEras(erasParam: InputEra[]): { eras: Era[]; anchorEra: Era } { // Ensure that the latest epoch is first in the array. This lets us try to // match eras in index order, with the last era getting the remaining older // years. Any reverse-signed era must be at the end. - eras.sort((e1, e2) => { + ArraySort.call(eras, (e1, e2) => { if (e1.reverseOf) return 1; if (e2.reverseOf) return -1; return e2.isoEpoch.year - e1.isoEpoch.year; @@ -2079,7 +2080,7 @@ const helperChinese: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { if ( month === undefined && monthCode.endsWith('L') && - !['M01L', 'M12L', 'M13L'].includes(monthCode) && + !ArrayIncludes.call(['M01L', 'M12L', 'M13L'], monthCode) && overflow === 'constrain' ) { let withoutML = monthCode.slice(1, -1); @@ -2262,7 +2263,7 @@ const nonIsoGeneralImpl: NonIsoGeneralImpl = { }, fields(fieldsParam) { let fields = fieldsParam; - if (fields.includes('year')) fields = [...fields, 'era', 'eraYear']; + if (ArrayIncludes.call(fields, 'year')) fields = [...fields, 'era', 'eraYear']; return fields; }, mergeFields(fields, additionalFields) { From c254d5589535bbb02bc75ca9802fe53ffca1a737 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 15 Dec 2021 17:52:55 -0800 Subject: [PATCH 3/8] Remove unused parameter --- lib/calendar.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/calendar.ts b/lib/calendar.ts index 0e937dd4..3d19bc1f 100644 --- a/lib/calendar.ts +++ b/lib/calendar.ts @@ -644,7 +644,7 @@ interface NonIsoHelperBase { reviseIntlEra?(calendarDate: any, isoDate?: any): { era: number; eraYear: number }; hasEra?: boolean; constantEra?: string; - checkIcuBugs?(calendarDate: any, isoDate: any): void; + checkIcuBugs?(isoDate: any): void; calendarType?: string; monthsInYear?(calendarDate: any, cache?: any): number; maximumMonthLength?(calendarDate?: any): number; @@ -762,7 +762,7 @@ const nonIsoHelperBase: NonIsoHelperBase = { result.era = era; result.eraYear = eraYear; } - if (this.checkIcuBugs) this.checkIcuBugs(result, isoDate); + if (this.checkIcuBugs) this.checkIcuBugs(isoDate); const calendarDate = this.adjustCalendarDate(result, cache, 'constrain', true); if (calendarDate.year === undefined) throw new RangeError(`Missing year converting ${JSON.stringify(isoDate)}`); @@ -1486,7 +1486,7 @@ const helperIndian: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { // expected. vulnerableToBceBug: new Date('0000-01-01T00:00Z').toLocaleDateString('en-US-u-ca-indian', { timeZone: 'UTC' }) !== '10/11/-79 Saka', - checkIcuBugs(calendarDate, isoDate) { + checkIcuBugs(isoDate) { if (this.vulnerableToBceBug && isoDate.year < 1) { throw new RangeError( `calendar '${this.id}' is broken for ISO dates before 0001-01-01` + @@ -1795,7 +1795,7 @@ const makeHelperGregorian = (id: BuiltinCalendarId, originalEras: InputEra[]) => .toLocaleDateString('en-US-u-ca-japanese', { timeZone: 'UTC' }) .startsWith('12'), calendarIsVulnerableToJulianBug: false, - checkIcuBugs(calendarDate, isoDate) { + checkIcuBugs(isoDate) { if (this.calendarIsVulnerableToJulianBug && this.v8IsVulnerableToJulianBug) { const beforeJulianSwitch = ES.CompareISODate(isoDate.year, isoDate.month, isoDate.day, 1582, 10, 15) < 0; if (beforeJulianSwitch) { From 3cb916e94e7a879a876668383619224ef42299b3 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 15 Dec 2021 17:55:44 -0800 Subject: [PATCH 4/8] Remove TODO comment The current code is correct, so no need for this comment. --- lib/calendar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/calendar.ts b/lib/calendar.ts index 3d19bc1f..46206e34 100644 --- a/lib/calendar.ts +++ b/lib/calendar.ts @@ -530,7 +530,7 @@ class OneObjectCache { constructor(cacheToClone: OneObjectCache = undefined) { this.now = globalThis.performance ? globalThis.performance.now() : Date.now(); if (cacheToClone !== undefined) { - let i = 0; // TODO why was this originally cacheToClone.length ? + let i = 0; for (const entry of cacheToClone.map.entries()) { if (++i > OneObjectCache.MAX_CACHE_ENTRIES) break; this.map.set(...entry); From fe846a3e0f7bf051c92aa0b51be832ee0748389a Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 15 Dec 2021 17:59:46 -0800 Subject: [PATCH 5/8] Call maximumMonthLength with correct args `year` might have been mutated in the first line of this method. So it's not OK to pass `calendarDate` to `maximumMonthLength`. Need to pass current year/month values instead. --- lib/calendar.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/calendar.ts b/lib/calendar.ts index 46206e34..dba78eca 100644 --- a/lib/calendar.ts +++ b/lib/calendar.ts @@ -1349,10 +1349,10 @@ const helperHebrew: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { } else { if (overflow === 'reject') { ES.RejectToRange(month, 1, this.monthsInYear({ year })); - ES.RejectToRange(day, 1, this.maximumMonthLength(calendarDate)); + ES.RejectToRange(day, 1, this.maximumMonthLength({ year, month })); } else { month = ES.ConstrainToRange(month, 1, this.monthsInYear({ year })); - day = ES.ConstrainToRange(day, 1, this.maximumMonthLength({ ...calendarDate, month })); + day = ES.ConstrainToRange(day, 1, this.maximumMonthLength({ year, month })); } if (monthCode === undefined) { monthCode = this.getMonthCode(year, month); From c1d0d842f52f50c5d29d327f19ca4422e6e0169d Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 15 Dec 2021 18:00:37 -0800 Subject: [PATCH 6/8] Additonal validation for hard-coded era data This adds an additional check to make sure that the (hard-coded) era definitions for each calendar aren't missing required `isoEpoch` properties. Note that this code is only run once at load time, not each time calendars are accessed, so there's no runtime impact. --- lib/calendar.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/calendar.ts b/lib/calendar.ts index dba78eca..94a8151e 100644 --- a/lib/calendar.ts +++ b/lib/calendar.ts @@ -1654,6 +1654,7 @@ function adjustEras(erasParam: InputEra[]): { eras: Era[]; anchorEra: Era } { ArraySort.call(eras, (e1, e2) => { if (e1.reverseOf) return 1; if (e2.reverseOf) return -1; + if (!e1.isoEpoch || !e2.isoEpoch) throw new RangeError('Invalid era data: missing ISO epoch'); return e2.isoEpoch.year - e1.isoEpoch.year; }); From b4420ed3dfad2da577450448c9aa3be5fcd68de3 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 15 Dec 2021 18:08:29 -0800 Subject: [PATCH 7/8] Fix bad code ordering in adjustCalendarDate Re-order code to guarantee that `calendarDate.year` is set before calculating the number of months in that year. The old code would break for any calendar that a) used a constant `era` like `islamic` and b) had a variable number of months in each year like `hebrew`. Today no ICU calendars fit in that Venn diagram, but if any are added in the future this code will break when initializing any calendar using an eraYear/era pair in a property bag. Better to fix it now! --- lib/calendar.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/calendar.ts b/lib/calendar.ts index 94a8151e..2578e33f 100644 --- a/lib/calendar.ts +++ b/lib/calendar.ts @@ -821,18 +821,22 @@ const nonIsoHelperBase: NonIsoHelperBase = { if (this.calendarType === 'lunisolar') throw new RangeError('Override required for lunisolar calendars'); let calendarDate = calendarDateParam; this.validateCalendarDate(calendarDate); - const largestMonth = this.monthsInYear(calendarDate, cache); - let { month, year, eraYear, monthCode } = calendarDate; - // For calendars that always use the same era, set it here so that derived // calendars won't need to implement this method simply to set the era. if (this.constantEra) { // year and eraYear always match when there's only one possible era - if (year === undefined) year = eraYear; - if (eraYear === undefined) eraYear = year; - calendarDate = { ...calendarDate, era: this.constantEra, year, eraYear }; + const { year, eraYear } = calendarDate; + calendarDate = { + ...calendarDate, + era: this.constantEra, + year: year !== undefined ? year : eraYear, + eraYear: eraYear !== undefined ? eraYear : year + }; } + const largestMonth = this.monthsInYear(calendarDate, cache); + let { month, monthCode } = calendarDate; + ({ month, monthCode } = resolveNonLunisolarMonth(calendarDate, overflow, largestMonth)); return { ...calendarDate, month, monthCode }; }, From 3a5e6c6fa8a1979188e3d6461cb7e603d2d07bd4 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 15 Dec 2021 19:46:38 -0800 Subject: [PATCH 8/8] Make TS happier with year/eraYear This was originally part of tc39/proposal-temporal#1977 but I removed it because it wasn't high-priority for JS. But it's still needed in this repo so I'm adding it back. Why? TS complains when you assign a maybe-undefined value to a must-not-be-undefined variable. This small runtime change is cleaner than adding a lotta type casts. --- lib/calendar.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/calendar.ts b/lib/calendar.ts index 2578e33f..44ec5a08 100644 --- a/lib/calendar.ts +++ b/lib/calendar.ts @@ -1305,8 +1305,8 @@ const helperHebrew: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { fromLegacyDate = false ) { let { year, eraYear, month, monthCode, day, monthExtra } = calendarDate; - if (year === undefined) year = eraYear; - if (eraYear === undefined) eraYear = year; + if (year === undefined && eraYear !== undefined) year = eraYear; + if (eraYear === undefined && year !== undefined) eraYear = year; if (fromLegacyDate) { // In Pre Node-14 V8, DateTimeFormat.formatToParts `month: 'numeric'` // output returns the numeric equivalent of `month` as a string, meaning