Skip to content

Compile with strictNullChecks/strictPropertyInitialization (focus on calendar.ts and ecmascript.ts) #109

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jan 11, 2022
250 changes: 248 additions & 2 deletions lib/calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,9 +455,213 @@ impl['iso8601'] = {
}
};

// Note: other built-in calendars than iso8601 are not part of the Temporal
// Note: Built-in calendars other than iso8601 are not part of the Temporal
// proposal for ECMA-262. These calendars will be standardized as part of
// ECMA-402.
// ECMA-402. Code below here includes an implementation of these calendars order
// to validate the Temporal API and to get feedback. However, non-ISO calendar
// implementation is subject to change because these calendars are
// implementation-defined.
//
// Some ES implementations don't include ECMA 402. For this reason, it's helpful
// to ensure a clean separation between the ISO calendar implementation which is
// a part of ECMA 262 and the non-ISO calendar implementation which requires
// ECMA 402.
//
// To ensure this separation, the implementation is split. The `NonIsoImpl`
// interface is the top-level implementation for all non-ISO calendars. This
// type has the same shape as the ECMA 262-only ISO calendar implementation so
// can use the same callers, tests, etc.
//
// A derived interface `NonIsoImplWithHelper` adds a `helper` property that
// includes the remaining non-ISO implementation properties and methods beyond
// the ISO implementation above. The `helper` property's shape is a base
// singleton object common to all calendars (`HelperSharedImpl`) that's extended
// (interface `HelperPerCalendarImpl`) with implementation that varies for each
// calendar.
//
// Typing of individual methods in the interfaces below uses the `this`
// "parameter" declaration definition, which is a fake parameter (stripped by TS
// during compilation and not visible at runtime) that tells TS what type `this`
// is for a method. For historical reasons, the initial implementation of
// non-ISO calendars mirrored the code style of the previous ISO-only
// implementation which didn't use ES6 classes. Using the `this` parameter is a
// hack to delay converting this file to use ES6 classes until the code was
// fully typed to make a `class` refactoring easier and safer. We'll probably do
// this conversion in the future. (PRs welcome!)

/**
* `NonIsoImpl` - The generic top-level implementation for all non-ISO
* calendars. This type has the same shape as the 262-only ISO calendar
* implementation, which means the `Calendar` class implementation can swap out
* the ISO for non-ISO implementations without changing any `Calendar` code.
*/
interface NonIsoImpl {
dateFromFields(
this: NonIsoImplWithHelper,
fieldsParam: Params['dateFromFields'][0],
options: NonNullable<Params['dateFromFields'][1]>,
calendar: Temporal.Calendar
): Temporal.PlainDate;
yearMonthFromFields(
this: NonIsoImplWithHelper,
fieldsParam: Params['yearMonthFromFields'][0],
options: NonNullable<Params['yearMonthFromFields'][1]>,
calendar: Temporal.Calendar
): Temporal.PlainYearMonth;
monthDayFromFields(
this: NonIsoImplWithHelper,
fieldsParam: Params['monthDayFromFields'][0],
options: NonNullable<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: 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<FullCalendarDate>): void;
adjustCalendarDate(
calendarDate: Partial<FullCalendarDate>,
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<CalendarYMD>, date2: Partial<CalendarYMD>): 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<FullCalendarDate>, overflow: Overflow, cache: OneObjectCache): IsoYMD;
}

/** Calendar-specific implementation */
interface HelperPerCalendarImpl extends HelperSharedImpl {
id: string;
reviseIntlEra?<T extends Partial<EraAndEraYear>>(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')) {
Expand Down Expand Up @@ -1232,6 +1436,27 @@ const nonIsoHelperBase: NonIsoHelperBase = {
}
};

interface HebrewMonthInfo {
[m: string]: (
| {
leap: undefined;
regular: number;
}
| {
leap: number;
regular: undefined;
}
) & {
monthCode: string;
days:
| number
| {
min: number;
max: number;
};
};
}

const helperHebrew: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, {
id: 'hebrew',
calendarType: 'lunisolar',
Expand Down Expand Up @@ -1428,6 +1653,20 @@ const helperPersian: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, {
}
} as Partial<NonIsoHelperBase>);

interface IndianMonthInfo {
[month: number]: {
length: number;
month: number;
day: number;
leap?: {
length: number;
month: number;
day: number;
};
nextYear?: true | undefined;
};
}

const helperIndian: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, {
id: 'indian',
calendarType: 'solar',
Expand Down Expand Up @@ -1958,6 +2197,13 @@ const helperJapanese: NonIsoHelperBase = ObjectAssign(
} as Partial<NonIsoHelperBase>
);

interface ChineseMonthInfo {
[key: string]: { monthIndex: number; daysInMonth: number };
}
interface ChineseDraftMonthInfo {
[key: string]: { monthIndex: number; daysInMonth?: number };
}

const helperChinese: NonIsoHelperBase = ObjectAssign({}, nonIsoHelperBase, {
id: 'chinese',
calendarType: 'lunisolar',
Expand Down