From 44db035e819162c42aa719f446027f9dcf38d1c8 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 15 Dec 2021 20:55:38 -0800 Subject: [PATCH 01/10] Only require year in Gergorian `inLeapYear` method A later commit is going to type all inLeapYear methods as only needing the year in the input, not a full date. This inLeapYear implementation is the only one where this type limitation needed a runtime change, so it's split into its own commit. --- lib/calendar.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/calendar.ts b/lib/calendar.ts index 44ec5a08..f122bee1 100644 --- a/lib/calendar.ts +++ b/lib/calendar.ts @@ -1691,8 +1691,11 @@ const makeHelperGregorian = (id: BuiltinCalendarId, originalEras: InputEra[]) => eras, anchorEra, calendarType: 'solar', - inLeapYear(calendarDate /*, cache */) { - const { year } = this.estimateIsoDate(calendarDate); + inLeapYear(calendarDate /*, cache: OneObjectCache */) { + // Calendars that don't override this method use the same months and leap + // years as Gregorian. Once we know the ISO year corresponding to the + // calendar year, we'll know if it's a leap year or not. + const { year } = this.estimateIsoDate({ month: 1, day: 1, year: calendarDate.year }); return isGregorianLeapYear(year); }, monthsInYear(/* calendarDate */) { From 7b1c123ea13b73babb09ac1b1bc57263ab960c6a Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 15 Dec 2021 21:53:19 -0800 Subject: [PATCH 02/10] Add new calendar types Declare an improved set of types for non-ISO calendars. They'll get used in later commits. --- lib/calendar.ts | 250 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 248 insertions(+), 2 deletions(-) diff --git a/lib/calendar.ts b/lib/calendar.ts index f122bee1..4308fb56 100644 --- a/lib/calendar.ts +++ b/lib/calendar.ts @@ -455,9 +455,213 @@ impl['iso8601'] = { } }; -// Note: other built-in calendars than iso8601 are not part of the Temporal +// Note: Built-in calendars other than iso8601 are not part of the Temporal // proposal for ECMA-262. These calendars will be standardized as part of -// ECMA-402. +// ECMA-402. Code below here includes an implementation of these calendars order +// to validate the Temporal API and to get feedback. However, non-ISO calendar +// implementation is subject to change because these calendars are +// implementation-defined. +// +// Some ES implementations don't include ECMA 402. For this reason, it's helpful +// to ensure a clean separation between the ISO calendar implementation which is +// a part of ECMA 262 and the non-ISO calendar implementation which requires +// ECMA 402. +// +// To ensure this separation, the implementation is split. The `NonIsoImpl` +// interface is the top-level implementation for all non-ISO calendars. This +// type has the same shape as the ECMA 262-only ISO calendar implementation so +// can use the same callers, tests, etc. +// +// A derived interface `NonIsoImplWithHelper` adds a `helper` property that +// includes the remaining non-ISO implementation properties and methods beyond +// the ISO implementation above. The `helper` property's shape is a base +// singleton object common to all calendars (`HelperSharedImpl`) that's extended +// (interface `HelperPerCalendarImpl`) with implementation that varies for each +// calendar. +// +// Typing of individual methods in the interfaces below uses the `this` +// "parameter" declaration definition, which is a fake parameter (stripped by TS +// during compilation and not visible at runtime) that tells TS what type `this` +// is for a method. For historical reasons, the initial implementation of +// non-ISO calendars mirrored the code style of the previous ISO-only +// implementation which didn't use ES6 classes. Using the `this` parameter is a +// hack to delay converting this file to use ES6 classes until the code was +// fully typed to make a `class` refactoring easier and safer. We'll probably do +// this conversion in the future. (PRs welcome!) + +/** + * `NonIsoImpl` - The generic top-level implementation for all non-ISO + * calendars. This type has the same shape as the 262-only ISO calendar + * implementation, which means the `Calendar` class implementation can swap out + * the ISO for non-ISO implementations without changing any `Calendar` code. + */ +interface NonIsoImpl { + dateFromFields( + this: NonIsoImplWithHelper, + fieldsParam: Params['dateFromFields'][0], + options: NonNullable, + calendar: Temporal.Calendar + ): Temporal.PlainDate; + yearMonthFromFields( + this: NonIsoImplWithHelper, + fieldsParam: Params['yearMonthFromFields'][0], + options: NonNullable, + calendar: Temporal.Calendar + ): Temporal.PlainYearMonth; + monthDayFromFields( + this: NonIsoImplWithHelper, + fieldsParam: Params['monthDayFromFields'][0], + options: NonNullable, + calendar: Temporal.Calendar + ): Temporal.PlainMonthDay; + fields(fieldsParam: string[]): Return['fields']; + mergeFields(fields: Params['mergeFields'][0], additionalFields: Params['mergeFields'][1]): Return['mergeFields']; + dateAdd( + this: NonIsoImplWithHelper, + date: Temporal.PlainDate, + years: number, + months: number, + weeks: number, + days: number, + overflow: Overflow, + calendar: Temporal.Calendar + ): Temporal.PlainDate; + dateUntil( + this: NonIsoImplWithHelper, + one: Temporal.PlainDate, + two: Temporal.PlainDate, + largestUnit: Temporal.DateUnit + ): { + years: number; + months: number; + weeks: number; + days: number; + }; + year(this: NonIsoImplWithHelper, date: Temporal.PlainDate): number; + month(this: NonIsoImplWithHelper, date: Temporal.PlainDate): number; + day(this: NonIsoImplWithHelper, date: Temporal.PlainDate): number; + era(this: NonIsoImplWithHelper, date: Temporal.PlainDate): string | undefined; + eraYear(this: NonIsoImplWithHelper, date: Temporal.PlainDate): number | undefined; + monthCode(this: NonIsoImplWithHelper, date: Temporal.PlainDate): string; + dayOfWeek(date: Temporal.PlainDate): number; + dayOfYear(this: NonIsoImplWithHelper, date: Temporal.PlainDate): number; + weekOfYear(date: Temporal.PlainDate): number; + daysInWeek(date: Temporal.PlainDate): number; + daysInMonth(this: NonIsoImplWithHelper, date: Temporal.PlainDate | Temporal.PlainYearMonth): number; + daysInYear(this: NonIsoImplWithHelper, dateParam: Temporal.PlainDate | Temporal.PlainYearMonth): number; + monthsInYear(this: NonIsoImplWithHelper, date: Temporal.PlainDate | Temporal.PlainYearMonth): number; + inLeapYear(this: NonIsoImplWithHelper, dateParam: Temporal.PlainDate | Temporal.PlainYearMonth): boolean; +} + +/** + * This type exists solely to ensure a compiler error is shown if a per-calendar + * implementation object doesn't declare a `helper` property. It will go away + * if we migrate to ES6 classes. + * + * The methods of NonIsoImpl all set their `this` to NonIsoImplWithHelper in + * order to avoid having to cast every use of `helper` to exclude `undefined`. + * */ +interface NonIsoImplWithHelper extends NonIsoImpl { + helper: HelperPerCalendarImpl; +} + +/** Shape of shared implementation code that applies to all calendars */ +interface HelperSharedImpl { + isoToCalendarDate(isoDate: IsoYMD, cache: OneObjectCache): FullCalendarDate; + validateCalendarDate(calendarDate: Partial): void; + adjustCalendarDate( + calendarDate: Partial, + cache?: OneObjectCache, + overflow?: Overflow, + fromLegacyDate?: boolean + ): FullCalendarDate; + regulateMonthDayNaive(calendarDate: FullCalendarDate, overflow: Overflow, cache: OneObjectCache): FullCalendarDate; + calendarToIsoDate(date: CalendarDateFields, overflow: Overflow, cache: OneObjectCache): IsoYMD; + temporalToCalendarDate( + date: Temporal.PlainDate | Temporal.PlainMonthDay | Temporal.PlainYearMonth, + cache: OneObjectCache + ): FullCalendarDate; + compareCalendarDates(date1: Partial, date2: Partial): 0 | 1 | -1; + regulateDate(calendarDate: CalendarYMD, overflow: Overflow, cache: OneObjectCache): FullCalendarDate; + addDaysIso(isoDate: IsoYMD, days: number, cache?: OneObjectCache): IsoYMD; + addDaysCalendar(calendarDate: CalendarYMD, days: number, cache: OneObjectCache): FullCalendarDate; + addMonthsCalendar(calendarDate: CalendarYMD, months: number, overflow: Overflow, cache: OneObjectCache): CalendarYMD; + addCalendar( + calendarDate: CalendarYMD, + { years, months, weeks, days }: { years?: number; months?: number; weeks?: number; days?: number }, + overflow: Overflow, + cache: OneObjectCache + ): FullCalendarDate; + untilCalendar( + calendarOne: FullCalendarDate, + calendarTwo: FullCalendarDate, + largestUnit: Temporal.DateUnit, + cache: OneObjectCache + ): { years: number; months: number; weeks: number; days: number }; + daysInMonth(calendarDate: CalendarYMD, cache: OneObjectCache): number; + daysInPreviousMonth(calendarDate: CalendarYMD, cache: OneObjectCache): number; + startOfCalendarYear(calendarDate: CalendarYearOnly): CalendarYMD; + startOfCalendarMonth(calendarDate: { year: number; month: number }): CalendarYMD; + calendarDaysUntil(calendarOne: CalendarYMD, calendarTwo: CalendarYMD, cache: OneObjectCache): number; + isoDaysUntil(oneIso: IsoYMD, twoIso: IsoYMD): number; + eraLength: 'long' | 'short' | 'narrow'; + getFormatter(): globalThis.Intl.DateTimeFormat; + formatter?: globalThis.Intl.DateTimeFormat; + hasEra: boolean; + monthDayFromFields(fields: Partial, overflow: Overflow, cache: OneObjectCache): IsoYMD; +} + +/** Calendar-specific implementation */ +interface HelperPerCalendarImpl extends HelperSharedImpl { + id: string; + reviseIntlEra?>(calendarDate: T, isoDate: IsoYMD): T; + constantEra?: string; + checkIcuBugs?(isoDate: IsoYMD): void; + calendarType?: string; + monthsInYear(calendarDate: CalendarYearOnly, cache?: OneObjectCache): number; + maximumMonthLength(calendarDate?: CalendarYM): number; + minimumMonthLength(calendarDate?: CalendarYM): number; + estimateIsoDate(calendarDate: CalendarYMD): IsoYMD; + inLeapYear(calendarDate: CalendarYearOnly, cache?: OneObjectCache): boolean; + + // Fields below here are only present in some subclasses but not others. + eras?: Era[]; + anchorEra?: Era; + calendarIsVulnerableToJulianBug?: boolean; + v8IsVulnerableToJulianBug?: boolean; +} + +/** + * This type is passed through from Calendar#dateFromFields(). + * `monthExtra` is additional information used internally to identify lunisolar leap months. + */ +type CalendarDateFields = Params['dateFromFields'][0] & { monthExtra?: string }; + +/** + * This is a "fully populated" calendar date record. It's only lacking + * `era`/`eraYear` (which may not be present in all calendars) and `monthExtra` + * which is only used in some cases. + */ +type FullCalendarDate = { + era?: string; + eraYear?: number; + year: number; + month: number; + monthCode: string; + day: number; + monthExtra?: string; +}; + +// The types below are various subsets of calendar dates +type CalendarYMD = { year: number; month: number; day: number }; +type CalendarYM = { year: number; month: number }; +type CalendarYearOnly = { year: number }; +type EraAndEraYear = { era: string; eraYear: number }; + +/** Record representing YMD of an ISO calendar date */ +type IsoYMD = { year: number; month: number; day: number }; + +type Overflow = Temporal.AssignmentOptions['overflow']; function monthCodeNumberPart(monthCode: string) { if (!monthCode.startsWith('M')) { @@ -1232,6 +1436,27 @@ const nonIsoHelperBase: NonIsoHelperBase = { } }; +interface HebrewMonthInfo { + [m: string]: ( + | { + leap: undefined; + regular: number; + } + | { + leap: number; + regular: undefined; + } + ) & { + monthCode: string; + days: + | number + | { + min: number; + max: number; + }; + }; +} + const helperHebrew: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { id: 'hebrew', calendarType: 'lunisolar', @@ -1428,6 +1653,20 @@ const helperPersian: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { } } as Partial); +interface IndianMonthInfo { + [month: number]: { + length: number; + month: number; + day: number; + leap?: { + length: number; + month: number; + day: number; + }; + nextYear?: true | undefined; + }; +} + const helperIndian: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { id: 'indian', calendarType: 'solar', @@ -1958,6 +2197,13 @@ const helperJapanese: NonIsoHelperBase = ObjectAssign( } as Partial ); +interface ChineseMonthInfo { + [key: string]: { monthIndex: number; daysInMonth: number }; +} +interface ChineseDraftMonthInfo { + [key: string]: { monthIndex: number; daysInMonth?: number }; +} + const helperChinese: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { id: 'chinese', calendarType: 'lunisolar', From a6b5854794a534ada9f96d84248f9305279b94c5 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 15 Dec 2021 22:17:37 -0800 Subject: [PATCH 03/10] Bulk renames to use new calendar types This commit is a search-n-replace to use the new calendar types defined in the previous commit: * nonIsoGeneralImpl => nonIsoImpl (the variable name) * NonIsoGeneralImpl => NonIsoImpl (the type) * typeof nonIsoGeneralImpl & { helper: NonIsoHelperBase } => NonIsoImplWithHelper * nonIsoHelperBase => helperSharedImpl (variable) * NonIsoHelperBase => HelperSharedImpl (type). The old NonIsoHelperBase interface is still present (under the new name) and the code uses the superset of both old and new same-named interfaces. The old one will be removed in the next commit once code using it is migrated to the new "split" interface with HelperSharedImpl and HelperPerCalendarImpl. But at least in the next commit there won't be lots of rename-only diffs. * Temporal.AssignmentOptions['overflow'] => Overflow * Temporal.ArithmeticOptions['overflow'] => Overflow (same values as AssignmentOptions enumeration) There's a lot more one-off changes coming up, but by breaking out these generic renames it will hopefully make everything else easier to review. --- lib/calendar.ts | 192 ++++++++++++++++-------------------------------- 1 file changed, 64 insertions(+), 128 deletions(-) diff --git a/lib/calendar.ts b/lib/calendar.ts index 4308fb56..9c349499 100644 --- a/lib/calendar.ts +++ b/lib/calendar.ts @@ -78,7 +78,7 @@ interface CalendarImpl { months: number, weeks: number, days: number, - overflow: Temporal.ArithmeticOptions['overflow'], + overflow: Overflow, calendar: Temporal.Calendar ): Temporal.PlainDate; dateUntil( @@ -683,7 +683,7 @@ function buildMonthCode(month: number | string, leap = false) { * */ function resolveNonLunisolarMonth( calendarDate: T, - overflow: Temporal.ArithmeticOptions['overflow'] = undefined, + overflow: Overflow = undefined, monthsPerYear = 12 ) { let { month, monthCode } = calendarDate; @@ -709,12 +709,6 @@ function resolveNonLunisolarMonth( return { ...calendarDate, month, monthCode }; } -// Note: other built-in calendars than iso8601 are not part of the Temporal -// proposal for ECMA-262. An implementation of these calendars is present in -// this polyfill in order to validate the Temporal API and to get early feedback -// about non-ISO calendars. However, non-ISO calendar implementation is subject -// to change because these calendars are implementation-defined. - type CachedTypes = Temporal.PlainYearMonth | Temporal.PlainDate | Temporal.PlainMonthDay; /** @@ -812,7 +806,7 @@ function simpleDateDiff(one: Required, two: Required }; } -interface NonIsoHelperBase { +interface HelperSharedImpl { id?: string; isoToCalendarDate(isoDate: any, cache: any): any; validateCalendarDate(calendarDate: any): void; @@ -846,7 +840,7 @@ interface NonIsoHelperBase { eraLength: 'long' | 'short' | 'narrow'; // reviseIntlEra can optionally be defined on subclasses of the base reviseIntlEra?(calendarDate: any, isoDate?: any): { era: number; eraYear: number }; - hasEra?: boolean; + hasEra: boolean; constantEra?: string; checkIcuBugs?(isoDate: any): void; calendarType?: string; @@ -878,7 +872,7 @@ interface NonIsoHelperBase { /** * Implementation that's common to all non-trivial non-ISO calendars */ -const nonIsoHelperBase: NonIsoHelperBase = { +const helperSharedImpl: HelperSharedImpl = { // The properties and methods below here should be the same for all lunar/lunisolar calendars. getFormatter() { // `new Intl.DateTimeFormat()` is amazingly slow and chews up RAM. Per @@ -1457,7 +1451,7 @@ interface HebrewMonthInfo { }; } -const helperHebrew: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { +const helperHebrew: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { id: 'hebrew', calendarType: 'lunisolar', inLeapYear(calendarDate /*, cache */) { @@ -1523,7 +1517,7 @@ const helperHebrew: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { } }, adjustCalendarDate( - this: typeof helperHebrew & NonIsoHelperBase, + this: typeof helperHebrew & HelperSharedImpl, calendarDate, cache, overflow = 'constrain', @@ -1597,16 +1591,16 @@ const helperHebrew: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { }, // All built-in calendars except Chinese/Dangi and Hebrew use an era hasEra: false -} as Partial); +} as Partial); /** * For Temporal purposes, the Islamic calendar is simple because it's always the * same 12 months in the same order. */ -const helperIslamic: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { +const helperIslamic: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { id: 'islamic', calendarType: 'lunar', - inLeapYear(this: typeof helperIslamic & NonIsoHelperBase, calendarDate, cache) { + inLeapYear(this: typeof helperIslamic & HelperSharedImpl, calendarDate, cache) { // In leap years, the 12th month has 30 days. In non-leap years: 29. const days = this.daysInMonth({ year: calendarDate.year, month: 12, day: 1 }, cache); return days === 30; @@ -1619,13 +1613,13 @@ const helperIslamic: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { DAYS_PER_ISLAMIC_YEAR: 354 + 11 / 30, DAYS_PER_ISO_YEAR: 365.2425, constantEra: 'ah', - estimateIsoDate(this: typeof helperIslamic & NonIsoHelperBase, calendarDate) { + estimateIsoDate(this: typeof helperIslamic & HelperSharedImpl, calendarDate) { const { year } = this.adjustCalendarDate(calendarDate); return { year: MathFloor((year * this.DAYS_PER_ISLAMIC_YEAR) / this.DAYS_PER_ISO_YEAR) + 622, month: 1, day: 1 }; } -} as Partial); +} as Partial); -const helperPersian: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { +const helperPersian: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { id: 'persian', calendarType: 'solar', inLeapYear(calendarDate, cache) { @@ -1647,11 +1641,11 @@ const helperPersian: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { return month <= 6 ? 31 : 30; }, constantEra: 'ap', - estimateIsoDate(this: typeof helperPersian & NonIsoHelperBase, calendarDate) { + estimateIsoDate(this: typeof helperPersian & HelperSharedImpl, calendarDate) { const { year } = this.adjustCalendarDate(calendarDate); return { year: year + 621, month: 1, day: 1 }; } -} as Partial); +} as Partial); interface IndianMonthInfo { [month: number]: { @@ -1667,7 +1661,7 @@ interface IndianMonthInfo { }; } -const helperIndian: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { +const helperIndian: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { id: 'indian', calendarType: 'solar', inLeapYear(calendarDate /*, cache*/) { @@ -1712,7 +1706,7 @@ const helperIndian: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { if (this.inLeapYear(calendarDate) && monthInfo.leap) monthInfo = monthInfo.leap; return monthInfo; }, - estimateIsoDate(this: typeof helperIndian & NonIsoHelperBase, calendarDateParam) { + estimateIsoDate(this: typeof helperIndian & HelperSharedImpl, calendarDateParam) { // FYI, this "estimate" is always the exact ISO date, which makes the Indian // calendar fast! const calendarDate = this.adjustCalendarDate(calendarDateParam); @@ -1737,7 +1731,7 @@ const helperIndian: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { ); } } -} as Partial); +} as Partial); /** * Era metadata defined for each calendar. @@ -1925,7 +1919,7 @@ function isGregorianLeapYear(year: number) { /** Base for all Gregorian-like calendars. */ const makeHelperGregorian = (id: BuiltinCalendarId, originalEras: InputEra[]) => { const { eras, anchorEra } = adjustEras(originalEras); - const helperGregorian: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { + const helperGregorian: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { id, eras, anchorEra, @@ -1973,7 +1967,7 @@ const makeHelperGregorian = (id: BuiltinCalendarId, originalEras: InputEra[]) => eraYear = year - e.anchorEpoch.year + (e.hasYearZero ? 0 : 1); return true; } - const comparison = nonIsoHelperBase.compareCalendarDates(adjustedCalendarDate, e.anchorEpoch); + const comparison = helperSharedImpl.compareCalendarDates(adjustedCalendarDate, e.anchorEpoch); if (comparison >= 0) { eraYear = year - e.anchorEpoch.year + (e.hasYearZero ? 0 : 1); return true; @@ -2013,7 +2007,7 @@ const makeHelperGregorian = (id: BuiltinCalendarId, originalEras: InputEra[]) => return { ...calendarDate, year, eraYear, era }; }, adjustCalendarDate( - this: typeof helperGregorian & NonIsoHelperBase, + this: typeof helperGregorian & HelperSharedImpl, calendarDateParam, cache, overflow /*, fromLegacyDate = false */ @@ -2025,10 +2019,10 @@ const makeHelperGregorian = (id: BuiltinCalendarId, originalEras: InputEra[]) => this.validateCalendarDate(calendarDate); calendarDate = this.completeEraYear(calendarDate); // TODO this can become `super` later. - calendarDate = ReflectApply(nonIsoHelperBase.adjustCalendarDate, this, [calendarDate, cache, overflow]); + calendarDate = ReflectApply(helperSharedImpl.adjustCalendarDate, this, [calendarDate, cache, overflow]); return calendarDate; }, - estimateIsoDate(this: typeof helperGregorian & NonIsoHelperBase, calendarDateParam) { + estimateIsoDate(this: typeof helperGregorian & HelperSharedImpl, calendarDateParam) { const calendarDate = this.adjustCalendarDate(calendarDateParam); const { year, month, day } = calendarDate; const { anchorEra } = this; @@ -2053,12 +2047,12 @@ const makeHelperGregorian = (id: BuiltinCalendarId, originalEras: InputEra[]) => } } } - } as Partial); + } as Partial); return helperGregorian; }; const makeHelperOrthodox = (id: BuiltinCalendarId, originalEras: InputEra[]) => { - const base: NonIsoHelperBase = makeHelperGregorian(id, originalEras); + const base: HelperSharedImpl = makeHelperGregorian(id, originalEras); return ObjectAssign(base, { inLeapYear(calendarDate /*, cache */) { // Leap years happen one year before the Julian leap year. Note that this @@ -2084,7 +2078,7 @@ const makeHelperOrthodox = (id: BuiltinCalendarId, originalEras: InputEra[]) => maximumMonthLength(calendarDate) { return this.minimumMonthLength(calendarDate); } - } as Partial); + } as Partial); }; // `coptic` and `ethiopic` calendars are very similar to `ethioaa` calendar, @@ -2141,7 +2135,7 @@ const helperGregory = ObjectAssign( } ); -const helperJapanese: NonIsoHelperBase = ObjectAssign( +const helperJapanese: HelperSharedImpl = ObjectAssign( {}, // NOTE: Only the 5 modern eras (Meiji and later) are included. For dates // before Meiji 1, the `ce` and `bce` eras are used. Challenges with pre-Meiji @@ -2194,7 +2188,7 @@ const helperJapanese: NonIsoHelperBase = ObjectAssign( if (this.eras.find((e) => e.name === era)) return { era, eraYear }; return isoYear < 1 ? { era: 'bce', eraYear: 1 - isoYear } : { era: 'ce', eraYear: isoYear }; } - } as Partial + } as Partial ); interface ChineseMonthInfo { @@ -2204,7 +2198,7 @@ interface ChineseDraftMonthInfo { [key: string]: { monthIndex: number; daysInMonth?: number }; } -const helperChinese: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { +const helperChinese: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { id: 'chinese', calendarType: 'lunisolar', inLeapYear(calendarDate, cache) { @@ -2217,7 +2211,7 @@ const helperChinese: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { minimumMonthLength: (/* calendarDate */) => 29, maximumMonthLength: (/* calendarDate */) => 30, getMonthList( - this: NonIsoHelperBase, + this: HelperSharedImpl, calendarYear, cache ): { [key: string]: { monthIndex: string; daysInMonth: number } } { @@ -2298,7 +2292,7 @@ const helperChinese: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { return { year, month: month >= 12 ? 12 : month + 1, day: 1 }; }, adjustCalendarDate( - this: typeof helperChinese & NonIsoHelperBase, + this: typeof helperChinese & HelperSharedImpl, calendarDate, cache, overflow = 'constrain', @@ -2383,77 +2377,19 @@ const helperChinese: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { }, // All built-in calendars except Chinese/Dangi and Hebrew use an era hasEra: false -} as Partial); +} as Partial); // Dangi (Korean) calendar has same implementation as Chinese const helperDangi = ObjectAssign({}, { ...helperChinese, id: 'dangi' }); -interface NonIsoGeneralImpl { - dateFromFields( - this: NonIsoGeneralImpl & { helper: NonIsoHelperBase }, - fieldsParam: Params['dateFromFields'][0], - options: Params['dateFromFields'][1], - calendar: Temporal.Calendar - ): Temporal.PlainDate; - yearMonthFromFields( - this: NonIsoGeneralImpl & { helper: NonIsoHelperBase }, - fieldsParam: Params['yearMonthFromFields'][0], - options: Params['yearMonthFromFields'][1], - calendar: Temporal.Calendar - ): Temporal.PlainYearMonth; - monthDayFromFields( - this: NonIsoGeneralImpl & { helper: NonIsoHelperBase }, - fieldsParam: Params['monthDayFromFields'][0], - options: Params['monthDayFromFields'][1], - calendar: Temporal.Calendar - ): Temporal.PlainMonthDay; - fields(fieldsParam: string[]): Return['fields']; - mergeFields(fields: Params['mergeFields'][0], additionalFields: Params['mergeFields'][1]): Return['mergeFields']; - dateAdd( - this: NonIsoGeneralImpl & { helper: NonIsoHelperBase }, - date: Temporal.PlainDate, - years: number, - months: number, - weeks: number, - days: number, - overflow: Temporal.AssignmentOptions['overflow'], - calendar: Temporal.Calendar - ): Temporal.PlainDate; - dateUntil( - this: NonIsoGeneralImpl & { helper: NonIsoHelperBase }, - one: Temporal.PlainDate, - two: Temporal.PlainDate, - largestUnit: Temporal.DateUnit - ): { - years: number; - months: number; - weeks: number; - days: number; - }; - year(this: NonIsoGeneralImpl & { helper: NonIsoHelperBase }, date: Temporal.PlainDate): number; - month(this: NonIsoGeneralImpl & { helper: NonIsoHelperBase }, date: Temporal.PlainDate): number; - day(this: NonIsoGeneralImpl & { helper: NonIsoHelperBase }, date: Temporal.PlainDate): number; - era(this: NonIsoGeneralImpl & { helper: NonIsoHelperBase }, date: Temporal.PlainDate): string; - eraYear(this: NonIsoGeneralImpl & { helper: NonIsoHelperBase }, date: Temporal.PlainDate): number; - monthCode(this: NonIsoGeneralImpl & { helper: NonIsoHelperBase }, date: Temporal.PlainDate): string; - dayOfWeek(date: Temporal.PlainDate): number; - dayOfYear(this: NonIsoGeneralImpl & { helper: NonIsoHelperBase }, date: Temporal.PlainDate): number; - weekOfYear(date: Temporal.PlainDate): number; - daysInWeek(date: Temporal.PlainDate): number; - daysInMonth(this: NonIsoGeneralImpl & { helper: NonIsoHelperBase }, date: any): number; - daysInYear(this: NonIsoGeneralImpl & { helper: NonIsoHelperBase }, dateParam: any): number; - monthsInYear(this: NonIsoGeneralImpl & { helper: NonIsoHelperBase }, date: any): number; - inLeapYear(this: NonIsoGeneralImpl & { helper: NonIsoHelperBase }, dateParam: any): boolean; -} - /** * Common implementation of all non-ISO calendars. * Per-calendar id and logic live in `id` and `helper` properties attached later. * This split allowed an easy separation between code that was similar between * ISO and non-ISO implementations vs. code that was very different. */ -const nonIsoGeneralImpl: NonIsoGeneralImpl = { - dateFromFields(this: typeof nonIsoGeneralImpl & { helper: NonIsoHelperBase }, fieldsParam, options, calendar) { +const nonIsoImpl: NonIsoImpl = { + dateFromFields(this: NonIsoImplWithHelper, fieldsParam, options, calendar) { const overflow = ES.ToTemporalOverflow(options); const cache = new OneObjectCache(); // Intentionally alphabetical @@ -2470,7 +2406,7 @@ const nonIsoGeneralImpl: NonIsoGeneralImpl = { cache.setObject(result); return result; }, - yearMonthFromFields(this: typeof nonIsoGeneralImpl & { helper: NonIsoHelperBase }, fieldsParam, options, calendar) { + yearMonthFromFields(this: NonIsoImplWithHelper, fieldsParam, options, calendar) { const overflow = ES.ToTemporalOverflow(options); const cache = new OneObjectCache(); // Intentionally alphabetical @@ -2487,7 +2423,7 @@ const nonIsoGeneralImpl: NonIsoGeneralImpl = { return result; }, monthDayFromFields( - this: typeof nonIsoGeneralImpl & { helper: NonIsoHelperBase }, + this: NonIsoImplWithHelper, fieldsParam: Params['monthDayFromFields'][0], options: Params['monthDayFromFields'][1], calendar: Temporal.CalendarProtocol @@ -2546,13 +2482,13 @@ const nonIsoGeneralImpl: NonIsoGeneralImpl = { return { ...original, ...additionalFieldsCopy }; }, dateAdd( - this: typeof nonIsoGeneralImpl & { helper: NonIsoHelperBase }, + this: NonIsoImplWithHelper, date: Temporal.PlainDate, years: number, months: number, weeks: number, days: number, - overflow: Temporal.AssignmentOptions['overflow'], + overflow: Overflow, calendar: Temporal.Calendar ) { const cache = OneObjectCache.getCacheForObject(date); @@ -2567,7 +2503,7 @@ const nonIsoGeneralImpl: NonIsoGeneralImpl = { return newTemporalObject; }, dateUntil( - this: typeof nonIsoGeneralImpl & { helper: NonIsoHelperBase }, + this: NonIsoImplWithHelper, one: Temporal.PlainDate, two: Temporal.PlainDate, largestUnit: Temporal.DateUnit @@ -2579,34 +2515,34 @@ const nonIsoGeneralImpl: NonIsoGeneralImpl = { const result = this.helper.untilCalendar(calendarOne, calendarTwo, largestUnit, cacheOne); return result; }, - year(this: typeof nonIsoGeneralImpl & { helper: NonIsoHelperBase }, date: Temporal.PlainDate) { + year(this: NonIsoImplWithHelper, date: Temporal.PlainDate) { const cache = OneObjectCache.getCacheForObject(date); const calendarDate = this.helper.temporalToCalendarDate(date, cache); return calendarDate.year; }, - month(this: typeof nonIsoGeneralImpl & { helper: NonIsoHelperBase }, date: Temporal.PlainDate) { + month(this: NonIsoImplWithHelper, date: Temporal.PlainDate) { const cache = OneObjectCache.getCacheForObject(date); const calendarDate = this.helper.temporalToCalendarDate(date, cache); return calendarDate.month; }, - day(this: typeof nonIsoGeneralImpl & { helper: NonIsoHelperBase }, date: Temporal.PlainDate) { + day(this: NonIsoImplWithHelper, date: Temporal.PlainDate) { const cache = OneObjectCache.getCacheForObject(date); const calendarDate = this.helper.temporalToCalendarDate(date, cache); return calendarDate.day; }, - era(this: typeof nonIsoGeneralImpl & { helper: NonIsoHelperBase }, date: Temporal.PlainDate) { + era(this: NonIsoImplWithHelper, date: Temporal.PlainDate) { if (!this.helper.hasEra) return undefined; const cache = OneObjectCache.getCacheForObject(date); const calendarDate = this.helper.temporalToCalendarDate(date, cache); return calendarDate.era; }, - eraYear(this: typeof nonIsoGeneralImpl & { helper: NonIsoHelperBase }, date: Temporal.PlainDate) { + eraYear(this: NonIsoImplWithHelper, date: Temporal.PlainDate) { if (!this.helper.hasEra) return undefined; const cache = OneObjectCache.getCacheForObject(date); const calendarDate = this.helper.temporalToCalendarDate(date, cache); return calendarDate.eraYear; }, - monthCode(this: typeof nonIsoGeneralImpl & { helper: NonIsoHelperBase }, date: Temporal.PlainDate) { + monthCode(this: NonIsoImplWithHelper, date: Temporal.PlainDate) { const cache = OneObjectCache.getCacheForObject(date); const calendarDate = this.helper.temporalToCalendarDate(date, cache); return calendarDate.monthCode; @@ -2614,7 +2550,7 @@ const nonIsoGeneralImpl: NonIsoGeneralImpl = { dayOfWeek(date: Temporal.PlainDate) { return impl['iso8601'].dayOfWeek(date); }, - dayOfYear(this: typeof nonIsoGeneralImpl & { helper: NonIsoHelperBase }, date: Temporal.PlainDate) { + dayOfYear(this: NonIsoImplWithHelper, date: Temporal.PlainDate) { const cache = OneObjectCache.getCacheForObject(date); const calendarDate = this.helper.isoToCalendarDate(date, cache); const startOfYear = this.helper.startOfCalendarYear(calendarDate); @@ -2627,7 +2563,7 @@ const nonIsoGeneralImpl: NonIsoGeneralImpl = { daysInWeek(date: Temporal.PlainDate) { return impl['iso8601'].daysInWeek(date); }, - daysInMonth(this: typeof nonIsoGeneralImpl & { helper: NonIsoHelperBase }, date: any) { + daysInMonth(this: NonIsoImplWithHelper, date: any) { const cache = OneObjectCache.getCacheForObject(date); const calendarDate = this.helper.temporalToCalendarDate(date, cache); @@ -2644,7 +2580,7 @@ const nonIsoGeneralImpl: NonIsoGeneralImpl = { const result = this.helper.calendarDaysUntil(startOfMonthCalendar, startOfNextMonthCalendar, cache); return result; }, - daysInYear(this: typeof nonIsoGeneralImpl & { helper: NonIsoHelperBase }, dateParam) { + daysInYear(this: NonIsoImplWithHelper, dateParam) { let date = dateParam; if (!HasSlot(date, ISO_YEAR)) date = ES.ToTemporalDate(date); const cache = OneObjectCache.getCacheForObject(date); @@ -2654,13 +2590,13 @@ const nonIsoGeneralImpl: NonIsoGeneralImpl = { const result = this.helper.calendarDaysUntil(startOfYearCalendar, startOfNextYearCalendar, cache); return result; }, - monthsInYear(this: typeof nonIsoGeneralImpl & { helper: NonIsoHelperBase }, date) { + monthsInYear(this: NonIsoImplWithHelper, date) { const cache = OneObjectCache.getCacheForObject(date); const calendarDate = this.helper.temporalToCalendarDate(date, cache); const result = this.helper.monthsInYear(calendarDate, cache); return result; }, - inLeapYear(this: typeof nonIsoGeneralImpl & { helper: NonIsoHelperBase }, dateParam) { + inLeapYear(this: NonIsoImplWithHelper, dateParam) { let date = dateParam; if (!HasSlot(date, ISO_YEAR)) date = ES.ToTemporalDate(date); const cache = OneObjectCache.getCacheForObject(date); @@ -2670,22 +2606,22 @@ const nonIsoGeneralImpl: NonIsoGeneralImpl = { } }; -impl['hebrew'] = ObjectAssign({}, nonIsoGeneralImpl, { helper: helperHebrew }); -impl['islamic'] = ObjectAssign({}, nonIsoGeneralImpl, { helper: helperIslamic }); +impl['hebrew'] = ObjectAssign({}, nonIsoImpl, { helper: helperHebrew }); +impl['islamic'] = ObjectAssign({}, nonIsoImpl, { helper: helperIslamic }); (['islamic-umalqura', 'islamic-tbla', 'islamic-civil', 'islamic-rgsa', 'islamicc'] as const).forEach((id) => { - impl[id] = ObjectAssign({}, nonIsoGeneralImpl, { helper: { ...helperIslamic, id } }); + impl[id] = ObjectAssign({}, nonIsoImpl, { helper: { ...helperIslamic, id } }); }); -impl['persian'] = ObjectAssign({}, nonIsoGeneralImpl, { helper: helperPersian }); -impl['ethiopic'] = ObjectAssign({}, nonIsoGeneralImpl, { helper: helperEthiopic }); -impl['ethioaa'] = ObjectAssign({}, nonIsoGeneralImpl, { helper: helperEthioaa }); -impl['coptic'] = ObjectAssign({}, nonIsoGeneralImpl, { helper: helperCoptic }); -impl['chinese'] = ObjectAssign({}, nonIsoGeneralImpl, { helper: helperChinese }); -impl['dangi'] = ObjectAssign({}, nonIsoGeneralImpl, { helper: helperDangi }); -impl['roc'] = ObjectAssign({}, nonIsoGeneralImpl, { helper: helperRoc }); -impl['indian'] = ObjectAssign({}, nonIsoGeneralImpl, { helper: helperIndian }); -impl['buddhist'] = ObjectAssign({}, nonIsoGeneralImpl, { helper: helperBuddhist }); -impl['japanese'] = ObjectAssign({}, nonIsoGeneralImpl, { helper: helperJapanese }); -impl['gregory'] = ObjectAssign({}, nonIsoGeneralImpl, { helper: helperGregory }); +impl['persian'] = ObjectAssign({}, nonIsoImpl, { helper: helperPersian }); +impl['ethiopic'] = ObjectAssign({}, nonIsoImpl, { helper: helperEthiopic }); +impl['ethioaa'] = ObjectAssign({}, nonIsoImpl, { helper: helperEthioaa }); +impl['coptic'] = ObjectAssign({}, nonIsoImpl, { helper: helperCoptic }); +impl['chinese'] = ObjectAssign({}, nonIsoImpl, { helper: helperChinese }); +impl['dangi'] = ObjectAssign({}, nonIsoImpl, { helper: helperDangi }); +impl['roc'] = ObjectAssign({}, nonIsoImpl, { helper: helperRoc }); +impl['indian'] = ObjectAssign({}, nonIsoImpl, { helper: helperIndian }); +impl['buddhist'] = ObjectAssign({}, nonIsoImpl, { helper: helperBuddhist }); +impl['japanese'] = ObjectAssign({}, nonIsoImpl, { helper: helperJapanese }); +impl['gregory'] = ObjectAssign({}, nonIsoImpl, { helper: helperGregory }); const BUILTIN_CALENDAR_IDS = Object.keys(impl) as BuiltinCalendarId[]; From 3d386cae6971998b00c9b2d543e7761236e49e1e Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 15 Dec 2021 22:23:24 -0800 Subject: [PATCH 04/10] Use new calendar types throughout With the easy renames out of the way in the last commit, this commit does the hard work of using the new calendar types: * Strongly type all method parameters and remove all use of `any` * Remove now-unnecessary type casts because parameter types are now more accurate. * Ensure all code compiles with strictNullChecks:true and strictPropertyInitialization: true * Remove obsolete types --- lib/calendar.ts | 451 +++++++++++++++++++++--------------------------- 1 file changed, 199 insertions(+), 252 deletions(-) diff --git a/lib/calendar.ts b/lib/calendar.ts index 9c349499..80b8b902 100644 --- a/lib/calendar.ts +++ b/lib/calendar.ts @@ -41,35 +41,33 @@ const ObjectKeys = Object.keys; const ReflectApply = Reflect.apply; interface CalendarImpl { - year(date: Temporal.PlainDate | Temporal.PlainDateTime | Temporal.PlainYearMonth): number; - month(date: Temporal.PlainDate | Temporal.PlainDateTime | Temporal.PlainYearMonth | Temporal.PlainMonthDay): number; - monthCode( - date: Temporal.PlainDate | Temporal.PlainDateTime | Temporal.PlainYearMonth | Temporal.PlainMonthDay - ): string; - day(date: Temporal.PlainDate | Temporal.PlainDateTime | Temporal.PlainMonthDay): number; - era(date: Temporal.PlainDate | Temporal.PlainDateTime): string | undefined; - eraYear(date: Temporal.PlainDate | Temporal.PlainDateTime): number | undefined; - dayOfWeek(date: Temporal.PlainDate | Temporal.PlainDateTime): number; - dayOfYear(date: Temporal.PlainDate | Temporal.PlainDateTime): number; - weekOfYear(date: Temporal.PlainDate | Temporal.PlainDateTime): number; - daysInWeek(date: Temporal.PlainDate | Temporal.PlainDateTime): number; - daysInMonth(date: Temporal.PlainDate | Temporal.PlainDateTime | Temporal.PlainYearMonth): number; - daysInYear(date: Temporal.PlainDate | Temporal.PlainDateTime | Temporal.PlainYearMonth): number; - monthsInYear(date: Temporal.PlainDate | Temporal.PlainDateTime | Temporal.PlainYearMonth): number; - inLeapYear(date: Temporal.PlainDate | Temporal.PlainDateTime | Temporal.PlainYearMonth): boolean; + year(date: Temporal.PlainDate | Temporal.PlainYearMonth): number; + month(date: Temporal.PlainDate | Temporal.PlainYearMonth | Temporal.PlainMonthDay): number; + monthCode(date: Temporal.PlainDate | Temporal.PlainYearMonth | Temporal.PlainMonthDay): string; + day(date: Temporal.PlainDate | Temporal.PlainMonthDay): number; + era(date: Temporal.PlainDate | Temporal.PlainYearMonth): string | undefined; + eraYear(date: Temporal.PlainDate | Temporal.PlainYearMonth): number | undefined; + dayOfWeek(date: Temporal.PlainDate): number; + dayOfYear(date: Temporal.PlainDate): number; + weekOfYear(date: Temporal.PlainDate): number; + daysInWeek(date: Temporal.PlainDate): number; + daysInMonth(date: Temporal.PlainDate | Temporal.PlainYearMonth): number; + daysInYear(date: Temporal.PlainDate | Temporal.PlainYearMonth): number; + monthsInYear(date: Temporal.PlainDate | Temporal.PlainYearMonth): number; + inLeapYear(date: Temporal.PlainDate | Temporal.PlainYearMonth): boolean; dateFromFields( fields: Params['dateFromFields'][0], - options: Params['dateFromFields'][1], + options: NonNullable, calendar: Temporal.Calendar ): Temporal.PlainDate; yearMonthFromFields( fields: Params['yearMonthFromFields'][0], - options: Params['yearMonthFromFields'][1], + options: NonNullable, calendar: Temporal.Calendar ): Temporal.PlainYearMonth; monthDayFromFields( fields: Params['monthDayFromFields'][0], - options: Params['monthDayFromFields'][1], + options: NonNullable, calendar: Temporal.Calendar ): Temporal.PlainMonthDay; dateAdd( @@ -224,38 +222,40 @@ export class Calendar implements Temporal.Calendar { let date = dateParam; if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver'); if (!ES.IsTemporalYearMonth(date)) date = ES.ToTemporalDate(date); - return impl[GetSlot(this, CALENDAR_ID)].year(date as Temporal.PlainDate); + return impl[GetSlot(this, CALENDAR_ID)].year(date as Temporal.PlainDate | Temporal.PlainYearMonth); } month(dateParam: Params['month'][0]): Return['month'] { let date = dateParam; if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver'); if (ES.IsTemporalMonthDay(date)) throw new TypeError('use monthCode on PlainMonthDay instead'); if (!ES.IsTemporalYearMonth(date)) date = ES.ToTemporalDate(date); - return impl[GetSlot(this, CALENDAR_ID)].month(date as Temporal.PlainDate); + return impl[GetSlot(this, CALENDAR_ID)].month(date as Temporal.PlainDate | Temporal.PlainYearMonth); } monthCode(dateParam: Params['monthCode'][0]): Return['monthCode'] { let date = dateParam; if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver'); if (!ES.IsTemporalYearMonth(date) && !ES.IsTemporalMonthDay(date)) date = ES.ToTemporalDate(date); - return impl[GetSlot(this, CALENDAR_ID)].monthCode(date as Temporal.PlainDate); + return impl[GetSlot(this, CALENDAR_ID)].monthCode( + date as Temporal.PlainDate | Temporal.PlainMonthDay | Temporal.PlainYearMonth + ); } day(dateParam: Params['day'][0]): Return['day'] { let date = dateParam; if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver'); if (!ES.IsTemporalMonthDay(date)) date = ES.ToTemporalDate(date); - return impl[GetSlot(this, CALENDAR_ID)].day(date as Temporal.PlainDate); + return impl[GetSlot(this, CALENDAR_ID)].day(date as Temporal.PlainDate | Temporal.PlainMonthDay); } era(dateParam: Params['era'][0]): Return['era'] { let date = dateParam; if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver'); if (!ES.IsTemporalYearMonth(date)) date = ES.ToTemporalDate(date); - return impl[GetSlot(this, CALENDAR_ID)].era(date as Temporal.PlainDate); + return impl[GetSlot(this, CALENDAR_ID)].era(date as Temporal.PlainDate | Temporal.PlainYearMonth); } eraYear(dateParam: Params['eraYear'][0]): Return['eraYear'] { let date = dateParam; if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver'); if (!ES.IsTemporalYearMonth(date)) date = ES.ToTemporalDate(date); - return impl[GetSlot(this, CALENDAR_ID)].eraYear(date as Temporal.PlainDate); + return impl[GetSlot(this, CALENDAR_ID)].eraYear(date as Temporal.PlainDate | Temporal.PlainYearMonth); } dayOfWeek(dateParam: Params['dayOfWeek'][0]): Return['dayOfWeek'] { if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver'); @@ -281,33 +281,25 @@ export class Calendar implements Temporal.Calendar { let date = dateParam; if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver'); if (!ES.IsTemporalYearMonth(date)) date = ES.ToTemporalDate(date); - // TODO: is the cast below (here and in other methods) safe? What if it's - // a PlainYearMonth? - return impl[GetSlot(this, CALENDAR_ID)].daysInMonth(date as Temporal.PlainDate); + return impl[GetSlot(this, CALENDAR_ID)].daysInMonth(date as Temporal.PlainDate | Temporal.PlainYearMonth); } daysInYear(dateParam: Params['daysInYear'][0]): Return['daysInYear'] { let date = dateParam; if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver'); if (!ES.IsTemporalYearMonth(date)) date = ES.ToTemporalDate(date); - // TODO: is the cast below (here and in other methods) safe? What if it's - // a PlainYearMonth? - return impl[GetSlot(this, CALENDAR_ID)].daysInYear(date as Temporal.PlainDate); + return impl[GetSlot(this, CALENDAR_ID)].daysInYear(date as Temporal.PlainDate | Temporal.PlainYearMonth); } monthsInYear(dateParam: Params['monthsInYear'][0]): Return['monthsInYear'] { let date = dateParam; if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver'); if (!ES.IsTemporalYearMonth(date)) date = ES.ToTemporalDate(date); - // TODO: is the cast below (here and in other methods) safe? What if it's - // a PlainYearMonth? - return impl[GetSlot(this, CALENDAR_ID)].monthsInYear(date as Temporal.PlainDate); + return impl[GetSlot(this, CALENDAR_ID)].monthsInYear(date as Temporal.PlainDate | Temporal.PlainYearMonth); } inLeapYear(dateParam: Params['inLeapYear'][0]): Return['inLeapYear'] { let date = dateParam; if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver'); if (!ES.IsTemporalYearMonth(date)) date = ES.ToTemporalDate(date); - // TODO: is the cast below (here and in other methods) safe? What if it's - // a PlainYearMonth? - return impl[GetSlot(this, CALENDAR_ID)].inLeapYear(date as Temporal.PlainDate); + return impl[GetSlot(this, CALENDAR_ID)].inLeapYear(date as Temporal.PlainDate | Temporal.PlainYearMonth); } toString(): string { if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver'); @@ -335,7 +327,7 @@ impl['iso8601'] = { ['year'] ]); fields = resolveNonLunisolarMonth(fields); - let { year, month, day } = fields as any; + let { year, month, day } = fields; ({ year, month, day } = ES.RegulateISODate(year, month, day, overflow)); return ES.CreateTemporalDate(year, month, day, calendar); }, @@ -681,7 +673,7 @@ function buildMonthCode(month: number | string, leap = false) { * If both are present, make sure they match. * This logic doesn't work for lunisolar calendars! * */ -function resolveNonLunisolarMonth( +function resolveNonLunisolarMonth( calendarDate: T, overflow: Overflow = undefined, monthsPerYear = 12 @@ -725,7 +717,7 @@ class OneObjectCache { now: number; hits = 0; misses = 0; - constructor(cacheToClone: OneObjectCache = undefined) { + constructor(cacheToClone?: OneObjectCache) { this.now = globalThis.performance ? globalThis.performance.now() : Date.now(); if (cacheToClone !== undefined) { let i = 0; @@ -782,15 +774,6 @@ class OneObjectCache { } } -type CalendarDate = { - era?: string; - eraYear?: number; - year?: number; - month?: number; - monthCode?: string; - day?: number; -}; - function toUtcIsoDateString({ isoYear, isoMonth, isoDay }: { isoYear: number; isoMonth: number; isoDay: number }) { const yearString = ES.ISOYearString(isoYear); const monthString = ES.ISODateTimePartString(isoMonth); @@ -798,7 +781,7 @@ function toUtcIsoDateString({ isoYear, isoMonth, isoDay }: { isoYear: number; is return `${yearString}-${monthString}-${dayString}T00:00Z`; } -function simpleDateDiff(one: Required, two: Required) { +function simpleDateDiff(one: CalendarYMD, two: CalendarYMD) { return { years: one.year - two.year, months: one.month - two.month, @@ -806,75 +789,12 @@ function simpleDateDiff(one: Required, two: Required }; } -interface HelperSharedImpl { - id?: string; - isoToCalendarDate(isoDate: any, cache: any): any; - validateCalendarDate(calendarDate: any): void; - adjustCalendarDate(calendarDate: any, cache?: any, overflow?: any, fromLegacyDate?: any): any; - regulateMonthDayNaive(calendarDate: any, overflow: any, cache: any): any; - calendarToIsoDate(date: any, overflow: string, cache: any): any; - temporalToCalendarDate(date: any, cache: any): any; - compareCalendarDates(date1: any, date2: any): any; - regulateDate(calendarDate: any, overflow: string, cache: any): any; - addDaysIso(isoDate: any, days: any, cache?: any): any; - addDaysCalendar(calendarDate: any, days: any, cache: any): any; - addMonthsCalendar(calendarDate: any, months: any, overflow: any, cache: any): any; - addCalendar( - calendarDate: any, - { years, months, weeks, days }: { years?: number; months?: number; weeks?: number; days?: number }, - overflow: any, - cache: any - ): any; - untilCalendar( - calendarOne: any, - calendarTwo: any, - largestUnit: any, - cache: any - ): { years: number; months: number; weeks: number; days: number }; - daysInMonth(calendarDate: any, cache: any): any; - daysInPreviousMonth(calendarDate: any, cache: any): any; - startOfCalendarYear(calendarDate: any): { year: any; month: number; day: number }; - startOfCalendarMonth(calendarDate: any): { year: any; month: any; day: number }; - calendarDaysUntil(calendarOne: any, calendarTwo: any, cache: any): any; - isoDaysUntil(oneIso: any, twoIso: any): any; - eraLength: 'long' | 'short' | 'narrow'; - // reviseIntlEra can optionally be defined on subclasses of the base - reviseIntlEra?(calendarDate: any, isoDate?: any): { era: number; eraYear: number }; - hasEra: boolean; - constantEra?: string; - checkIcuBugs?(isoDate: any): void; - calendarType?: string; - monthsInYear?(calendarDate: any, cache?: any): number; - maximumMonthLength?(calendarDate?: any): number; - minimumMonthLength?(calendarDate?: any): number; - estimateIsoDate?(isoDate: any): any; - monthDayFromFields(fields: any, overflow: any, cache: any): any; - formatter?: globalThis.Intl.DateTimeFormat; - getFormatter(): globalThis.Intl.DateTimeFormat; - // Fields below here are only present in some subclasses. - // TODO: fix this up! - inLeapYear?(calendarDate: any, cache?: any): boolean; - getMonthCode?(year: number, month: number): string; - minMaxMonthLength?(calendarDate: any, minOrMax: 'min' | 'max'): number; - months?: any; // months metadata, differs per calendar - getMonthList?(year: number, cache: any): any; // chinese only - DAYS_PER_ISLAMIC_YEAR?: number; - DAYS_PER_ISO_YEAR?: number; - vulnerableToBceBug?: boolean; - eras?: Era[]; - anchorEra?: Era; - calendarIsVulnerableToJulianBug?: boolean; - v8IsVulnerableToJulianBug?: boolean; - completeEraYear?(calendarDate: any): any; // gregorian only - getMonthInfo?(calendarDate: any): any; -} - /** - * Implementation that's common to all non-trivial non-ISO calendars + * Implementation that's common to all non-ISO calendars */ const helperSharedImpl: HelperSharedImpl = { // The properties and methods below here should be the same for all lunar/lunisolar calendars. - getFormatter() { + getFormatter(this: HelperPerCalendarImpl) { // `new Intl.DateTimeFormat()` is amazingly slow and chews up RAM. Per // https://bugs.chromium.org/p/v8/issues/detail?id=6528#c4, we cache one // DateTimeFormat instance per calendar. Caching is lazy so we only pay for @@ -892,7 +812,7 @@ const helperSharedImpl: HelperSharedImpl = { } return this.formatter; }, - isoToCalendarDate(isoDate, cache) { + isoToCalendarDate(this: HelperPerCalendarImpl, isoDate, cache) { const { year: isoYear, month: isoMonth, day: isoDay } = isoDate; const key = JSON.stringify({ func: 'isoToCalendarDate', isoYear, isoMonth, isoDay, id: this.id }); const cached = cache.get(key); @@ -906,11 +826,11 @@ const helperSharedImpl: HelperSharedImpl = { } catch (e) { throw new RangeError(`Invalid ISO date: ${JSON.stringify({ isoYear, isoMonth, isoDay })}`); } - const result: any = {}; + const result: Partial = {}; for (let { type, value } of parts) { if (type === 'year') result.eraYear = +value; - // TODO: remove this type annotation when this value gets into TS lib types - if ((type as any) === 'relatedYear') result.eraYear = +value; + // TODO: remove this type annotation when `relatedYear` gets into TS lib types + if (type === ('relatedYear' as Intl.DateTimeFormatPartTypes)) result.eraYear = +value; if (type === 'month') { const matches = /^([-0-9.]+)(.*?)$/.exec(value); if (!matches || matches.length != 3) throw new RangeError(`Unexpected month: ${value}`); @@ -954,7 +874,7 @@ const helperSharedImpl: HelperSharedImpl = { ); } // Translate eras that may be handled differently by Temporal vs. by Intl - // (e.g. Japanese pre-Meiji eras). See #526 for details. + // (e.g. Japanese pre-Meiji eras). See https://github.com/tc39/proposal-temporal/issues/526. if (this.reviseIntlEra) { const { era, eraYear } = this.reviseIntlEra(result, isoDate); result.era = era; @@ -981,8 +901,11 @@ const helperSharedImpl: HelperSharedImpl = { }); return calendarDate; }, - validateCalendarDate(calendarDate) { - const { era, month, year, day, eraYear, monthCode, monthExtra } = calendarDate; + validateCalendarDate( + this: HelperPerCalendarImpl, + calendarDate: Partial + ): asserts calendarDate is FullCalendarDate { + const { era, month, year, day, eraYear, monthCode, monthExtra } = calendarDate as Partial; // When there's a suffix (e.g. "5bis" for a leap month in Chinese calendar) // the derived class must deal with it. if (monthExtra !== undefined) throw new RangeError('Unexpected `monthExtra` value'); @@ -1015,7 +938,7 @@ const helperSharedImpl: HelperSharedImpl = { * - no eras or a constant era defined in `.constantEra` * - non-lunisolar calendar (no leap months) * */ - adjustCalendarDate(calendarDateParam, cache, overflow /*, fromLegacyDate = false */) { + adjustCalendarDate(this: HelperPerCalendarImpl, calendarDateParam, cache, overflow /*, fromLegacyDate = false */) { if (this.calendarType === 'lunisolar') throw new RangeError('Override required for lunisolar calendars'); let calendarDate = calendarDateParam; this.validateCalendarDate(calendarDate); @@ -1032,13 +955,13 @@ const helperSharedImpl: HelperSharedImpl = { }; } - const largestMonth = this.monthsInYear(calendarDate, cache); + const largestMonth = this.monthsInYear(calendarDate as CalendarYearOnly, cache); let { month, monthCode } = calendarDate; ({ month, monthCode } = resolveNonLunisolarMonth(calendarDate, overflow, largestMonth)); - return { ...calendarDate, month, monthCode }; + return { ...(calendarDate as typeof calendarDate & CalendarYMD), month, monthCode }; }, - regulateMonthDayNaive(calendarDate, overflow, cache) { + regulateMonthDayNaive(this: HelperPerCalendarImpl, calendarDate, overflow, cache) { const largestMonth = this.monthsInYear(calendarDate, cache); let { month, day } = calendarDate; if (overflow === 'reject') { @@ -1050,8 +973,8 @@ const helperSharedImpl: HelperSharedImpl = { } return { ...calendarDate, month, day }; }, - calendarToIsoDate(dateParam, overflow = 'constrain', cache) { - const originalDate = dateParam; + calendarToIsoDate(this: HelperPerCalendarImpl, dateParam, overflow: Overflow = 'constrain', cache) { + const originalDate = dateParam as Partial; // First, normalize the calendar date to ensure that (year, month, day) // are all present, converting monthCode and eraYear if needed. let date = this.adjustCalendarDate(dateParam, cache, overflow, false); @@ -1204,8 +1127,8 @@ const helperSharedImpl: HelperSharedImpl = { compareCalendarDates(date1Param, date2Param) { // `date1` and `date2` are already records. The calls below simply validate // that all three required fields are present. - const date1 = ES.PrepareTemporalFields(date1Param, [['day'], ['month'], ['year']]) as any; - const date2 = ES.PrepareTemporalFields(date2Param, [['day'], ['month'], ['year']]) as any; + const date1 = ES.PrepareTemporalFields(date1Param, [['day'], ['month'], ['year']]); + const date2 = ES.PrepareTemporalFields(date2Param, [['day'], ['month'], ['year']]); if (date1.year !== date2.year) return ES.ComparisonResult(date1.year - date2.year); if (date1.month !== date2.month) return ES.ComparisonResult(date1.month - date2.month); if (date1.day !== date2.day) return ES.ComparisonResult(date1.day - date2.day); @@ -1226,7 +1149,7 @@ const helperSharedImpl: HelperSharedImpl = { const addedCalendar = this.isoToCalendarDate(addedIso, cache); return addedCalendar; }, - addMonthsCalendar(calendarDateParam, months, overflow, cache) { + addMonthsCalendar(this: HelperPerCalendarImpl, calendarDateParam, months, overflow, cache) { let calendarDate = calendarDateParam; const { day } = calendarDate; for (let i = 0, absMonths = MathAbs(months); i < absMonths; i++) { @@ -1300,7 +1223,7 @@ const helperSharedImpl: HelperSharedImpl = { // until we go over the target, then back up one month and calculate // remaining days and weeks. let current; - let next = yearsAdded; + let next: CalendarYMD = yearsAdded; do { months += sign; current = next; @@ -1318,7 +1241,7 @@ const helperSharedImpl: HelperSharedImpl = { } return { years, months, weeks, days }; }, - daysInMonth(calendarDate, cache) { + daysInMonth(this: HelperPerCalendarImpl, calendarDate, cache) { // Add enough days to roll over to the next month. One we're in the next // month, we can calculate the length of the current month. NOTE: This // algorithm assumes that months are continuous. It would break if a @@ -1344,7 +1267,7 @@ const helperSharedImpl: HelperSharedImpl = { const endOfMonthCalendar = this.isoToCalendarDate(endOfMonthIso, cache); return endOfMonthCalendar.day; }, - daysInPreviousMonth(calendarDate, cache) { + daysInPreviousMonth(this: HelperPerCalendarImpl, calendarDate, cache) { const { day, month, year } = calendarDate; // Check to see if we already know the month length, and return it if so @@ -1389,7 +1312,7 @@ const helperSharedImpl: HelperSharedImpl = { eraLength: 'short', // All built-in calendars except Chinese/Dangi and Hebrew use an era hasEra: true, - monthDayFromFields(fields, overflow, cache) { + monthDayFromFields(this: HelperPerCalendarImpl, fields, overflow, cache) { let { year, month, monthCode, day, era, eraYear } = fields; if (monthCode === undefined) { if (year === undefined && (era === undefined || eraYear === undefined)) { @@ -1451,10 +1374,10 @@ interface HebrewMonthInfo { }; } -const helperHebrew: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { +const helperHebrew: HelperPerCalendarImpl = ObjectAssign({}, helperSharedImpl as HelperPerCalendarImpl, { id: 'hebrew', calendarType: 'lunisolar', - inLeapYear(calendarDate /*, cache */) { + inLeapYear(calendarDate: CalendarYearOnly /*, cache: OneObjectCache */) { const { year } = calendarDate; // FYI: In addition to adding a month in leap years, the Hebrew calendar // also has per-year changes to the number of days of Heshvan and Kislev. @@ -1463,33 +1386,25 @@ const helperHebrew: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { // Hebrew-only prototype fields or methods. return (7 * year + 1) % 19 < 7; }, - monthsInYear(calendarDate) { + monthsInYear(calendarDate: CalendarYearOnly) { return this.inLeapYear(calendarDate) ? 13 : 12; }, - minimumMonthLength(calendarDate) { + minimumMonthLength(calendarDate: CalendarYM) { return this.minMaxMonthLength(calendarDate, 'min'); }, - maximumMonthLength(calendarDate) { + maximumMonthLength(calendarDate: CalendarYM) { return this.minMaxMonthLength(calendarDate, 'max'); }, - minMaxMonthLength(calendarDate, minOrMax) { + minMaxMonthLength(calendarDate: CalendarYM, minOrMax: 'min' | 'max') { const { month, year } = calendarDate; const monthCode = this.getMonthCode(year, month); - type HebrewMonths = { - [m: string]: { - leap: number; - regular: number | undefined; - monthCode: string; - days: number | { min: number; max: number }; - }; - }; - const monthInfo = ObjectEntries(this.months as HebrewMonths).find((m) => m[1].monthCode === monthCode); + const monthInfo = ObjectEntries(this.months).find((m) => m[1].monthCode === monthCode); if (monthInfo === undefined) throw new RangeError(`unmatched Hebrew month: ${month}`); const daysInMonth = monthInfo[1].days; return typeof daysInMonth === 'number' ? daysInMonth : daysInMonth[minOrMax]; }, /** Take a guess at what ISO date a particular calendar date corresponds to */ - estimateIsoDate(calendarDate) { + estimateIsoDate(calendarDate: CalendarYMD) { const { year } = calendarDate; return { year: year - 3760, month: 1, day: 1 }; }, @@ -1509,7 +1424,7 @@ const helperHebrew: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { Av: { leap: 12, regular: 11, monthCode: 'M11', days: 30 }, Elul: { leap: 13, regular: 12, monthCode: 'M12', days: 29 } }, - getMonthCode(year, month) { + getMonthCode(year: number, month: number) { if (this.inLeapYear({ year })) { return month === 6 ? buildMonthCode(5, true) : buildMonthCode(month < 6 ? month : month - 1); } else { @@ -1517,13 +1432,20 @@ const helperHebrew: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { } }, adjustCalendarDate( - this: typeof helperHebrew & HelperSharedImpl, - calendarDate, - cache, - overflow = 'constrain', + this: HelperPerCalendarImpl & { months: HebrewMonthInfo; getMonthCode(year: number, month: number): string }, + calendarDate: Partial, + cache?: OneObjectCache, + overflow: Overflow = 'constrain', fromLegacyDate = false ) { - let { year, eraYear, month, monthCode, day, monthExtra } = calendarDate; + // The incoming type is actually CalendarDate (same as args to + // Calendar.dateFromParams) but TS isn't smart enough to follow all the + // reassignments below, so as an alternative to 10+ type casts, we'll lie + // here and claim that the type has `day` and `year` filled in already. + let { year, eraYear, month, monthCode, day, monthExtra } = calendarDate as Omit< + typeof calendarDate, + 'year' | 'day' + > & { year: number; day: number }; if (year === undefined && eraYear !== undefined) year = eraYear; if (eraYear === undefined && year !== undefined) eraYear = year; if (fromLegacyDate) { @@ -1537,9 +1459,10 @@ const helperHebrew: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { if (monthExtra) { const monthInfo = this.months[monthExtra]; if (!monthInfo) throw new RangeError(`Unrecognized month from formatToParts: ${monthExtra}`); - month = this.inLeapYear({ year }) ? monthInfo.leap : monthInfo.regular; + month = (this.inLeapYear({ year }) ? monthInfo.leap : monthInfo.regular) as number; } - monthCode = this.getMonthCode(year, month); + // if we're getting data from legacy Date, then `month` will always be present + monthCode = this.getMonthCode(year, month as number); const result = { year, month, day, era: undefined as string | undefined, eraYear, monthCode }; return result; } else { @@ -1547,7 +1470,7 @@ const helperHebrew: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { // that all fields are present. this.validateCalendarDate(calendarDate); if (month === undefined) { - if (monthCode.endsWith('L')) { + if ((monthCode as string).endsWith('L')) { if (monthCode !== 'M05L') { throw new RangeError(`Hebrew leap month must have monthCode M05L, not ${monthCode}`); } @@ -1563,7 +1486,7 @@ const helperHebrew: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { } } } else { - month = monthCodeNumberPart(monthCode); + month = monthCodeNumberPart(monthCode as string); // if leap month is before this one, the month index is one more than the month code if (this.inLeapYear({ year }) && month > 6) month++; const largestMonth = this.monthsInYear({ year }); @@ -1591,16 +1514,16 @@ const helperHebrew: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { }, // All built-in calendars except Chinese/Dangi and Hebrew use an era hasEra: false -} as Partial); +}); /** * For Temporal purposes, the Islamic calendar is simple because it's always the * same 12 months in the same order. */ -const helperIslamic: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { +const helperIslamic: HelperPerCalendarImpl = ObjectAssign({}, helperSharedImpl, { id: 'islamic', calendarType: 'lunar', - inLeapYear(this: typeof helperIslamic & HelperSharedImpl, calendarDate, cache) { + inLeapYear(this: HelperPerCalendarImpl, calendarDate: CalendarYearOnly, cache: OneObjectCache) { // In leap years, the 12th month has 30 days. In non-leap years: 29. const days = this.daysInMonth({ year: calendarDate.year, month: 12, day: 1 }, cache); return days === 30; @@ -1613,16 +1536,19 @@ const helperIslamic: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { DAYS_PER_ISLAMIC_YEAR: 354 + 11 / 30, DAYS_PER_ISO_YEAR: 365.2425, constantEra: 'ah', - estimateIsoDate(this: typeof helperIslamic & HelperSharedImpl, calendarDate) { + estimateIsoDate( + this: HelperPerCalendarImpl & { DAYS_PER_ISLAMIC_YEAR: number; DAYS_PER_ISO_YEAR: number }, + calendarDate: CalendarYMD + ) { const { year } = this.adjustCalendarDate(calendarDate); return { year: MathFloor((year * this.DAYS_PER_ISLAMIC_YEAR) / this.DAYS_PER_ISO_YEAR) + 622, month: 1, day: 1 }; } -} as Partial); +}); -const helperPersian: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { +const helperPersian = ObjectAssign({}, helperSharedImpl, { id: 'persian', calendarType: 'solar', - inLeapYear(calendarDate, cache) { + inLeapYear(calendarDate: CalendarYearOnly, cache?: OneObjectCache) { // Same logic (count days in the last month) for Persian as for Islamic, // even though Persian is solar and Islamic is lunar. return helperIslamic.inLeapYear(calendarDate, cache); @@ -1630,22 +1556,22 @@ const helperPersian: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { monthsInYear(/* calendarYear, cache */) { return 12; }, - minimumMonthLength(calendarDate) { + minimumMonthLength(calendarDate: CalendarYM) { const { month } = calendarDate; if (month === 12) return 29; return month <= 6 ? 31 : 30; }, - maximumMonthLength(calendarDate) { + maximumMonthLength(calendarDate: CalendarYM) { const { month } = calendarDate; if (month === 12) return 30; return month <= 6 ? 31 : 30; }, constantEra: 'ap', - estimateIsoDate(this: typeof helperPersian & HelperSharedImpl, calendarDate) { + estimateIsoDate(this: HelperPerCalendarImpl, calendarDate: CalendarYMD) { const { year } = this.adjustCalendarDate(calendarDate); return { year: year + 621, month: 1, day: 1 }; } -} as Partial); +}); interface IndianMonthInfo { [month: number]: { @@ -1661,10 +1587,10 @@ interface IndianMonthInfo { }; } -const helperIndian: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { +const helperIndian: HelperPerCalendarImpl = ObjectAssign({}, helperSharedImpl, { id: 'indian', calendarType: 'solar', - inLeapYear(calendarDate /*, cache*/) { + inLeapYear(calendarDate: CalendarYearOnly /*, cache: OneObjectCache */) { // From https://en.wikipedia.org/wiki/Indian_national_calendar: // Years are counted in the Saka era, which starts its year 0 in the year 78 // of the Common Era. To determine leap years, add 78 to the Saka year – if @@ -1675,10 +1601,16 @@ const helperIndian: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { monthsInYear(/* calendarYear, cache */) { return 12; }, - minimumMonthLength(calendarDate) { + minimumMonthLength( + this: HelperPerCalendarImpl & { getMonthInfo(calendarDate: CalendarYM): IndianMonthInfo[number] }, + calendarDate: CalendarYM + ) { return this.getMonthInfo(calendarDate).length; }, - maximumMonthLength(calendarDate) { + maximumMonthLength( + this: HelperPerCalendarImpl & { getMonthInfo(calendarDate: CalendarYM): IndianMonthInfo[number] }, + calendarDate: CalendarYM + ) { return this.getMonthInfo(calendarDate).length; }, constantEra: 'saka', @@ -1699,14 +1631,17 @@ const helperIndian: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { 11: { length: 30, month: 1, nextYear: true, day: 21 }, 12: { length: 30, month: 2, nextYear: true, day: 20 } }, - getMonthInfo(calendarDate) { + getMonthInfo(this: HelperPerCalendarImpl & { months: IndianMonthInfo }, calendarDate: CalendarYM) { const { month } = calendarDate; let monthInfo = this.months[month]; if (monthInfo === undefined) throw new RangeError(`Invalid month: ${month}`); if (this.inLeapYear(calendarDate) && monthInfo.leap) monthInfo = monthInfo.leap; return monthInfo; }, - estimateIsoDate(this: typeof helperIndian & HelperSharedImpl, calendarDateParam) { + estimateIsoDate( + this: HelperPerCalendarImpl & { getMonthInfo(calendarDate: CalendarYM): IndianMonthInfo[number] }, + calendarDateParam: CalendarYMD + ) { // FYI, this "estimate" is always the exact ISO date, which makes the Indian // calendar fast! const calendarDate = this.adjustCalendarDate(calendarDateParam); @@ -1723,7 +1658,7 @@ const helperIndian: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { // expected. vulnerableToBceBug: new Date('0000-01-01T00:00Z').toLocaleDateString('en-US-u-ca-indian', { timeZone: 'UTC' }) !== '10/11/-79 Saka', - checkIcuBugs(isoDate) { + checkIcuBugs(isoDate: IsoYMD) { if (this.vulnerableToBceBug && isoDate.year < 1) { throw new RangeError( `calendar '${this.id}' is broken for ISO dates before 0001-01-01` + @@ -1731,7 +1666,7 @@ const helperIndian: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { ); } } -} as Partial); +}); /** * Era metadata defined for each calendar. @@ -1750,7 +1685,7 @@ interface InputEra { * then a calendar month and day are included. Otherwise * `{ month: 1, day: 1 }` is assumed. */ - anchorEpoch?: { year: number } | { year: number; month: number; day: number }; + anchorEpoch?: CalendarYearOnly | CalendarYMD; /** ISO date of the first day of this era */ isoEpoch?: { year: number; month: number; day: number }; @@ -1784,7 +1719,7 @@ interface Era { /** * alternate name of the era used in old versions of ICU data - *format is `era{n}` where n is the zero-based index of the era + * format is `era{n}` where n is the zero-based index of the era * with the oldest era being 0. * */ genericName: string; @@ -1797,10 +1732,10 @@ interface Era { * mid-year then a calendar month and day are included. * Otherwise `{ month: 1, day: 1 }` is assumed. */ - anchorEpoch: { year: number; month: number; day: number }; + anchorEpoch: CalendarYMD; /** ISO date of the first day of this era */ - isoEpoch: { year: number; month: number; day: number }; + isoEpoch: IsoYMD; /** * If present, then this era counts years backwards like BC @@ -1847,7 +1782,7 @@ function adjustEras(erasParam: InputEra[]): { eras: Era[]; anchorEra: Era } { // Find the "anchor era" which is the era used for (era-less) `year`. Reversed // eras can never be anchors. The era without an `anchorEpoch` property is the // anchor. - let anchorEra: Era | InputEra; + let anchorEra: Era | InputEra | undefined; eras.forEach((e) => { if (e.isAnchor || (!e.anchorEpoch && !e.reverseOf)) { if (anchorEra) throw new RangeError('Invalid era data: cannot have multiple anchor eras'); @@ -1919,12 +1854,12 @@ function isGregorianLeapYear(year: number) { /** Base for all Gregorian-like calendars. */ const makeHelperGregorian = (id: BuiltinCalendarId, originalEras: InputEra[]) => { const { eras, anchorEra } = adjustEras(originalEras); - const helperGregorian: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { + const helperGregorian = ObjectAssign({}, helperSharedImpl, { id, eras, anchorEra, calendarType: 'solar', - inLeapYear(calendarDate /*, cache: OneObjectCache */) { + inLeapYear(this: HelperPerCalendarImpl, calendarDate: CalendarYearOnly /*, cache: OneObjectCache */) { // Calendars that don't override this method use the same months and leap // years as Gregorian. Once we know the ISO year corresponding to the // calendar year, we'll know if it's a leap year or not. @@ -1934,17 +1869,17 @@ const makeHelperGregorian = (id: BuiltinCalendarId, originalEras: InputEra[]) => monthsInYear(/* calendarDate */) { return 12; }, - minimumMonthLength(calendarDate) { + minimumMonthLength(this: HelperPerCalendarImpl, calendarDate: CalendarYM) { const { month } = calendarDate; if (month === 2) return this.inLeapYear(calendarDate) ? 29 : 28; return [4, 6, 9, 11].indexOf(month) >= 0 ? 30 : 31; }, - maximumMonthLength(calendarDate) { + maximumMonthLength(this: HelperPerCalendarImpl, calendarDate: CalendarYM) { return this.minimumMonthLength(calendarDate); }, /** Fill in missing parts of the (year, era, eraYear) tuple */ - completeEraYear(calendarDate) { - const checkField = (name: string, value: string | number) => { + completeEraYear(this: HelperPerCalendarImpl & { eras: Era[] }, calendarDate: Partial) { + const checkField = (name: keyof FullCalendarDate, value: string | number | undefined) => { const currentValue = calendarDate[name]; if (currentValue != null && currentValue != value) { throw new RangeError(`Input ${name} ${currentValue} doesn't match calculated value ${value}`); @@ -2007,25 +1942,26 @@ const makeHelperGregorian = (id: BuiltinCalendarId, originalEras: InputEra[]) => return { ...calendarDate, year, eraYear, era }; }, adjustCalendarDate( - this: typeof helperGregorian & HelperSharedImpl, - calendarDateParam, - cache, - overflow /*, fromLegacyDate = false */ + this: HelperPerCalendarImpl & { completeEraYear(calendarDate: Partial): FullCalendarDate }, + calendarDateParam: Partial, + cache: OneObjectCache, + overflow: Overflow + /*, fromLegacyDate = false */ ) { let calendarDate = calendarDateParam; // Because this is not a lunisolar calendar, it's safe to convert monthCode to a number const { month, monthCode } = calendarDate; - if (month === undefined) calendarDate = { ...calendarDate, month: monthCodeNumberPart(monthCode) }; + if (month === undefined) calendarDate = { ...calendarDate, month: monthCodeNumberPart(monthCode as string) }; this.validateCalendarDate(calendarDate); calendarDate = this.completeEraYear(calendarDate); // TODO this can become `super` later. calendarDate = ReflectApply(helperSharedImpl.adjustCalendarDate, this, [calendarDate, cache, overflow]); return calendarDate; }, - estimateIsoDate(this: typeof helperGregorian & HelperSharedImpl, calendarDateParam) { + estimateIsoDate(this: HelperPerCalendarImpl, calendarDateParam: CalendarYMD) { const calendarDate = this.adjustCalendarDate(calendarDateParam); const { year, month, day } = calendarDate; - const { anchorEra } = this; + const { anchorEra } = this as { anchorEra: Era }; const isoYearEstimate = year + anchorEra.isoEpoch.year - (anchorEra.hasYearZero ? 0 : 1); return ES.RegulateISODate(isoYearEstimate, month, day, 'constrain'); }, @@ -2036,7 +1972,7 @@ const makeHelperGregorian = (id: BuiltinCalendarId, originalEras: InputEra[]) => .toLocaleDateString('en-US-u-ca-japanese', { timeZone: 'UTC' }) .startsWith('12'), calendarIsVulnerableToJulianBug: false, - checkIcuBugs(isoDate) { + checkIcuBugs(isoDate: IsoYMD) { if (this.calendarIsVulnerableToJulianBug && this.v8IsVulnerableToJulianBug) { const beforeJulianSwitch = ES.CompareISODate(isoDate.year, isoDate.month, isoDate.day, 1582, 10, 15) < 0; if (beforeJulianSwitch) { @@ -2047,14 +1983,14 @@ const makeHelperGregorian = (id: BuiltinCalendarId, originalEras: InputEra[]) => } } } - } as Partial); + }); return helperGregorian; }; const makeHelperOrthodox = (id: BuiltinCalendarId, originalEras: InputEra[]) => { - const base: HelperSharedImpl = makeHelperGregorian(id, originalEras); + const base = makeHelperGregorian(id, originalEras); return ObjectAssign(base, { - inLeapYear(calendarDate /*, cache */) { + inLeapYear(calendarDate: CalendarYearOnly /*, cache: OneObjectCache */) { // Leap years happen one year before the Julian leap year. Note that this // calendar is based on the Julian calendar which has a leap year every 4 // years, unlike the Gregorian calendar which doesn't have leap years on @@ -2069,16 +2005,16 @@ const makeHelperOrthodox = (id: BuiltinCalendarId, originalEras: InputEra[]) => monthsInYear(/* calendarDate */) { return 13; }, - minimumMonthLength(calendarDate) { + minimumMonthLength(this: HelperPerCalendarImpl, calendarDate: CalendarYM) { const { month } = calendarDate; // Ethiopian/Coptic calendars have 12 30-day months and an extra 5-6 day 13th month. if (month === 13) return this.inLeapYear(calendarDate) ? 6 : 5; return 30; }, - maximumMonthLength(calendarDate) { + maximumMonthLength(this: HelperPerCalendarImpl, calendarDate: CalendarYM) { return this.minimumMonthLength(calendarDate); } - } as Partial); + }); }; // `coptic` and `ethiopic` calendars are very similar to `ethioaa` calendar, @@ -2119,23 +2055,23 @@ const helperBuddhist = ObjectAssign( } ); -const helperGregory = ObjectAssign( +const helperGregory: HelperPerCalendarImpl = ObjectAssign( {}, makeHelperGregorian('gregory', [ { name: 'ce', isoEpoch: { year: 1, month: 1, day: 1 } }, { name: 'bce', reverseOf: 'ce' } ]), { - reviseIntlEra(calendarDate: { era?: string; eraYear?: number } /*, isoDate*/) { + reviseIntlEra>(calendarDate: T /*, isoDate: IsoDate*/): T { let { era, eraYear } = calendarDate; if (era === 'bc') era = 'bce'; if (era === 'ad') era = 'ce'; - return { era, eraYear }; + return { era, eraYear } as T; } } ); -const helperJapanese: HelperSharedImpl = ObjectAssign( +const helperJapanese: HelperPerCalendarImpl = ObjectAssign( {}, // NOTE: Only the 5 modern eras (Meiji and later) are included. For dates // before Meiji 1, the `ce` and `bce` eras are used. Challenges with pre-Meiji @@ -2182,13 +2118,17 @@ const helperJapanese: HelperSharedImpl = ObjectAssign( // default "short" era, so need to use the long format. eraLength: 'long', calendarIsVulnerableToJulianBug: true, - reviseIntlEra(this: typeof helperJapanese, calendarDate, isoDate) { + reviseIntlEra>( + this: HelperPerCalendarImpl & { eras: Era[] }, + calendarDate: T, + isoDate: IsoYMD + ): T { const { era, eraYear } = calendarDate; const { year: isoYear } = isoDate; - if (this.eras.find((e) => e.name === era)) return { era, eraYear }; - return isoYear < 1 ? { era: 'bce', eraYear: 1 - isoYear } : { era: 'ce', eraYear: isoYear }; + if (this.eras.find((e) => e.name === era)) return { era, eraYear } as T; + return (isoYear < 1 ? { era: 'bce', eraYear: 1 - isoYear } : { era: 'ce', eraYear: isoYear }) as T; } - } as Partial + } ); interface ChineseMonthInfo { @@ -2198,23 +2138,23 @@ interface ChineseDraftMonthInfo { [key: string]: { monthIndex: number; daysInMonth?: number }; } -const helperChinese: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { +const helperChinese: HelperPerCalendarImpl = ObjectAssign({}, helperSharedImpl, { id: 'chinese', calendarType: 'lunisolar', - inLeapYear(calendarDate, cache) { - const months = (this as typeof helperChinese).getMonthList(calendarDate.year, cache); + inLeapYear( + this: HelperPerCalendarImpl & { getMonthList(year: number, cache: OneObjectCache): ChineseMonthInfo }, + calendarDate: CalendarYearOnly, + cache: OneObjectCache + ) { + const months = this.getMonthList(calendarDate.year, cache as OneObjectCache); return ObjectEntries(months).length === 13; }, - monthsInYear(calendarDate, cache) { - return (this as typeof helperChinese).inLeapYear(calendarDate, cache) ? 13 : 12; + monthsInYear(this: HelperPerCalendarImpl, calendarDate: CalendarYearOnly, cache: OneObjectCache) { + return this.inLeapYear(calendarDate, cache) ? 13 : 12; }, minimumMonthLength: (/* calendarDate */) => 29, maximumMonthLength: (/* calendarDate */) => 30, - getMonthList( - this: HelperSharedImpl, - calendarYear, - cache - ): { [key: string]: { monthIndex: string; daysInMonth: number } } { + getMonthList(this: HelperPerCalendarImpl, calendarYear: number, cache: OneObjectCache): ChineseMonthInfo { if (calendarYear === undefined) { throw new TypeError('Missing year'); } @@ -2228,8 +2168,8 @@ const helperChinese: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { // Now add the requested number of days, which may wrap to the next month. legacyDate.setUTCDate(daysPastFeb1 + 1); const newYearGuess = dateTimeFormat.formatToParts(legacyDate); - const calendarMonthString = newYearGuess.find((tv) => tv.type === 'month').value; - const calendarDay = +newYearGuess.find((tv) => tv.type === 'day').value; + const calendarMonthString = (newYearGuess.find((tv) => tv.type === 'month') as Intl.DateTimeFormatPart).value; + const calendarDay = +(newYearGuess.find((tv) => tv.type === 'day') as Intl.DateTimeFormatPart).value; let calendarYearToVerify: globalThis.Intl.DateTimeFormatPart | number | undefined = newYearGuess.find( (tv) => (tv.type as string) === 'relatedYear' ); @@ -2260,15 +2200,15 @@ const helperChinese: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { // Now back up to near the start of the first month, but not too near that // off-by-one issues matter. isoDaysDelta -= calendarDay - 5; - const result = {} as any; // TODO: type the month list result + const result = {} as ChineseDraftMonthInfo; let monthIndex = 1; - let oldCalendarDay: number; - let oldMonthString: string; + let oldCalendarDay: number | undefined; + let oldMonthString: string | undefined; let done = false; do { ({ calendarMonthString, calendarDay, calendarYearToVerify } = getCalendarDate(calendarYear, isoDaysDelta)); if (oldCalendarDay) { - result[oldMonthString].daysInMonth = oldCalendarDay + 30 - calendarDay; + result[oldMonthString as string].daysInMonth = oldCalendarDay + 30 - calendarDay; } if (calendarYearToVerify !== calendarYear) { done = true; @@ -2285,19 +2225,19 @@ const helperChinese: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { result[oldMonthString].daysInMonth = oldCalendarDay + 30 - calendarDay; cache.set(key, result); - return result; + return result as ChineseMonthInfo; }, - estimateIsoDate(calendarDate) { + estimateIsoDate(calendarDate: CalendarYMD) { const { year, month } = calendarDate; return { year, month: month >= 12 ? 12 : month + 1, day: 1 }; }, adjustCalendarDate( - this: typeof helperChinese & HelperSharedImpl, - calendarDate, - cache, - overflow = 'constrain', + this: HelperPerCalendarImpl & { getMonthList(year: number, cache: OneObjectCache): ChineseMonthInfo }, + calendarDate: Partial, + cache: OneObjectCache, + overflow: Overflow = 'constrain', fromLegacyDate = false - ) { + ): FullCalendarDate { let { year, month, monthExtra, day, monthCode, eraYear } = calendarDate; if (fromLegacyDate) { // Legacy Date output returns a string that's an integer with an optional @@ -2305,13 +2245,13 @@ const helperChinese: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { // month. Below we'll normalize the output. year = eraYear; if (monthExtra && monthExtra !== 'bis') throw new RangeError(`Unexpected leap month suffix: ${monthExtra}`); - const monthCode = buildMonthCode(month, monthExtra !== undefined); + const monthCode = buildMonthCode(month as number, monthExtra !== undefined); const monthString = `${month}${monthExtra || ''}`; - const months = (this as typeof helperChinese).getMonthList(year, cache); + const months = this.getMonthList(year as number, cache); const monthInfo = months[monthString]; if (monthInfo === undefined) throw new RangeError(`Unmatched month ${monthString} in Chinese year ${year}`); month = monthInfo.monthIndex; - return { year, month, day, era: undefined, eraYear, monthCode }; + return { year: year as number, month, day: day as number, era: undefined, eraYear, monthCode }; } else { // When called without input coming from legacy Date output, // simply ensure that all fields are present. @@ -2319,19 +2259,19 @@ const helperChinese: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { if (year === undefined) year = eraYear; if (eraYear === undefined) eraYear = year; if (month === undefined) { - const months = (this as typeof helperChinese).getMonthList(year, cache); - let numberPart = monthCode.replace('L', 'bis').slice(1); + const months = this.getMonthList(year as number, cache); + let numberPart = (monthCode as string).replace('L', 'bis').slice(1); if (numberPart[0] === '0') numberPart = numberPart.slice(1); let monthInfo = months[numberPart]; month = monthInfo && monthInfo.monthIndex; // If this leap month isn't present in this year, constrain down to the last day of the previous month. if ( month === undefined && - monthCode.endsWith('L') && - !ArrayIncludes.call(['M01L', 'M12L', 'M13L'], monthCode) && + (monthCode as string).endsWith('L') && + !ArrayIncludes.call(['M01L', 'M12L', 'M13L'], monthCode as string) && overflow === 'constrain' ) { - let withoutML = monthCode.slice(1, -1); + let withoutML = (monthCode as string).slice(1, -1); if (withoutML[0] === '0') withoutML = withoutML.slice(1); monthInfo = months[withoutML]; if (monthInfo) { @@ -2343,17 +2283,17 @@ const helperChinese: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { throw new RangeError(`Unmatched month ${monthCode} in Chinese year ${year}`); } } else if (monthCode === undefined) { - const months = (this as typeof helperChinese).getMonthList(year, cache); + const months = this.getMonthList(year as number, cache); const monthEntries = ObjectEntries(months); const largestMonth = monthEntries.length; if (overflow === 'reject') { ES.RejectToRange(month, 1, largestMonth); - ES.RejectToRange(day, 1, this.maximumMonthLength()); + ES.RejectToRange(day as number, 1, this.maximumMonthLength()); } else { month = ES.ConstrainToRange(month, 1, largestMonth); day = ES.ConstrainToRange(day, 1, this.maximumMonthLength()); } - const matchingMonthEntry = monthEntries.find(([, v]) => (v as { monthIndex: string }).monthIndex === month); + const matchingMonthEntry = monthEntries.find(([, v]) => v.monthIndex === month); if (matchingMonthEntry === undefined) { throw new RangeError(`Invalid month ${month} in Chinese year ${year}`); } @@ -2363,7 +2303,7 @@ const helperChinese: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { ); } else { // Both month and monthCode are present. Make sure they don't conflict. - const months = (this as typeof helperChinese).getMonthList(year, cache); + const months = this.getMonthList(year as number, cache); let numberPart = monthCode.replace('L', 'bis').slice(1); if (numberPart[0] === '0') numberPart = numberPart.slice(1); const monthInfo = months[numberPart]; @@ -2372,12 +2312,19 @@ const helperChinese: HelperSharedImpl = ObjectAssign({}, helperSharedImpl, { throw new RangeError(`monthCode ${monthCode} doesn't correspond to month ${month} in Chinese year ${year}`); } } - return { ...calendarDate, year, eraYear, month, monthCode, day }; + return { + ...calendarDate, + year: year as number, + eraYear, + month, + monthCode: monthCode as string, + day: day as number + }; } }, // All built-in calendars except Chinese/Dangi and Hebrew use an era hasEra: false -} as Partial); +}); // Dangi (Korean) calendar has same implementation as Chinese const helperDangi = ObjectAssign({}, { ...helperChinese, id: 'dangi' }); @@ -2425,7 +2372,7 @@ const nonIsoImpl: NonIsoImpl = { monthDayFromFields( this: NonIsoImplWithHelper, fieldsParam: Params['monthDayFromFields'][0], - options: Params['monthDayFromFields'][1], + options: NonNullable, calendar: Temporal.CalendarProtocol ) { const overflow = ES.ToTemporalOverflow(options); @@ -2563,7 +2510,7 @@ const nonIsoImpl: NonIsoImpl = { daysInWeek(date: Temporal.PlainDate) { return impl['iso8601'].daysInWeek(date); }, - daysInMonth(this: NonIsoImplWithHelper, date: any) { + daysInMonth(this: NonIsoImplWithHelper, date) { const cache = OneObjectCache.getCacheForObject(date); const calendarDate = this.helper.temporalToCalendarDate(date, cache); From 1ad8315ad5f0aded09db9cdbbf7f6a2ce68a6818 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 15 Dec 2021 22:38:38 -0800 Subject: [PATCH 05/10] More precise TS types in ecmascript.ts This commit makes ecmascript.ts changes for TS: * Support `strictNullChecks: true`. This means that any type that could possbly be undefined or null needs to be declared as such. This leads to a lot of types needing to be redefied to add or to exclude `undefined`. * Support `strictPropertyInitialization: true`. This means that all variables must be explicitly declared before they're used. * Remove use of `any` and replace with more precise types. Unlike calendar.ts, there were no bulk renames in this file. Just lots of one-off changes. --- lib/ecmascript.ts | 256 +++++++++++++++++++++++++++++----------------- 1 file changed, 162 insertions(+), 94 deletions(-) diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index ba0bf001..f391ef39 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -295,19 +295,18 @@ export function RejectObjectWithCalendarOrTimeZone(item: AnyTemporalLikeType) { if (HasSlot(item, CALENDAR) || HasSlot(item, TIME_ZONE)) { throw new TypeError('with() does not support a calendar or timeZone property'); } - if ((item as any).calendar !== undefined) { + if ((item as { calendar: unknown }).calendar !== undefined) { throw new TypeError('with() does not support a calendar property'); } - if ((item as any).timeZone !== undefined) { + if ((item as { timeZone: unknown }).timeZone !== undefined) { throw new TypeError('with() does not support a timeZone property'); } } function ParseTemporalTimeZone(stringIdent: string) { - // TODO: why aren't these three variables destructured to include `undefined` as possible types? let { ianaName, offset, z } = ParseTemporalTimeZoneString(stringIdent); if (ianaName) return ianaName; if (z) return 'UTC'; - return offset; // if !ianaName && !z then offset must be present + return offset as string; // if !ianaName && !z then offset must be present } function FormatCalendarAnnotation(id: string, showCalendar: Temporal.ShowCalendarOption['calendarName']) { @@ -521,7 +520,7 @@ function ParseTemporalInstant(isoString: string) { const epochNs = GetEpochFromISOParts(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond); if (epochNs === null) throw new RangeError('DateTime outside of supported range'); - const offsetNs = z ? 0 : ParseTimeZoneOffsetString(offset); + const offsetNs = z ? 0 : ParseTimeZoneOffsetString(offset as string); return JSBI.subtract(epochNs, JSBI.BigInt(offsetNs)); } @@ -734,7 +733,7 @@ export function ToTemporalRoundingMode( return GetOption(options, 'roundingMode', ['ceil', 'floor', 'trunc', 'halfExpand'], fallback); } -export function NegateTemporalRoundingMode(roundingMode: Temporal.ToStringPrecisionOptions['roundingMode']) { +export function NegateTemporalRoundingMode(roundingMode: Temporal.RoundingMode) { switch (roundingMode) { case 'ceil': return 'floor'; @@ -747,7 +746,7 @@ export function NegateTemporalRoundingMode(roundingMode: Temporal.ToStringPrecis export function ToTemporalOffset( options: Temporal.OffsetDisambiguationOptions, - fallback: Temporal.OffsetDisambiguationOptions['offset'] + fallback: Required['offset'] ) { return GetOption(options, 'offset', ['prefer', 'use', 'ignore', 'reject'], fallback); } @@ -861,31 +860,35 @@ export function ToSecondsStringPrecision(options: Temporal.ToStringPrecisionOpti } } -export function ToLargestTemporalUnit( +type ToSingularUnit | 'auto'> = Exclude< + T, + Temporal.PluralUnit | 'auto' +>; + +export function ToLargestTemporalUnit( options: { largestUnit?: Temporal.LargestUnit }, - fallback: Allowed | 'auto', - disallowedStrings?: ReadonlyArray -): Allowed | 'auto'; + fallback: undefined +): ToSingularUnit | 'auto' | undefined; export function ToLargestTemporalUnit< - Allowed extends Temporal.DateTimeUnit, - Disallowed extends Temporal.DateTimeUnit, - IfAuto extends Allowed | undefined = Allowed + Allowed extends Temporal.LargestUnit, + Disallowed extends Temporal.DateTimeUnit >( - options: { largestUnit?: Temporal.LargestUnit }, - fallback: Allowed | 'auto', + options: { largestUnit?: Allowed | undefined }, + fallback: 'auto', disallowedStrings: ReadonlyArray, - autoValue?: IfAuto -): Allowed; + autoValue: ToSingularUnit +): ToSingularUnit; export function ToLargestTemporalUnit< Allowed extends Temporal.DateTimeUnit, - Disallowed extends Temporal.DateTimeUnit, - IfAuto extends Allowed | undefined = Allowed + Disallowed extends ToSingularUnit>, + Fallback extends ToSingularUnit | 'auto' | undefined >( options: { largestUnit?: Temporal.LargestUnit }, - fallback: Allowed | 'auto', + fallback: Fallback, disallowedStrings: ReadonlyArray = [], - autoValue?: IfAuto -): IfAuto extends undefined ? Allowed | 'auto' : Allowed { + autoValue?: Exclude, 'auto'> | undefined +): ToSingularUnit | (Fallback extends undefined ? undefined : 'auto') { + type Ret = ToSingularUnit | (Fallback extends undefined ? undefined : 'auto'); const singular = new Map( SINGULAR_PLURAL_UNITS.filter(([, sing]) => !disallowedStrings.includes(sing as Disallowed)) ) as Map, Allowed>; @@ -894,33 +897,35 @@ export function ToLargestTemporalUnit< allowed.delete(s as unknown as Allowed); } const retval = GetOption(options, 'largestUnit', ['auto', ...allowed, ...singular.keys()], fallback); - type RetType = IfAuto extends undefined ? Allowed | 'auto' : Allowed; if (retval === 'auto' && autoValue !== undefined) return autoValue; if (singular.has(retval as Temporal.PluralUnit)) { - return singular.get(retval as Temporal.PluralUnit) as RetType; + return singular.get(retval as Temporal.PluralUnit) as Ret; } - return retval as RetType; + return retval as Ret; } export function ToSmallestTemporalUnit< - Allowed extends Temporal.DateTimeUnit, - Fallback extends Allowed, - Disallowed extends Temporal.DateTimeUnit + Allowed extends Temporal.SmallestUnit, + Fallback extends ToSingularUnit | undefined, + Disallowed extends ToSingularUnit> >( - options: { smallestUnit?: Temporal.SmallestUnit }, + options: { smallestUnit?: Allowed | undefined }, fallback: Fallback, disallowedStrings: ReadonlyArray = [] -): Allowed { +): ToSingularUnit | (Fallback extends undefined ? undefined : never) { + type Ret = ToSingularUnit | (Fallback extends undefined ? undefined : never); const singular = new Map( SINGULAR_PLURAL_UNITS.filter(([, sing]) => !disallowedStrings.includes(sing as Disallowed)) - ) as Map, Allowed>; + ) as Map>; const allowed = new Set(ALLOWED_UNITS) as Set; for (const s of disallowedStrings) { allowed.delete(s as unknown as Allowed); } const value = GetOption(options, 'smallestUnit', [...allowed, ...singular.keys()], fallback); - if (singular.has(value as Temporal.PluralUnit)) return singular.get(value as Temporal.PluralUnit); - return value as Allowed; + if (singular.has(value as Allowed)) { + return singular.get(value as Allowed) as Ret; + } + return value as Ret; } export function ToTemporalDurationTotalUnit(options: { @@ -948,9 +953,7 @@ export function ToRelativeTemporalObject(options: { | undefined; }): Temporal.ZonedDateTime | Temporal.PlainDate | undefined { const relativeTo = options.relativeTo; - // TODO: `as undefined` below should not be needed. Verify that it can be - // removed after strictNullChecks is enabled. - if (relativeTo === undefined) return relativeTo as undefined; + if (relativeTo === undefined) return relativeTo; let offsetBehaviour: OffsetBehaviour = 'option'; let matchMinutes = false; @@ -1042,7 +1045,7 @@ export function DefaultTemporalLargestUnit( milliseconds: number, microseconds: number, nanoseconds: number -) { +): Temporal.DateTimeUnit { const singular = new Map(SINGULAR_PLURAL_UNITS); for (const [prop, v] of [ ['years', years], @@ -1056,7 +1059,7 @@ export function DefaultTemporalLargestUnit( ['microseconds', microseconds], ['nanoseconds', nanoseconds] ] as const) { - if (v !== 0) return singular.get(prop); + if (v !== 0) return singular.get(prop) as Temporal.DateTimeUnit; } return 'nanosecond'; } @@ -1286,7 +1289,7 @@ export function ToTemporalDate( export function InterpretTemporalDateTimeFields( calendar: Temporal.CalendarProtocol, - fields: Required>, + fields: Pick>, options: Temporal.AssignmentOptions ) { let { hour, minute, second, millisecond, microsecond, nanosecond } = ToTemporalTimeRecord(fields); @@ -1442,8 +1445,7 @@ export function ToTemporalMonthDay( ToTemporalOverflow(options); // validate and ignore let { month, day, referenceISOYear, calendar: maybeStringCalendar } = ParseTemporalMonthDayString(ToString(item)); - // TODO: should this be a ternary? - let calendar: Temporal.CalendarProtocol | string = maybeStringCalendar; + let calendar: Temporal.CalendarProtocol | string | undefined = maybeStringCalendar; if (calendar === undefined) calendar = GetISO8601Calendar(); calendar = ToTemporalCalendar(calendar); @@ -1458,7 +1460,7 @@ export function ToTemporalMonthDay( export function ToTemporalTime( itemParam: PlainTimeParams['from'][0], - overflow: PlainTimeParams['from'][1]['overflow'] = 'constrain' + overflow: NonNullable['overflow'] = 'constrain' ) { let item = itemParam; let hour, minute, second, millisecond, microsecond, nanosecond, calendar; @@ -1552,7 +1554,7 @@ export function InterpretISODateTimeOffset( offsetBehaviour: OffsetBehaviour, offsetNs: number, timeZone: Temporal.TimeZoneProtocol, - disambiguation: Temporal.ToInstantOptions['disambiguation'], + disambiguation: NonNullable, offsetOpt: Temporal.OffsetDisambiguationOptions['offset'], matchMinute: boolean ) { @@ -1618,7 +1620,7 @@ export function ToTemporalZonedDateTime( microsecond: number, nanosecond: number, timeZone, - offset: string, + offset: string | undefined, calendar: string | Temporal.CalendarProtocol; let matchMinute = false; let offsetBehaviour: OffsetBehaviour = 'option'; @@ -1668,7 +1670,9 @@ export function ToTemporalZonedDateTime( matchMinute = true; // ISO strings may specify offset with less precision } let offsetNs = 0; - if (offsetBehaviour === 'option') offsetNs = ParseTimeZoneOffsetString(offset); + // The code above guarantees that if offsetBehaviour === 'option', then + // `offset` is not undefined. + if (offsetBehaviour === 'option') offsetNs = ParseTimeZoneOffsetString(offset as string); const disambiguation = ToTemporalDisambiguation(options); const offsetOpt = ToTemporalOffset(options, 'reject'); const epochNanoseconds = InterpretISODateTimeOffset( @@ -2080,7 +2084,7 @@ export function ToTemporalCalendar(calendarLikeParam: CalendarParams['from'][0]) if (IsObject(calendarLike)) { if (HasSlot(calendarLike, CALENDAR)) return GetSlot(calendarLike, CALENDAR); if (!('calendar' in calendarLike)) return calendarLike; - calendarLike = calendarLike.calendar; + calendarLike = (calendarLike as unknown as { calendar: string | Temporal.CalendarProtocol }).calendar; if (IsObject(calendarLike) && !('calendar' in calendarLike)) return calendarLike; } const identifier = ToString(calendarLike); @@ -2097,10 +2101,10 @@ export function ToTemporalCalendar(calendarLikeParam: CalendarParams['from'][0]) } function GetTemporalCalendarWithISODefault( - item: Temporal.CalendarProtocol | { calendar?: Temporal.PlainDateLike['calendar'] } | undefined + item: Temporal.CalendarProtocol | { calendar?: Temporal.PlainDateLike['calendar'] } ): Temporal.Calendar | Temporal.CalendarProtocol { if (HasSlot(item, CALENDAR)) return GetSlot(item, CALENDAR); - const { calendar } = item; + const { calendar } = item as Exclude; if (calendar === undefined) return GetISO8601Calendar(); return ToTemporalCalendar(calendar); } @@ -2160,7 +2164,7 @@ export function ToTemporalTimeZone(temporalTimeZoneLikeParam: TimeZoneParams['fr if (IsObject(temporalTimeZoneLike)) { if (IsTemporalZonedDateTime(temporalTimeZoneLike)) return GetSlot(temporalTimeZoneLike, TIME_ZONE); if (!('timeZone' in temporalTimeZoneLike)) return temporalTimeZoneLike; - temporalTimeZoneLike = (temporalTimeZoneLike as { timeZone: typeof temporalTimeZoneLike }).timeZone; + temporalTimeZoneLike = (temporalTimeZoneLike as unknown as { timeZone: typeof temporalTimeZoneLike }).timeZone; if (IsObject(temporalTimeZoneLike) && !('timeZone' in temporalTimeZoneLike)) { return temporalTimeZoneLike; } @@ -2247,7 +2251,7 @@ export function BuiltinTimeZoneGetPlainDateTimeFor( export function BuiltinTimeZoneGetInstantFor( timeZone: Temporal.TimeZoneProtocol, dateTime: Temporal.PlainDateTime, - disambiguation: Temporal.ToInstantOptions['disambiguation'] + disambiguation: NonNullable ) { const possibleInstants = GetPossibleInstantsFor(timeZone, dateTime); return DisambiguatePossibleInstants(possibleInstants, timeZone, dateTime, disambiguation); @@ -2257,7 +2261,7 @@ function DisambiguatePossibleInstants( possibleInstants: Temporal.Instant[], timeZone: Temporal.TimeZoneProtocol, dateTime: Temporal.PlainDateTime, - disambiguation: Temporal.ToInstantOptions['disambiguation'] + disambiguation: NonNullable ) { const Instant = GetIntrinsic('%Temporal.Instant%'); const numInstants = possibleInstants.length; @@ -2469,14 +2473,16 @@ export function TemporalInstantToString( return `${year}-${month}-${day}T${hour}:${minute}${seconds}${timeZoneString}`; } +interface ToStringOptions { + unit: ReturnType['unit']; + increment: number; + roundingMode: ReturnType; +} + export function TemporalDurationToString( duration: Temporal.Duration, precision: Temporal.ToStringPrecisionOptions['fractionalSecondDigits'] = 'auto', - options: { - unit: ReturnType['unit']; - increment: number; - roundingMode: ReturnType; - } = undefined + options: ToStringOptions | undefined = undefined ) { function formatNumber(num: number) { if (num <= NumberMaxSafeInteger) return num.toString(10); @@ -2558,11 +2564,7 @@ export function TemporalDateTimeToString( dateTime: Temporal.PlainDateTime, precision: ReturnType['precision'], showCalendar: ReturnType = 'auto', - options: { - unit: ReturnType['unit']; - increment: number; - roundingMode: ReturnType; - } = undefined + options: ToStringOptions | undefined = undefined ) { let year = GetSlot(dateTime, ISO_YEAR); let month = GetSlot(dateTime, ISO_MONTH); @@ -2645,11 +2647,7 @@ export function TemporalZonedDateTimeToString( showCalendar: ReturnType = 'auto', showTimeZone: ReturnType = 'auto', showOffset: ReturnType = 'auto', - options: { - unit: ReturnType['unit']; - increment: number; - roundingMode: ReturnType; - } = undefined + options: ToStringOptions | undefined = undefined ) { let instant = GetSlot(zdt, INSTANT); @@ -2917,7 +2915,7 @@ export function GetIANATimeZoneEpochValue( } return epochNanoseconds; }) - .filter((x) => x !== undefined); + .filter((x) => x !== undefined) as JSBI[]; } export function LeapYear(year: number) { @@ -3341,7 +3339,7 @@ export function UnbalanceDurationRelative( const sign = DurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0); let calendar; - let relativeTo: Temporal.PlainDate; + let relativeTo: Temporal.PlainDate | undefined; if (relativeToParam) { relativeTo = ToTemporalDate(relativeToParam); calendar = GetSlot(relativeTo, CALENDAR); @@ -3361,7 +3359,7 @@ export function UnbalanceDurationRelative( // balance years down to months const dateAdd = calendar.dateAdd; const dateUntil = calendar.dateUntil; - let relativeToDateOnly: Temporal.PlainDateLike = relativeTo; + let relativeToDateOnly: Temporal.PlainDateLike = relativeTo as Temporal.PlainDateLike; while (MathAbs(years) > 0) { const addOptions = ObjectCreate(null); const newRelativeTo = CalendarDateAdd(calendar, relativeToDateOnly, oneYear, addOptions, dateAdd); @@ -3380,7 +3378,7 @@ export function UnbalanceDurationRelative( // balance years down to days while (MathAbs(years) > 0) { let oneYearDays; - ({ relativeTo, days: oneYearDays } = MoveRelativeDate(calendar, relativeTo, oneYear)); + ({ relativeTo, days: oneYearDays } = MoveRelativeDate(calendar, relativeTo as Temporal.PlainDate, oneYear)); days += oneYearDays; years -= sign; } @@ -3388,7 +3386,7 @@ export function UnbalanceDurationRelative( // balance months down to days while (MathAbs(months) > 0) { let oneMonthDays; - ({ relativeTo, days: oneMonthDays } = MoveRelativeDate(calendar, relativeTo, oneMonth)); + ({ relativeTo, days: oneMonthDays } = MoveRelativeDate(calendar, relativeTo as Temporal.PlainDate, oneMonth)); days += oneMonthDays; months -= sign; } @@ -3398,7 +3396,7 @@ export function UnbalanceDurationRelative( while (MathAbs(years) > 0) { if (!calendar) throw new RangeError('a starting point is required for balancing calendar units'); let oneYearDays; - ({ relativeTo, days: oneYearDays } = MoveRelativeDate(calendar, relativeTo, oneYear)); + ({ relativeTo, days: oneYearDays } = MoveRelativeDate(calendar, relativeTo as Temporal.PlainDate, oneYear)); days += oneYearDays; years -= sign; } @@ -3407,7 +3405,7 @@ export function UnbalanceDurationRelative( while (MathAbs(months) > 0) { if (!calendar) throw new RangeError('a starting point is required for balancing calendar units'); let oneMonthDays; - ({ relativeTo, days: oneMonthDays } = MoveRelativeDate(calendar, relativeTo, oneMonth)); + ({ relativeTo, days: oneMonthDays } = MoveRelativeDate(calendar, relativeTo as Temporal.PlainDate, oneMonth)); days += oneMonthDays; months -= sign; } @@ -3416,7 +3414,7 @@ export function UnbalanceDurationRelative( while (MathAbs(weeks) > 0) { if (!calendar) throw new RangeError('a starting point is required for balancing calendar units'); let oneWeekDays; - ({ relativeTo, days: oneWeekDays } = MoveRelativeDate(calendar, relativeTo, oneWeek)); + ({ relativeTo, days: oneWeekDays } = MoveRelativeDate(calendar, relativeTo as Temporal.PlainDate, oneWeek)); days += oneWeekDays; weeks -= sign; } @@ -3443,7 +3441,7 @@ export function BalanceDurationRelative( if (sign === 0) return { years, months, weeks, days }; let calendar; - let relativeTo: Temporal.PlainDate; + let relativeTo: Temporal.PlainDate | undefined; if (relativeToParam) { relativeTo = ToTemporalDate(relativeToParam); calendar = GetSlot(relativeTo, CALENDAR); @@ -3458,7 +3456,11 @@ export function BalanceDurationRelative( if (!calendar) throw new RangeError('a starting point is required for years balancing'); // balance days up to years let newRelativeTo, oneYearDays; - ({ relativeTo: newRelativeTo, days: oneYearDays } = MoveRelativeDate(calendar, relativeTo, oneYear)); + ({ relativeTo: newRelativeTo, days: oneYearDays } = MoveRelativeDate( + calendar, + relativeTo as Temporal.PlainDate, + oneYear + )); while (MathAbs(days) >= MathAbs(oneYearDays)) { days -= oneYearDays; years += sign; @@ -3468,7 +3470,11 @@ export function BalanceDurationRelative( // balance days up to months let oneMonthDays; - ({ relativeTo: newRelativeTo, days: oneMonthDays } = MoveRelativeDate(calendar, relativeTo, oneMonth)); + ({ relativeTo: newRelativeTo, days: oneMonthDays } = MoveRelativeDate( + calendar, + relativeTo as Temporal.PlainDate, + oneMonth + )); while (MathAbs(days) >= MathAbs(oneMonthDays)) { days -= oneMonthDays; months += sign; @@ -3479,11 +3485,17 @@ export function BalanceDurationRelative( // balance months up to years const dateAdd = calendar.dateAdd; const addOptions = ObjectCreate(null); - newRelativeTo = CalendarDateAdd(calendar, relativeTo, oneYear, addOptions, dateAdd); + newRelativeTo = CalendarDateAdd(calendar, relativeTo as Temporal.PlainDate, oneYear, addOptions, dateAdd); const dateUntil = calendar.dateUntil; const untilOptions = ObjectCreate(null); untilOptions.largestUnit = 'month'; - let untilResult = CalendarDateUntil(calendar, relativeTo, newRelativeTo, untilOptions, dateUntil); + let untilResult = CalendarDateUntil( + calendar, + relativeTo as Temporal.PlainDate, + newRelativeTo, + untilOptions, + dateUntil + ); let oneYearMonths = GetSlot(untilResult, MONTHS); while (MathAbs(months) >= MathAbs(oneYearMonths)) { months -= oneYearMonths; @@ -3502,7 +3514,11 @@ export function BalanceDurationRelative( if (!calendar) throw new RangeError('a starting point is required for months balancing'); // balance days up to months let newRelativeTo, oneMonthDays; - ({ relativeTo: newRelativeTo, days: oneMonthDays } = MoveRelativeDate(calendar, relativeTo, oneMonth)); + ({ relativeTo: newRelativeTo, days: oneMonthDays } = MoveRelativeDate( + calendar, + relativeTo as Temporal.PlainDate, + oneMonth + )); while (MathAbs(days) >= MathAbs(oneMonthDays)) { days -= oneMonthDays; months += sign; @@ -3515,7 +3531,11 @@ export function BalanceDurationRelative( if (!calendar) throw new RangeError('a starting point is required for weeks balancing'); // balance days up to weeks let newRelativeTo, oneWeekDays; - ({ relativeTo: newRelativeTo, days: oneWeekDays } = MoveRelativeDate(calendar, relativeTo, oneWeek)); + ({ relativeTo: newRelativeTo, days: oneWeekDays } = MoveRelativeDate( + calendar, + relativeTo as Temporal.PlainDate, + oneWeek + )); while (MathAbs(days) >= MathAbs(oneWeekDays)) { days -= oneWeekDays; weeks += sign; @@ -3575,8 +3595,10 @@ export function CreateNegatedTemporalDuration(duration: Temporal.Duration) { ); } -export function ConstrainToRange(value: number, min: number, max: number) { - return MathMin(max, MathMax(min, value)); +export function ConstrainToRange(value: number | undefined, min: number, max: number) { + // Math.Max accepts undefined values and returns NaN. Undefined values are + // used for optional params in the method below. + return MathMin(max, MathMax(min, value as number)); } function ConstrainISODate(year: number, monthParam: number, dayParam?: number) { const month = ConstrainToRange(monthParam, 1, 12); @@ -4283,7 +4305,7 @@ export function AddDateTime( milliseconds: number, microseconds: number, nanoseconds: number, - options: Temporal.ArithmeticOptions + options?: Temporal.ArithmeticOptions ) { let days = daysParam; // Add the time part @@ -4519,7 +4541,7 @@ function DaysUntil( function MoveRelativeDate( calendar: Temporal.CalendarProtocol, - relativeToParam: ReturnType, + relativeToParam: NonNullable>, duration: Temporal.Duration ) { const options = ObjectCreate(null); @@ -4733,7 +4755,9 @@ export function RoundDuration( // First convert time units up to days, if rounding to days or higher units. // If rounding relative to a ZonedDateTime, then some days may not be 24h. - let dayLengthNs: JSBI; + // TS doesn't know that `dayLengthNs` is only used if the unit is day or + // larger. This makes the cast below acceptable. + let dayLengthNs: JSBI = undefined as unknown as JSBI; if (unit === 'year' || unit === 'month' || unit === 'week' || unit === 'day') { nanoseconds = TotalDurationNanoseconds(0, hours, minutes, seconds, milliseconds, microseconds, nanosecondsParam, 0); let intermediate; @@ -4758,10 +4782,22 @@ export function RoundDuration( const yearsDuration = new TemporalDuration(years); const dateAdd = calendar.dateAdd; const firstAddOptions = ObjectCreate(null); - const yearsLater = CalendarDateAdd(calendar, relativeTo, yearsDuration, firstAddOptions, dateAdd); + const yearsLater = CalendarDateAdd( + calendar, + relativeTo as Temporal.PlainDate, + yearsDuration, + firstAddOptions, + dateAdd + ); const yearsMonthsWeeks = new TemporalDuration(years, months, weeks); const secondAddOptions = ObjectCreate(null); - const yearsMonthsWeeksLater = CalendarDateAdd(calendar, relativeTo, yearsMonthsWeeks, secondAddOptions, dateAdd); + const yearsMonthsWeeksLater = CalendarDateAdd( + calendar, + relativeTo as Temporal.PlainDate, + yearsMonthsWeeks, + secondAddOptions, + dateAdd + ); const monthsWeeksInDays = DaysUntil(yearsLater, yearsMonthsWeeksLater); relativeTo = yearsLater; days += monthsWeeksInDays; @@ -4811,10 +4847,22 @@ export function RoundDuration( const yearsMonths = new TemporalDuration(years, months); const dateAdd = calendar.dateAdd; const firstAddOptions = ObjectCreate(null); - const yearsMonthsLater = CalendarDateAdd(calendar, relativeTo, yearsMonths, firstAddOptions, dateAdd); + const yearsMonthsLater = CalendarDateAdd( + calendar, + relativeTo as Temporal.PlainDate, + yearsMonths, + firstAddOptions, + dateAdd + ); const yearsMonthsWeeks = new TemporalDuration(years, months, weeks); const secondAddOptions = ObjectCreate(null); - const yearsMonthsWeeksLater = CalendarDateAdd(calendar, relativeTo, yearsMonthsWeeks, secondAddOptions, dateAdd); + const yearsMonthsWeeksLater = CalendarDateAdd( + calendar, + relativeTo as Temporal.PlainDate, + yearsMonthsWeeks, + secondAddOptions, + dateAdd + ); const weeksInDays = DaysUntil(yearsMonthsLater, yearsMonthsWeeksLater); relativeTo = yearsMonthsLater; days += weeksInDays; @@ -4854,7 +4902,7 @@ export function RoundDuration( const sign = MathSign(days); const oneWeek = new TemporalDuration(0, 0, days < 0 ? -1 : 1); let oneWeekDays; - ({ relativeTo, days: oneWeekDays } = MoveRelativeDate(calendar, relativeTo, oneWeek)); + ({ relativeTo, days: oneWeekDays } = MoveRelativeDate(calendar, relativeTo as Temporal.PlainDate, oneWeek)); while (MathAbs(days) >= MathAbs(oneWeekDays)) { weeks += sign; days -= oneWeekDays; @@ -5073,7 +5121,7 @@ export function ComparisonResult(value: number) { } export function GetOptionsObject(options: T) { - if (options === undefined) return ObjectCreate(null) as T; + if (options === undefined) return ObjectCreate(null) as NonNullable; if (IsObject(options) && options !== null) return options; throw new TypeError(`Options parameter must be an object, not ${options === null ? 'null' : `${typeof options}`}`); } @@ -5085,11 +5133,31 @@ export function CreateOnePropObject(propName: K, propValue: } function GetOption

