diff --git a/lib/calendar.ts b/lib/calendar.ts index 44ec5a08..08376ae0 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( @@ -78,7 +76,7 @@ interface CalendarImpl { months: number, weeks: number, days: number, - overflow: Temporal.ArithmeticOptions['overflow'], + overflow: Overflow, calendar: Temporal.Calendar ): Temporal.PlainDate; dateUntil( @@ -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); }, @@ -455,9 +447,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')) { @@ -477,9 +673,9 @@ 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: Temporal.ArithmeticOptions['overflow'] = undefined, + overflow: Overflow = undefined, monthsPerYear = 12 ) { let { month, monthCode } = calendarDate; @@ -505,12 +701,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; /** @@ -527,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; @@ -584,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); @@ -600,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, @@ -608,75 +789,12 @@ function simpleDateDiff(one: Required, two: Required }; } -interface NonIsoHelperBase { - 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 nonIsoHelperBase: NonIsoHelperBase = { +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 @@ -694,7 +812,7 @@ const nonIsoHelperBase: NonIsoHelperBase = { } 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); @@ -708,11 +826,11 @@ const nonIsoHelperBase: NonIsoHelperBase = { } 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}`); @@ -756,7 +874,7 @@ const nonIsoHelperBase: NonIsoHelperBase = { ); } // 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; @@ -783,8 +901,11 @@ const nonIsoHelperBase: NonIsoHelperBase = { }); 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'); @@ -817,7 +938,7 @@ const nonIsoHelperBase: NonIsoHelperBase = { * - 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); @@ -834,13 +955,13 @@ const nonIsoHelperBase: NonIsoHelperBase = { }; } - 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') { @@ -852,8 +973,8 @@ const nonIsoHelperBase: NonIsoHelperBase = { } 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); @@ -1006,8 +1127,8 @@ const nonIsoHelperBase: NonIsoHelperBase = { 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); @@ -1028,7 +1149,7 @@ const nonIsoHelperBase: NonIsoHelperBase = { 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++) { @@ -1102,7 +1223,7 @@ const nonIsoHelperBase: NonIsoHelperBase = { // 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; @@ -1120,7 +1241,7 @@ const nonIsoHelperBase: NonIsoHelperBase = { } 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 @@ -1146,7 +1267,7 @@ const nonIsoHelperBase: NonIsoHelperBase = { 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 @@ -1191,7 +1312,7 @@ const nonIsoHelperBase: NonIsoHelperBase = { 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)) { @@ -1232,10 +1353,31 @@ const nonIsoHelperBase: NonIsoHelperBase = { } }; -const helperHebrew: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { +interface HebrewMonthInfo { + [m: string]: ( + | { + leap: undefined; + regular: number; + } + | { + leap: number; + regular: undefined; + } + ) & { + monthCode: string; + days: + | number + | { + min: number; + max: number; + }; + }; +} + +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. @@ -1244,33 +1386,25 @@ const helperHebrew: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { // 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 }; }, @@ -1290,7 +1424,7 @@ const helperHebrew: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { 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 { @@ -1298,13 +1432,20 @@ const helperHebrew: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { } }, adjustCalendarDate( - this: typeof helperHebrew & NonIsoHelperBase, - 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) { @@ -1320,7 +1461,8 @@ const helperHebrew: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { if (!monthInfo) throw new RangeError(`Unrecognized month from formatToParts: ${monthExtra}`); month = this.inLeapYear({ year }) ? monthInfo.leap : monthInfo.regular; } - monthCode = this.getMonthCode(year, month); + // 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; } else { @@ -1328,7 +1470,7 @@ const helperHebrew: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { // 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}`); } @@ -1344,7 +1486,7 @@ const helperHebrew: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { } } } 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 }); @@ -1372,16 +1514,16 @@ const helperHebrew: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { }, // 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: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { +const helperIslamic: HelperPerCalendarImpl = ObjectAssign({}, helperSharedImpl, { id: 'islamic', calendarType: 'lunar', - inLeapYear(this: typeof helperIslamic & NonIsoHelperBase, 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; @@ -1394,16 +1536,19 @@ 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: 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: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { +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); @@ -1411,27 +1556,41 @@ const helperPersian: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { 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 & NonIsoHelperBase, calendarDate) { + estimateIsoDate(this: HelperPerCalendarImpl, calendarDate: CalendarYMD) { const { year } = this.adjustCalendarDate(calendarDate); return { year: year + 621, month: 1, day: 1 }; } -} as Partial); +}); -const helperIndian: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { +interface IndianMonthInfo { + [month: number]: { + length: number; + month: number; + day: number; + leap?: { + length: number; + month: number; + day: number; + }; + nextYear?: true | undefined; + }; +} + +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 @@ -1442,10 +1601,16 @@ const helperIndian: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { 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', @@ -1466,14 +1631,17 @@ const helperIndian: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { 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 & NonIsoHelperBase, 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); @@ -1490,7 +1658,7 @@ const helperIndian: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { // expected. vulnerableToBceBug: new Date('0000-01-01T00:00Z').toLocaleDateString('en-US-u-ca-indian', { timeZone: 'UTC' }) !== '10/11/-79 Saka', - checkIcuBugs(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` + @@ -1498,7 +1666,7 @@ const helperIndian: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { ); } } -} as Partial); +}); /** * Era metadata defined for each calendar. @@ -1517,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 }; @@ -1551,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; @@ -1564,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 @@ -1614,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'); @@ -1686,29 +1854,32 @@ 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 = ObjectAssign({}, helperSharedImpl, { id, eras, anchorEra, calendarType: 'solar', - inLeapYear(calendarDate /*, cache */) { - const { year } = this.estimateIsoDate(calendarDate); + 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. + const { year } = this.estimateIsoDate({ month: 1, day: 1, year: calendarDate.year }); return isGregorianLeapYear(year); }, 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}`); @@ -1731,7 +1902,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; @@ -1771,25 +1942,26 @@ const makeHelperGregorian = (id: BuiltinCalendarId, originalEras: InputEra[]) => return { ...calendarDate, year, eraYear, era }; }, adjustCalendarDate( - this: typeof helperGregorian & NonIsoHelperBase, - 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(nonIsoHelperBase.adjustCalendarDate, this, [calendarDate, cache, overflow]); + calendarDate = ReflectApply(helperSharedImpl.adjustCalendarDate, this, [calendarDate, cache, overflow]); return calendarDate; }, - estimateIsoDate(this: typeof helperGregorian & NonIsoHelperBase, 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'); }, @@ -1800,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) { @@ -1811,14 +1983,14 @@ const makeHelperGregorian = (id: BuiltinCalendarId, originalEras: InputEra[]) => } } } - } as Partial); + }); return helperGregorian; }; const makeHelperOrthodox = (id: BuiltinCalendarId, originalEras: InputEra[]) => { - const base: NonIsoHelperBase = 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 @@ -1833,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, @@ -1883,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: NonIsoHelperBase = 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 @@ -1946,32 +2118,43 @@ const helperJapanese: NonIsoHelperBase = 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 + } ); -const helperChinese: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { +interface ChineseMonthInfo { + [key: string]: { monthIndex: number; daysInMonth: number }; +} +interface ChineseDraftMonthInfo { + [key: string]: { monthIndex: number; daysInMonth?: number }; +} + +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: NonIsoHelperBase, - calendarYear, - cache - ): { [key: string]: { monthIndex: string; daysInMonth: number } } { + getMonthList(this: HelperPerCalendarImpl, calendarYear: number, cache: OneObjectCache): ChineseMonthInfo { if (calendarYear === undefined) { throw new TypeError('Missing year'); } @@ -1985,8 +2168,8 @@ const helperChinese: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { // 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' ); @@ -2017,15 +2200,15 @@ const helperChinese: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { // 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; @@ -2042,19 +2225,19 @@ const helperChinese: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { 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 & NonIsoHelperBase, - 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 @@ -2062,13 +2245,13 @@ const helperChinese: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { // 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. @@ -2076,19 +2259,19 @@ const helperChinese: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { 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) { @@ -2100,17 +2283,17 @@ const helperChinese: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { 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}`); } @@ -2120,7 +2303,7 @@ const helperChinese: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { ); } 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]; @@ -2129,82 +2312,31 @@ const helperChinese: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, { 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' }); -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 @@ -2221,7 +2353,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 @@ -2238,9 +2370,9 @@ const nonIsoGeneralImpl: NonIsoGeneralImpl = { return result; }, monthDayFromFields( - this: typeof nonIsoGeneralImpl & { helper: NonIsoHelperBase }, + this: NonIsoImplWithHelper, fieldsParam: Params['monthDayFromFields'][0], - options: Params['monthDayFromFields'][1], + options: NonNullable, calendar: Temporal.CalendarProtocol ) { const overflow = ES.ToTemporalOverflow(options); @@ -2297,13 +2429,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); @@ -2318,7 +2450,7 @@ const nonIsoGeneralImpl: NonIsoGeneralImpl = { return newTemporalObject; }, dateUntil( - this: typeof nonIsoGeneralImpl & { helper: NonIsoHelperBase }, + this: NonIsoImplWithHelper, one: Temporal.PlainDate, two: Temporal.PlainDate, largestUnit: Temporal.DateUnit @@ -2330,34 +2462,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; @@ -2365,7 +2497,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); @@ -2378,7 +2510,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) { const cache = OneObjectCache.getCacheForObject(date); const calendarDate = this.helper.temporalToCalendarDate(date, cache); @@ -2395,7 +2527,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); @@ -2405,13 +2537,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); @@ -2421,22 +2553,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[]; diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index ba0bf001..5c242dbf 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. 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; @@ -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; @@ -4787,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( @@ -4811,10 +4850,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; @@ -4831,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( @@ -4854,16 +4908,19 @@ 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; ({ 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( @@ -4878,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, @@ -5073,7 +5132,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 +5144,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]; 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%'); diff --git a/lib/intrinsicclass.ts b/lib/intrinsicclass.ts index b9846f87..9c374834 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,17 @@ export function MakeIntrinsicClass( }); } for (const prop of Object.getOwnPropertyNames(Class)) { - const desc = Object.getOwnPropertyDescriptor(Class, prop); + // 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); + // 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); 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); 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/",