>>( + options: O, + property: P, + allowedValues: ReadonlyArray, + fallback: undefined +): O[P]; +function GetOption< + P extends string, + O extends Partial>, + Fallback extends Required[P] | undefined +>( + options: O, + property: P, + allowedValues: ReadonlyArray, + fallback: Fallback +): Fallback extends undefined ? O[P] | undefined : Required[P]; +function GetOption< + P extends string, + O extends Partial>, + Fallback extends Required[P] | undefined +>( options: O, property: P, allowedValues: ReadonlyArray, fallback: O[P] -) { +): Fallback extends undefined ? O[P] | undefined : Required[P] { let value = options[property]; if (value !== undefined) { value = ToString(value) as O[P]; From 196295e9dcf619703e4aeda32ee94df196cb7f90 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 15 Dec 2021 22:40:45 -0800 Subject: [PATCH 06/10] Tighten types of intl.ts * Refactor type names for clarity * Add Temporal.Instant to list of accepted types --- lib/intl.ts | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/lib/intl.ts b/lib/intl.ts index 79fa58e2..b5c3bdc8 100644 --- a/lib/intl.ts +++ b/lib/intl.ts @@ -16,16 +16,7 @@ import { TIME_ZONE } from './slots'; import { Temporal, Intl } from '..'; -import { - DateTimeFormatParams as Params, - DateTimeFormatReturn as Return, - InstantParams, - PlainDateParams, - PlainDateTimeParams, - PlainMonthDayParams, - PlainTimeParams, - PlainYearMonthParams -} from './internaltypes'; +import { DateTimeFormatParams as Params, DateTimeFormatReturn as Return } from './internaltypes'; const DATE = Symbol('date'); const YM = Symbol('ym'); @@ -340,7 +331,9 @@ function amend(optionsParam: Intl.DateTimeFormatOptions = {}, amended: MaybeFals return options as globalThis.Intl.DateTimeFormatOptions; } -function timeAmend(optionsParam: Intl.DateTimeFormatOptions) { +type OptionsType = NonNullable[1]>; + +function timeAmend(optionsParam: OptionsType) { let options = amend(optionsParam, { year: false, month: false, @@ -359,7 +352,7 @@ function timeAmend(optionsParam: Intl.DateTimeFormatOptions) { return options; } -function yearMonthAmend(optionsParam: PlainYearMonthParams['toLocaleString'][1]) { +function yearMonthAmend(optionsParam: OptionsType) { let options = amend(optionsParam, { day: false, hour: false, @@ -377,7 +370,7 @@ function yearMonthAmend(optionsParam: PlainYearMonthParams['toLocaleString'][1]) return options; } -function monthDayAmend(optionsParam: PlainMonthDayParams['toLocaleString'][1]) { +function monthDayAmend(optionsParam: OptionsType) { let options = amend(optionsParam, { year: false, hour: false, @@ -395,7 +388,7 @@ function monthDayAmend(optionsParam: PlainMonthDayParams['toLocaleString'][1]) { return options; } -function dateAmend(optionsParam: PlainDateParams['toLocaleString'][1]) { +function dateAmend(optionsParam: OptionsType) { let options = amend(optionsParam, { hour: false, minute: false, @@ -414,7 +407,7 @@ function dateAmend(optionsParam: PlainDateParams['toLocaleString'][1]) { return options; } -function datetimeAmend(optionsParam: PlainDateTimeParams['toLocaleString'][1]) { +function datetimeAmend(optionsParam: OptionsType) { let options = amend(optionsParam, { timeZoneName: false }); if (!hasTimeOptions(options) && !hasDateOptions(options)) { options = ObjectAssign({}, options, { @@ -429,7 +422,7 @@ function datetimeAmend(optionsParam: PlainDateTimeParams['toLocaleString'][1]) { return options; } -function zonedDateTimeAmend(optionsParam: PlainTimeParams['toLocaleString'][1]) { +function zonedDateTimeAmend(optionsParam: OptionsType) { let options = optionsParam; if (!hasTimeOptions(options) && !hasDateOptions(options)) { options = ObjectAssign({}, options, { @@ -445,7 +438,7 @@ function zonedDateTimeAmend(optionsParam: PlainTimeParams['toLocaleString'][1]) return options; } -function instantAmend(optionsParam: InstantParams['toLocaleString'][1]) { +function instantAmend(optionsParam: OptionsType) { let options = optionsParam; if (!hasTimeOptions(options) && !hasDateOptions(options)) { options = ObjectAssign({}, options, { @@ -460,11 +453,11 @@ function instantAmend(optionsParam: InstantParams['toLocaleString'][1]) { return options; } -function hasDateOptions(options: Parameters[1]) { +function hasDateOptions(options: OptionsType) { return 'year' in options || 'month' in options || 'day' in options || 'weekday' in options || 'dateStyle' in options; } -function hasTimeOptions(options: Parameters[1]) { +function hasTimeOptions(options: OptionsType) { return ( 'hour' in options || 'minute' in options || 'second' in options || 'timeStyle' in options || 'dayPeriod' in options ); @@ -509,7 +502,8 @@ type TypesWithToLocaleString = | Temporal.PlainTime | Temporal.PlainYearMonth | Temporal.PlainMonthDay - | Temporal.ZonedDateTime; + | Temporal.ZonedDateTime + | Temporal.Instant; function extractOverrides(temporalObj: Params['format'][0], main: DateTimeFormatImpl) { const DateTime = GetIntrinsic('%Temporal.PlainDateTime%'); From bfa01106673e7451327f41aeef13bec4af3327e4 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 15 Dec 2021 22:43:19 -0800 Subject: [PATCH 07/10] Support strictNullChecks in intrinsics.ts Previously, GetIntrinsic returned a type that could be `undefined`. That's fixed in this commit. Note that intrinsics.ts may be removed completely in #105; if that PR lands first then we'll remove this commit. --- lib/intrinsicclass.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/intrinsicclass.ts b/lib/intrinsicclass.ts index b9846f87..6734f164 100644 --- a/lib/intrinsicclass.ts +++ b/lib/intrinsicclass.ts @@ -36,9 +36,9 @@ interface StandaloneIntrinsics { 'Temporal.Calendar.from': typeof Temporal.Calendar.from; } type RegisteredStandaloneIntrinsics = { [key in keyof StandaloneIntrinsics as `%${key}%`]: StandaloneIntrinsics[key] }; -const INTRINSICS: Partial & - Partial & - Partial = {}; +const INTRINSICS = {} as TemporalIntrinsicRegisteredKeys & + TemporalIntrinsicPrototypeRegisteredKeys & + RegisteredStandaloneIntrinsics; type customFormatFunction = ( this: T, @@ -96,13 +96,13 @@ export function MakeIntrinsicClass( }); } for (const prop of Object.getOwnPropertyNames(Class)) { - const desc = Object.getOwnPropertyDescriptor(Class, prop); + const desc = Object.getOwnPropertyDescriptor(Class, prop) as PropertyDescriptor; if (!desc.configurable || !desc.enumerable) continue; desc.enumerable = false; Object.defineProperty(Class, prop, desc); } for (const prop of Object.getOwnPropertyNames(Class.prototype)) { - const desc = Object.getOwnPropertyDescriptor(Class.prototype, prop); + const desc = Object.getOwnPropertyDescriptor(Class.prototype, prop) as PropertyDescriptor; if (!desc.configurable || !desc.enumerable) continue; desc.enumerable = false; Object.defineProperty(Class.prototype, prop, desc); From 288550045de26ffd0aeb458b1d5f2378209cbfae Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 15 Dec 2021 22:45:47 -0800 Subject: [PATCH 08/10] strtictNullChecks in PT, TZ, and ZDT code This commit makes a handful of type changes in plaintime.ts, timezone.ts, and zoneddatetime.ts to handle parameters or variables that may be undefined or null. There's only a few changes. --- lib/plaintime.ts | 2 +- lib/timezone.ts | 4 ++-- lib/zoneddatetime.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/plaintime.ts b/lib/plaintime.ts index f9c041b2..40b109d7 100644 --- a/lib/plaintime.ts +++ b/lib/plaintime.ts @@ -43,7 +43,7 @@ type TemporalTimeToStringOptions = { function TemporalTimeToString( time: Temporal.PlainTime, precision: ReturnType['precision'], - options: TemporalTimeToStringOptions = undefined + options: TemporalTimeToStringOptions | undefined = undefined ) { let hour = GetSlot(time, ISO_HOUR); let minute = GetSlot(time, ISO_MINUTE); diff --git a/lib/timezone.ts b/lib/timezone.ts index 9da3c8b3..93d94443 100644 --- a/lib/timezone.ts +++ b/lib/timezone.ts @@ -125,7 +125,7 @@ export class TimeZone implements Temporal.TimeZone { return null; } - let epochNanoseconds = GetSlot(startingPoint, EPOCHNANOSECONDS); + let epochNanoseconds: JSBI | null = GetSlot(startingPoint, EPOCHNANOSECONDS); const Instant = GetIntrinsic('%Temporal.Instant%'); epochNanoseconds = ES.GetIANATimeZoneNextTransition(epochNanoseconds, id); return epochNanoseconds === null ? null : new Instant(epochNanoseconds); @@ -140,7 +140,7 @@ export class TimeZone implements Temporal.TimeZone { return null; } - let epochNanoseconds = GetSlot(startingPoint, EPOCHNANOSECONDS); + let epochNanoseconds: JSBI | null = GetSlot(startingPoint, EPOCHNANOSECONDS); const Instant = GetIntrinsic('%Temporal.Instant%'); epochNanoseconds = ES.GetIANATimeZonePreviousTransition(epochNanoseconds, id); return epochNanoseconds === null ? null : new Instant(epochNanoseconds); diff --git a/lib/zoneddatetime.ts b/lib/zoneddatetime.ts index 56c1af03..4f9f242c 100644 --- a/lib/zoneddatetime.ts +++ b/lib/zoneddatetime.ts @@ -225,9 +225,9 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { entries.push([fieldName, undefined]); } }); - let fields = ES.PrepareTemporalFields(this, entries as any); + let fields = ES.PrepareTemporalFields(this, entries); fields = ES.CalendarMergeFields(calendar, fields, props); - fields = ES.PrepareTemporalFields(fields, entries as any); + fields = ES.PrepareTemporalFields(fields, entries); const { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = ES.InterpretTemporalDateTimeFields(calendar, fields, options); const offsetNs = ES.ParseTimeZoneOffsetString(fields.offset); From d5a6c76534686ae36334e46b6bfacb3e2ba37803 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 15 Dec 2021 22:55:16 -0800 Subject: [PATCH 09/10] tsconfig changes Finally, enable strictNullChecks and strictPropertyInitialization in tsonfig.json. --- tsconfig.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index bee7c8d8..8a050968 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,8 +21,8 @@ "skipDefaultLibCheck": true, "strictBindCallApply": true, "strictFunctionTypes": true, - // "strictNullChecks": true, - // "strictPropertyInitialization": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, "stripInternal": true, "target": "es2020", "outDir": "tsc-out/", From 245b56850fd4212c0210a7367d5c32979089a10c Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Mon, 10 Jan 2022 23:53:32 -0800 Subject: [PATCH 10/10] Changes from code reviews --- lib/calendar.ts | 4 ++-- lib/ecmascript.ts | 29 ++++++++++++++++++++--------- lib/intrinsicclass.ts | 8 ++++++-- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/lib/calendar.ts b/lib/calendar.ts index 80b8b902..08376ae0 100644 --- a/lib/calendar.ts +++ b/lib/calendar.ts @@ -1459,9 +1459,9 @@ const helperHebrew: HelperPerCalendarImpl = ObjectAssign({}, helperSharedImpl as if (monthExtra) { const monthInfo = this.months[monthExtra]; if (!monthInfo) throw new RangeError(`Unrecognized month from formatToParts: ${monthExtra}`); - month = (this.inLeapYear({ year }) ? monthInfo.leap : monthInfo.regular) as number; + month = this.inLeapYear({ year }) ? monthInfo.leap : monthInfo.regular; } - // if we're getting data from legacy Date, then `month` will always be present + // Because we're getting data from legacy Date, then `month` will always be present monthCode = this.getMonthCode(year, month as number); const result = { year, month, day, era: undefined as string | undefined, eraYear, monthCode }; return result; diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index f391ef39..5c242dbf 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -4756,8 +4756,8 @@ export function RoundDuration( // First convert time units up to days, if rounding to days or higher units. // If rounding relative to a ZonedDateTime, then some days may not be 24h. // TS doesn't know that `dayLengthNs` is only used if the unit is day or - // larger. This makes the cast below acceptable. - let dayLengthNs: JSBI = undefined as unknown as JSBI; + // larger. We'll cast away `undefined` when it's used lower down below. + let dayLengthNs: JSBI | undefined; if (unit === 'year' || unit === 'month' || unit === 'week' || unit === 'day') { nanoseconds = TotalDurationNanoseconds(0, hours, minutes, seconds, milliseconds, microseconds, nanosecondsParam, 0); let intermediate; @@ -4823,9 +4823,12 @@ export function RoundDuration( // the duration. This lets us do days-or-larger rounding using BigInt // math which reduces precision loss. oneYearDays = MathAbs(oneYearDays); - const divisor = JSBI.multiply(JSBI.BigInt(oneYearDays), dayLengthNs); + // dayLengthNs is never undefined if unit is `day` or larger. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const divisor = JSBI.multiply(JSBI.BigInt(oneYearDays), dayLengthNs!); nanoseconds = JSBI.add( - JSBI.add(JSBI.multiply(divisor, JSBI.BigInt(years)), JSBI.multiply(JSBI.BigInt(days), dayLengthNs)), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + JSBI.add(JSBI.multiply(divisor, JSBI.BigInt(years)), JSBI.multiply(JSBI.BigInt(days), dayLengthNs!)), nanoseconds ); const rounded = RoundNumberToIncrement( @@ -4879,9 +4882,12 @@ export function RoundDuration( ({ relativeTo, days: oneMonthDays } = MoveRelativeDate(calendar, relativeTo, oneMonth)); } oneMonthDays = MathAbs(oneMonthDays); - const divisor = JSBI.multiply(JSBI.BigInt(oneMonthDays), dayLengthNs); + // dayLengthNs is never undefined if unit is `day` or larger. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const divisor = JSBI.multiply(JSBI.BigInt(oneMonthDays), dayLengthNs!); nanoseconds = JSBI.add( - JSBI.add(JSBI.multiply(divisor, JSBI.BigInt(months)), JSBI.multiply(JSBI.BigInt(days), dayLengthNs)), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + JSBI.add(JSBI.multiply(divisor, JSBI.BigInt(months)), JSBI.multiply(JSBI.BigInt(days), dayLengthNs!)), nanoseconds ); const rounded = RoundNumberToIncrement( @@ -4909,9 +4915,12 @@ export function RoundDuration( ({ relativeTo, days: oneWeekDays } = MoveRelativeDate(calendar, relativeTo, oneWeek)); } oneWeekDays = MathAbs(oneWeekDays); - const divisor = JSBI.multiply(JSBI.BigInt(oneWeekDays), dayLengthNs); + // dayLengthNs is never undefined if unit is `day` or larger. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const divisor = JSBI.multiply(JSBI.BigInt(oneWeekDays), dayLengthNs!); nanoseconds = JSBI.add( - JSBI.add(JSBI.multiply(divisor, JSBI.BigInt(weeks)), JSBI.multiply(JSBI.BigInt(days), dayLengthNs)), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + JSBI.add(JSBI.multiply(divisor, JSBI.BigInt(weeks)), JSBI.multiply(JSBI.BigInt(days), dayLengthNs!)), nanoseconds ); const rounded = RoundNumberToIncrement( @@ -4926,7 +4935,9 @@ export function RoundDuration( break; } case 'day': { - const divisor = dayLengthNs; + // dayLengthNs is never undefined if unit is `day` or larger. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const divisor = dayLengthNs!; nanoseconds = JSBI.add(JSBI.multiply(divisor, JSBI.BigInt(days)), nanoseconds); const rounded = RoundNumberToIncrement( nanoseconds, diff --git a/lib/intrinsicclass.ts b/lib/intrinsicclass.ts index 6734f164..9c374834 100644 --- a/lib/intrinsicclass.ts +++ b/lib/intrinsicclass.ts @@ -96,13 +96,17 @@ export function MakeIntrinsicClass( }); } for (const prop of Object.getOwnPropertyNames(Class)) { - const desc = Object.getOwnPropertyDescriptor(Class, prop) as PropertyDescriptor; + // we know that `prop` is present, so the descriptor is never undefined + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const desc = Object.getOwnPropertyDescriptor(Class, prop)!; if (!desc.configurable || !desc.enumerable) continue; desc.enumerable = false; Object.defineProperty(Class, prop, desc); } for (const prop of Object.getOwnPropertyNames(Class.prototype)) { - const desc = Object.getOwnPropertyDescriptor(Class.prototype, prop) as PropertyDescriptor; + // we know that `prop` is present, so the descriptor is never undefined + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const desc = Object.getOwnPropertyDescriptor(Class.prototype, prop)!; if (!desc.configurable || !desc.enumerable) continue; desc.enumerable = false; Object.defineProperty(Class.prototype, prop, desc);