mirror of
https://github.com/bendtherules/ask262.git
synced 2026-08-18 13:21:55 +00:00
725 lines
28 KiB
TypeScript
725 lines
28 KiB
TypeScript
import { CanonicalizeUValue } from '../../ecma402/not-implemented.mts';
|
|
import { __ts_cast__, isArray, type Mutable } from '../../helpers.mts';
|
|
import { ParseMonthCode, ParseTemporalCalendarString } from '../../parser/TemporalParser.mts';
|
|
import { isTemporalPlainDateTimeObject } from '../../intrinsics/Temporal/PlainDateTime.mts';
|
|
import { isTemporalPlainMonthDayObject } from '../../intrinsics/Temporal/PlainMonthDay.mts';
|
|
import { isTemporalZonedDateTimeObject } from '../../intrinsics/Temporal/ZonedDateTime.mts';
|
|
import { isTemporalPlainDateObject, type ISODateRecord } from '../../intrinsics/Temporal/PlainDate.mts';
|
|
import { isTemporalPlainYearMonthObject } from '../../intrinsics/Temporal/PlainYearMonth.mts';
|
|
import { ToZeroPaddedDecimalString } from './addition.mts';
|
|
import type { YearWeekRecord } from './addition.mts';
|
|
import {
|
|
EpochDaysToEpochMs,
|
|
EpochTimeForYear,
|
|
EpochTimeToDayInYear,
|
|
EpochTimeToWeekDay,
|
|
ISODateToEpochDays,
|
|
MathematicalDaysInYear,
|
|
MathematicalInLeapYear,
|
|
TemporalUnit,
|
|
ToIntegerWithTruncation, ToOffsetString, ToPositiveIntegerWithTruncation, type DateUnit,
|
|
} from './temporal.mts';
|
|
import { ToTemporalTimeZoneIdentifier } from './time-zone.mts';
|
|
import { mark_OtherCalendarNotImplemented, unreachable_OtherCalendarNotImplemented } from './not-implemented.mts';
|
|
import {
|
|
AddDaysToISODate,
|
|
Assert,
|
|
BalanceISOYearMonth,
|
|
CompareISODate,
|
|
CreateDateDurationRecord,
|
|
CreateISODateRecord,
|
|
F,
|
|
Get,
|
|
ISODateSurpasses,
|
|
ISODateWithinLimits,
|
|
JSStringValue,
|
|
NumberValue,
|
|
ObjectValue,
|
|
Q,
|
|
R,
|
|
RegulateISODate,
|
|
Throw,
|
|
ToString,
|
|
Value,
|
|
X,
|
|
ZeroDateDuration,
|
|
type DateDurationRecord,
|
|
type PlainCompletion, type PlainEvaluator,
|
|
} from '#self';
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-calendar-types */
|
|
export type CalendarType = 'iso8601';
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-canonicalizecalendar */
|
|
export function CanonicalizeCalendar(id: string): PlainCompletion<CalendarType> {
|
|
const calendars = AvailableCalendars();
|
|
if (!calendars.includes(id.toLowerCase() as CalendarType)) {
|
|
return Throw.RangeError('$1 is not a supported calendar', id);
|
|
}
|
|
return CanonicalizeUValue('ca', id) as CalendarType;
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-availablecalendars */
|
|
export function AvailableCalendars(): CalendarType[] {
|
|
mark_OtherCalendarNotImplemented();
|
|
return ['iso8601'];
|
|
}
|
|
|
|
export type MonthCode = string & { __brand: 'MonthCode' };
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-createmonthcode */
|
|
export function CreateMonthCode(monthNumber: number, isLeapMonth: boolean): MonthCode {
|
|
if (!isLeapMonth) Assert(monthNumber > 0);
|
|
const numberPart = ToZeroPaddedDecimalString(monthNumber, 2);
|
|
if (isLeapMonth) {
|
|
return `M${numberPart}L` as MonthCode;
|
|
}
|
|
return `M${numberPart}` as MonthCode;
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-calendar-date-records */
|
|
export interface CalendarDateRecord {
|
|
readonly Era: string | undefined;
|
|
readonly EraYear: number | undefined;
|
|
readonly Year: number;
|
|
readonly Month: number;
|
|
readonly MonthCode: string;
|
|
readonly Day: number;
|
|
readonly DayOfWeek: number;
|
|
readonly DayOfYear: number;
|
|
readonly WeekOfYear: YearWeekRecord;
|
|
readonly DaysInWeek: number;
|
|
readonly DaysInMonth: number;
|
|
readonly DaysInYear: number;
|
|
readonly MonthsInYear: number;
|
|
readonly InLeapYear: boolean;
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#table-temporal-calendar-fields-record-fields */
|
|
export interface CalendarFieldsRecord {
|
|
readonly Era: string | undefined;
|
|
readonly EraYear: number | undefined;
|
|
Year: number | undefined;
|
|
Month: number | undefined;
|
|
MonthCode: string | undefined;
|
|
Day: number | undefined;
|
|
Hour: number | undefined;
|
|
Minute: number | undefined;
|
|
Second: number | undefined;
|
|
Millisecond: number | undefined;
|
|
Microsecond: number | undefined;
|
|
Nanosecond: number | undefined;
|
|
OffsetString: string | undefined;
|
|
readonly TimeZone: string | undefined;
|
|
}
|
|
|
|
export enum Table19_Conversion {
|
|
ToString = 'to-string',
|
|
ToIntegerWithTruncation = 'to-integer-with-truncation',
|
|
ToPositiveIntegerWithTruncation = 'to-positive-integer-with-truncation',
|
|
ToTemporalTimeZoneIdentifier = 'to-temporal-time-zone-identifier',
|
|
ToMonthCode = 'to-month-code',
|
|
ToOffsetString = 'to-offset-string',
|
|
}
|
|
|
|
export type CalendarFieldsRecordEnumerationKey = 'era' | 'era-year' | 'year' | 'month' | 'month-code' | 'day' | 'hour' | 'minute' | 'second' | 'millisecond' | 'microsecond' | 'nanosecond' | 'offset' | 'time-zone';
|
|
|
|
export const Table19_CalendarFieldsRecordFields = [
|
|
/* eslint-disable object-curly-newline */
|
|
{ FieldName: 'Era', DefaultValue: undefined, PropertyKey: 'era', EnumerationKey: 'era', Conversion: Table19_Conversion.ToString },
|
|
{ FieldName: 'EraYear', DefaultValue: undefined, PropertyKey: 'eraYear', EnumerationKey: 'era-year', Conversion: Table19_Conversion.ToIntegerWithTruncation },
|
|
{ FieldName: 'Year', DefaultValue: undefined, PropertyKey: 'year', EnumerationKey: 'year', Conversion: Table19_Conversion.ToIntegerWithTruncation },
|
|
{ FieldName: 'Month', DefaultValue: undefined, PropertyKey: 'month', EnumerationKey: 'month', Conversion: Table19_Conversion.ToPositiveIntegerWithTruncation },
|
|
{ FieldName: 'MonthCode', DefaultValue: undefined, PropertyKey: 'monthCode', EnumerationKey: 'month-code', Conversion: Table19_Conversion.ToMonthCode },
|
|
{ FieldName: 'Day', DefaultValue: undefined, PropertyKey: 'day', EnumerationKey: 'day', Conversion: Table19_Conversion.ToPositiveIntegerWithTruncation },
|
|
{ FieldName: 'Hour', DefaultValue: 0, PropertyKey: 'hour', EnumerationKey: 'hour', Conversion: Table19_Conversion.ToIntegerWithTruncation },
|
|
{ FieldName: 'Minute', DefaultValue: 0, PropertyKey: 'minute', EnumerationKey: 'minute', Conversion: Table19_Conversion.ToIntegerWithTruncation },
|
|
{ FieldName: 'Second', DefaultValue: 0, PropertyKey: 'second', EnumerationKey: 'second', Conversion: Table19_Conversion.ToIntegerWithTruncation },
|
|
{ FieldName: 'Millisecond', DefaultValue: 0, PropertyKey: 'millisecond', EnumerationKey: 'millisecond', Conversion: Table19_Conversion.ToIntegerWithTruncation },
|
|
{ FieldName: 'Microsecond', DefaultValue: 0, PropertyKey: 'microsecond', EnumerationKey: 'microsecond', Conversion: Table19_Conversion.ToIntegerWithTruncation },
|
|
{ FieldName: 'Nanosecond', DefaultValue: 0, PropertyKey: 'nanosecond', EnumerationKey: 'nanosecond', Conversion: Table19_Conversion.ToIntegerWithTruncation },
|
|
{ FieldName: 'OffsetString', DefaultValue: undefined, PropertyKey: 'offsetString', EnumerationKey: 'offset', Conversion: Table19_Conversion.ToOffsetString },
|
|
{ FieldName: 'TimeZone', DefaultValue: undefined, PropertyKey: 'timeZone', EnumerationKey: 'time-zone', Conversion: Table19_Conversion.ToTemporalTimeZoneIdentifier },
|
|
/* eslint-enable object-curly-newline */
|
|
] as const satisfies {
|
|
FieldName: keyof CalendarFieldsRecord;
|
|
DefaultValue: string | number | undefined;
|
|
PropertyKey: string;
|
|
EnumerationKey: CalendarFieldsRecordEnumerationKey;
|
|
Conversion: Table19_Conversion;
|
|
}[];
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-preparecalendarfields */
|
|
export function* PrepareCalendarFields(
|
|
calendar: CalendarType,
|
|
fields: ObjectValue,
|
|
calendarFieldNames: readonly CalendarFieldsRecordEnumerationKey[],
|
|
nonCalendarFieldNames: readonly CalendarFieldsRecordEnumerationKey[],
|
|
requiredFieldNames: 'partial' | readonly CalendarFieldsRecordEnumerationKey[],
|
|
): PlainEvaluator<CalendarFieldsRecord> {
|
|
// Assert: If requiredFieldNames is a List, requiredFieldNames contains zero or one of each of the elements of calendarFieldNames and nonCalendarFieldNames.
|
|
if (isArray(requiredFieldNames)) {
|
|
Assert(calendarFieldNames.every((name) => requiredFieldNames.filter((requiredName) => name === requiredName).length <= 1));
|
|
Assert(nonCalendarFieldNames.every((name) => requiredFieldNames.filter((requiredName) => name === requiredName).length <= 1));
|
|
}
|
|
let fieldNames: CalendarFieldsRecordEnumerationKey[] = [...calendarFieldNames, ...nonCalendarFieldNames];
|
|
const extraFieldNames = CalendarExtraFields(calendar, calendarFieldNames);
|
|
fieldNames = [...fieldNames, ...extraFieldNames];
|
|
// Assert: fieldNames contains no duplicate elements.
|
|
Assert(fieldNames.length === new Set(fieldNames).size);
|
|
const result: Mutable<CalendarFieldsRecord> = {
|
|
Era: undefined,
|
|
EraYear: undefined,
|
|
Year: undefined,
|
|
Month: undefined,
|
|
MonthCode: undefined,
|
|
Day: undefined,
|
|
Hour: undefined,
|
|
Minute: undefined,
|
|
Second: undefined,
|
|
Millisecond: undefined,
|
|
Microsecond: undefined,
|
|
Nanosecond: undefined,
|
|
OffsetString: undefined,
|
|
TimeZone: undefined,
|
|
};
|
|
let any = false;
|
|
|
|
// Let sortedPropertyNames be a List whose elements are the values in the Property Key column of Table 19 corresponding to the elements of fieldNames, sorted according to lexicographic code unit order.
|
|
const sortedPropertyNames = [...Table19_CalendarFieldsRecordFields].sort((a, b) => (a.PropertyKey < b.PropertyKey ? -1 : 1));
|
|
|
|
for (const {
|
|
FieldName, PropertyKey, Conversion, DefaultValue, EnumerationKey,
|
|
} of sortedPropertyNames) {
|
|
__ts_cast__<keyof CalendarFieldsRecord>(FieldName);
|
|
// Let key be the value in the Enumeration Key column of Table 19 corresponding to the row whose Property Key value is property.
|
|
const key = EnumerationKey;
|
|
let value = Q(yield* Get(fields, Value(PropertyKey)));
|
|
|
|
if (value !== Value.undefined) {
|
|
any = true;
|
|
|
|
if (Conversion === Table19_Conversion.ToIntegerWithTruncation) {
|
|
value = F(Q(yield* ToIntegerWithTruncation(value)));
|
|
} else if (Conversion === Table19_Conversion.ToPositiveIntegerWithTruncation) {
|
|
value = F(Q(yield* ToPositiveIntegerWithTruncation(value)));
|
|
} else if (Conversion === Table19_Conversion.ToString) {
|
|
value = Q(yield* ToString(value));
|
|
} else if (Conversion === Table19_Conversion.ToTemporalTimeZoneIdentifier) {
|
|
value = Value(Q(ToTemporalTimeZoneIdentifier(value)));
|
|
} else if (Conversion === Table19_Conversion.ToMonthCode) {
|
|
const parsed = Q(yield* ParseMonthCode(value));
|
|
value = Value(CreateMonthCode(parsed.MonthNumber, parsed.IsLeapMonth));
|
|
} else {
|
|
Assert(Conversion === Table19_Conversion.ToOffsetString);
|
|
value = Value(Q(yield* ToOffsetString(value)));
|
|
}
|
|
|
|
let assignValue;
|
|
if (value instanceof NumberValue) {
|
|
assignValue = R(value);
|
|
} else if (value instanceof JSStringValue) {
|
|
assignValue = value.stringValue();
|
|
}
|
|
if (assignValue === undefined) {
|
|
throw new Error('invalid type');
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
result[FieldName] = assignValue as any;
|
|
} else if (isArray(requiredFieldNames)) {
|
|
if (requiredFieldNames.includes(key)) {
|
|
return Throw.TypeError('$1 is a required on object $2', key, fields);
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
result[FieldName] = DefaultValue as any;
|
|
}
|
|
}
|
|
|
|
if (requiredFieldNames === 'partial' && !any) {
|
|
return Throw.TypeError('$1 is not a TemporalTimeLike object', fields);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-calendarfieldkeyspresent */
|
|
export function CalendarFieldKeysPresent(fields: CalendarFieldsRecord): CalendarFieldsRecordEnumerationKey[] {
|
|
const list: CalendarFieldsRecordEnumerationKey[] = [];
|
|
for (const { FieldName, EnumerationKey } of Table19_CalendarFieldsRecordFields) {
|
|
const value = fields[FieldName];
|
|
const enumerationKey = EnumerationKey;
|
|
if (value !== undefined) {
|
|
list.push(enumerationKey);
|
|
}
|
|
}
|
|
return list;
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-calendarmergefields */
|
|
export function CalendarMergeFields(calendar: CalendarType, fields: CalendarFieldsRecord, additionalFields: CalendarFieldsRecord): CalendarFieldsRecord {
|
|
const additionalKeys = CalendarFieldKeysPresent(additionalFields);
|
|
const overriddenKeys = CalendarFieldKeysToIgnore(calendar, additionalKeys);
|
|
const merged: Mutable<CalendarFieldsRecord> = {
|
|
Era: undefined,
|
|
EraYear: undefined,
|
|
Year: undefined,
|
|
Month: undefined,
|
|
MonthCode: undefined,
|
|
Day: undefined,
|
|
Hour: undefined,
|
|
Minute: undefined,
|
|
Second: undefined,
|
|
Millisecond: undefined,
|
|
Microsecond: undefined,
|
|
Nanosecond: undefined,
|
|
OffsetString: undefined,
|
|
TimeZone: undefined,
|
|
};
|
|
const fieldsKeys = CalendarFieldKeysPresent(fields);
|
|
for (const { EnumerationKey, FieldName } of Table19_CalendarFieldsRecordFields) {
|
|
const key = EnumerationKey;
|
|
if (fieldsKeys.includes(key) && !overriddenKeys.includes(key)) {
|
|
const propValue = fields[FieldName];
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
merged[FieldName] = propValue as any;
|
|
}
|
|
if (additionalKeys.includes(key)) {
|
|
const propValue = additionalFields[FieldName];
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
merged[FieldName] = propValue as any;
|
|
}
|
|
}
|
|
return merged;
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-nonisodateadd */
|
|
export function NonISODateAdd(
|
|
_calendar: CalendarType,
|
|
_isoDate: ISODateRecord,
|
|
_duration: DateDurationRecord,
|
|
_overflow: 'constrain' | 'reject',
|
|
): never {
|
|
mark_OtherCalendarNotImplemented();
|
|
unreachable_OtherCalendarNotImplemented();
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-calendardateadd */
|
|
export function CalendarDateAdd(
|
|
calendar: CalendarType,
|
|
isoDate: ISODateRecord,
|
|
duration: DateDurationRecord,
|
|
overflow: 'constrain' | 'reject',
|
|
): PlainCompletion<ISODateRecord> {
|
|
let result: ISODateRecord;
|
|
if (calendar === 'iso8601') {
|
|
const intermediate = Q(BalanceISOYearMonth(isoDate.Year + duration.Years, isoDate.Month + duration.Months));
|
|
const regulated = Q(RegulateISODate(intermediate.Year, intermediate.Month, isoDate.Day, overflow));
|
|
const days = regulated.Day + duration.Days + 7 * duration.Weeks;
|
|
result = Q(AddDaysToISODate(regulated, days));
|
|
} else {
|
|
result = Q(NonISODateAdd(calendar, isoDate, duration, overflow));
|
|
}
|
|
if (!ISODateWithinLimits(result)) {
|
|
return Throw.RangeError('Resulting ISODate is out of range');
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-nonisodateuntil */
|
|
export function NonISODateUntil(
|
|
_calendar: CalendarType,
|
|
_one: ISODateRecord,
|
|
_two: ISODateRecord,
|
|
_largestUnit: DateUnit,
|
|
): never {
|
|
mark_OtherCalendarNotImplemented();
|
|
unreachable_OtherCalendarNotImplemented();
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-calendardateuntil */
|
|
export function CalendarDateUntil(
|
|
calendar: CalendarType,
|
|
one: ISODateRecord,
|
|
two: ISODateRecord,
|
|
largestUnit: DateUnit,
|
|
): DateDurationRecord {
|
|
if (calendar === 'iso8601') {
|
|
const sign = -CompareISODate(one, two) as 1 | -1 | 0;
|
|
if (sign === 0) {
|
|
return ZeroDateDuration();
|
|
}
|
|
let years = 0;
|
|
if (largestUnit === TemporalUnit.Year) {
|
|
let candidateYears = sign;
|
|
while (!ISODateSurpasses(sign, one, two, candidateYears, 0, 0, 0)) {
|
|
years = candidateYears;
|
|
candidateYears += sign;
|
|
}
|
|
}
|
|
let months = 0;
|
|
if (largestUnit === TemporalUnit.Month) {
|
|
let candidateMonths = sign;
|
|
while (!ISODateSurpasses(sign, one, two, years, candidateMonths, 0, 0)) {
|
|
months = candidateMonths;
|
|
candidateMonths += sign;
|
|
}
|
|
}
|
|
let weeks = 0;
|
|
if (largestUnit === TemporalUnit.Week) {
|
|
let candidateWeeks = sign;
|
|
while (!ISODateSurpasses(sign, one, two, years, months, candidateWeeks, 0)) {
|
|
weeks = candidateWeeks;
|
|
candidateWeeks += sign;
|
|
}
|
|
}
|
|
let days = 0;
|
|
let candidateDays = sign;
|
|
while (!ISODateSurpasses(sign, one, two, years, months, weeks, candidateDays)) {
|
|
days = candidateDays;
|
|
candidateDays += sign;
|
|
}
|
|
return X(CreateDateDurationRecord(years, months, weeks, days));
|
|
}
|
|
return NonISODateUntil(calendar, one, two, largestUnit);
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-totemporalcalendaridentifier */
|
|
export function ToTemporalCalendarIdentifier(temporalCalendarLike: Value): PlainCompletion<CalendarType> {
|
|
if (temporalCalendarLike instanceof ObjectValue) {
|
|
if (
|
|
isTemporalPlainDateObject(temporalCalendarLike)
|
|
|| isTemporalPlainDateTimeObject(temporalCalendarLike)
|
|
|| isTemporalPlainMonthDayObject(temporalCalendarLike)
|
|
|| isTemporalPlainYearMonthObject(temporalCalendarLike)
|
|
|| isTemporalZonedDateTimeObject(temporalCalendarLike)) {
|
|
return temporalCalendarLike.Calendar;
|
|
}
|
|
}
|
|
if (!(temporalCalendarLike instanceof JSStringValue)) {
|
|
return Throw.TypeError('temporalCalendarLike must be a string or a Temporal object, but got $1', temporalCalendarLike);
|
|
}
|
|
const identifier = Q(ParseTemporalCalendarString(temporalCalendarLike.stringValue()));
|
|
return Q(CanonicalizeCalendar(identifier));
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-gettemporalcalendaridentifierwithisodefault */
|
|
export function* GetTemporalCalendarIdentifierWithISODefault(item: ObjectValue): PlainEvaluator<CalendarType> {
|
|
if (isTemporalPlainDateObject(item)
|
|
|| isTemporalPlainDateTimeObject(item)
|
|
|| isTemporalPlainMonthDayObject(item)
|
|
|| isTemporalPlainYearMonthObject(item)
|
|
|| isTemporalZonedDateTimeObject(item)) {
|
|
return item.Calendar;
|
|
}
|
|
const calendarLike = Q(yield* Get(item, Value('calendar')));
|
|
if (calendarLike === Value.undefined) {
|
|
return 'iso8601';
|
|
}
|
|
return Q(ToTemporalCalendarIdentifier(calendarLike));
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-calendardatefromfields */
|
|
export function* CalendarDateFromFields(
|
|
calendar: CalendarType,
|
|
fields: CalendarFieldsRecord,
|
|
overflow: 'constrain' | 'reject',
|
|
): PlainEvaluator<ISODateRecord> {
|
|
Q(yield* CalendarResolveFields(calendar, fields, 'date'));
|
|
const result = Q(CalendarDateToISO(calendar, fields, overflow));
|
|
if (!ISODateWithinLimits(result)) {
|
|
return Throw.RangeError('Resulting ISODate is out of range');
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-calendaryearmonthfromfields */
|
|
export function* CalendarYearMonthFromFields(
|
|
calendar: CalendarType,
|
|
fields: CalendarFieldsRecord,
|
|
overflow: 'constrain' | 'reject',
|
|
): PlainEvaluator<ISODateRecord> {
|
|
Q(yield* CalendarResolveFields(calendar, fields, 'year-month'));
|
|
// Let firstDayIndex be the 1-based index of the first day of the month described by fields (i.e., 1 unless the month's first day is skipped by this calendar.)
|
|
const firstDayIndex = 1;
|
|
fields.Day = firstDayIndex;
|
|
const result = Q(CalendarDateToISO(calendar, fields, overflow));
|
|
if (!ISODateWithinLimits(result)) {
|
|
return Throw.RangeError('Resulting ISODate is out of range');
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-calendarmonthdayfromfields */
|
|
export function* CalendarMonthDayFromFields(
|
|
calendar: CalendarType,
|
|
fields: CalendarFieldsRecord,
|
|
overflow: 'constrain' | 'reject',
|
|
): PlainEvaluator<ISODateRecord> {
|
|
Q(yield* CalendarResolveFields(calendar, fields, 'month-day'));
|
|
const result = Q(CalendarMonthDayToISOReferenceDate(calendar, fields, overflow));
|
|
if (!ISODateWithinLimits(result)) {
|
|
return Throw.RangeError('Resulting ISODate is out of range');
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-formatcalendarannotation */
|
|
export function FormatCalendarAnnotation(
|
|
id: CalendarType,
|
|
showCalendar: 'auto' | 'always' | 'never' | 'critical',
|
|
): string {
|
|
if (showCalendar === 'never') {
|
|
return '';
|
|
}
|
|
if (showCalendar === 'auto' && id === 'iso8601') {
|
|
return '';
|
|
}
|
|
const flag = showCalendar === 'critical' ? '!' : '';
|
|
return `[${flag}u-ca=${id}]`;
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-calendarequals */
|
|
export function CalendarEquals(one: CalendarType, two: CalendarType): boolean {
|
|
if (CanonicalizeUValue('ca', one) === CanonicalizeUValue('ca', two)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-isodaysinmonth */
|
|
export function ISODaysInMonth(year: number, month: number): number {
|
|
if (month === 1 || month === 3 || month === 5 || month === 7 || month === 8 || month === 10 || month === 12) {
|
|
return 31;
|
|
}
|
|
if (month === 4 || month === 6 || month === 9 || month === 11) {
|
|
return 30;
|
|
}
|
|
Assert(month === 2);
|
|
return 28 + MathematicalInLeapYear(EpochTimeForYear(year));
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-isoweekofyear */
|
|
export function ISOWeekOfYear(isoDate: ISODateRecord): YearWeekRecord {
|
|
const year = isoDate.Year;
|
|
const wednesday = 3;
|
|
const thursday = 4;
|
|
const friday = 5;
|
|
const saturday = 6;
|
|
const daysInWeek = 7;
|
|
const maxWeekNumber = 53;
|
|
const dayOfYear = ISODayOfYear(isoDate);
|
|
const dayOfWeek = ISODayOfWeek(isoDate);
|
|
const week = Math.floor((dayOfYear + daysInWeek - dayOfWeek + wednesday) / daysInWeek);
|
|
if (week < 1) {
|
|
// NOTE: This is the last week of the previous year.
|
|
const jan1st = CreateISODateRecord(year, 1, 1);
|
|
const dayOfJan1st = ISODayOfWeek(jan1st);
|
|
if (dayOfJan1st === friday) {
|
|
return { Week: maxWeekNumber, Year: year - 1 };
|
|
}
|
|
if (dayOfJan1st === saturday && MathematicalInLeapYear(EpochTimeForYear(year - 1)) === 1) {
|
|
return { Week: maxWeekNumber, Year: year - 1 };
|
|
}
|
|
return { Week: maxWeekNumber - 1, Year: year - 1 };
|
|
}
|
|
if (week === maxWeekNumber) {
|
|
const daysInYear = MathematicalDaysInYear(year);
|
|
const daysLaterInYear = daysInYear - dayOfYear;
|
|
const daysAfterThursday = thursday - dayOfWeek;
|
|
if (daysLaterInYear < daysAfterThursday) {
|
|
return { Week: 1, Year: year + 1 };
|
|
}
|
|
}
|
|
return { Week: week, Year: year };
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-isodayofyear */
|
|
export function ISODayOfYear(isoDate: ISODateRecord): number {
|
|
const epochDays = ISODateToEpochDays(isoDate.Year, isoDate.Month - 1, isoDate.Day);
|
|
return EpochTimeToDayInYear(EpochDaysToEpochMs(epochDays, 0)) + 1;
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-isodayofweek */
|
|
export function ISODayOfWeek(isoDate: ISODateRecord): number {
|
|
const epochDays = ISODateToEpochDays(isoDate.Year, isoDate.Month - 1, isoDate.Day);
|
|
const dayOfWeek = EpochTimeToWeekDay(EpochDaysToEpochMs(epochDays, 0));
|
|
if (dayOfWeek === 0) {
|
|
return 7;
|
|
}
|
|
return dayOfWeek;
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-nonisocalendardatetoiso */
|
|
export function NonISOCalendarDateToISO(
|
|
_calendar: CalendarType,
|
|
_fields: CalendarFieldsRecord,
|
|
_overflow: 'constrain' | 'reject',
|
|
): PlainCompletion<ISODateRecord> {
|
|
mark_OtherCalendarNotImplemented();
|
|
unreachable_OtherCalendarNotImplemented();
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-calendardatetoiso */
|
|
export function CalendarDateToISO(
|
|
calendar: CalendarType,
|
|
fields: CalendarFieldsRecord,
|
|
overflow: 'constrain' | 'reject',
|
|
): PlainCompletion<ISODateRecord> {
|
|
if (calendar === 'iso8601') {
|
|
Assert(fields.Year !== undefined && fields.Month !== undefined && fields.Day !== undefined);
|
|
return Q(RegulateISODate(fields.Year, fields.Month, fields.Day, overflow));
|
|
}
|
|
return Q(NonISOCalendarDateToISO(calendar, fields, overflow));
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-nonisomonthdaytoisoreferencedate */
|
|
export function NonISOMonthDayToISOReferenceDate(
|
|
_calendar: CalendarType,
|
|
_fields: CalendarFieldsRecord,
|
|
_overflow: 'constrain' | 'reject',
|
|
): never {
|
|
mark_OtherCalendarNotImplemented();
|
|
unreachable_OtherCalendarNotImplemented();
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-calendarmonthdaytoisoreferencedate */
|
|
export function CalendarMonthDayToISOReferenceDate(
|
|
calendar: CalendarType,
|
|
fields: CalendarFieldsRecord,
|
|
overflow: 'constrain' | 'reject',
|
|
): PlainCompletion<ISODateRecord> {
|
|
if (calendar === 'iso8601') {
|
|
Assert(fields.Month !== undefined && fields.Day !== undefined);
|
|
const referenceISOYear = 1972;
|
|
const year = fields.Year === undefined ? referenceISOYear : fields.Year;
|
|
const result = Q(RegulateISODate(year, fields.Month, fields.Day, overflow));
|
|
return CreateISODateRecord(referenceISOYear, result.Month, result.Day);
|
|
}
|
|
return Q(NonISOMonthDayToISOReferenceDate(calendar, fields, overflow));
|
|
}
|
|
|
|
|
|
// NonISOCalendarISOToDate
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-nonisocalendarisotodate */
|
|
export function NonISOCalendarISOToDate(
|
|
_calendar: CalendarType,
|
|
_isoDate: ISODateRecord,
|
|
): CalendarDateRecord {
|
|
mark_OtherCalendarNotImplemented();
|
|
unreachable_OtherCalendarNotImplemented();
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-calendarisotodate */
|
|
export function CalendarISOToDate(
|
|
calendar: CalendarType,
|
|
isoDate: ISODateRecord,
|
|
): CalendarDateRecord {
|
|
if (calendar === 'iso8601') {
|
|
const inLeapYear = MathematicalInLeapYear(EpochTimeForYear(isoDate.Year)) === 1;
|
|
return {
|
|
Era: undefined,
|
|
EraYear: undefined,
|
|
Year: isoDate.Year,
|
|
Month: isoDate.Month,
|
|
MonthCode: CreateMonthCode(isoDate.Month, false),
|
|
Day: isoDate.Day,
|
|
DayOfWeek: ISODayOfWeek(isoDate),
|
|
DayOfYear: ISODayOfYear(isoDate),
|
|
WeekOfYear: ISOWeekOfYear(isoDate),
|
|
DaysInWeek: 7,
|
|
DaysInMonth: ISODaysInMonth(isoDate.Year, isoDate.Month),
|
|
DaysInYear: MathematicalDaysInYear(isoDate.Year),
|
|
MonthsInYear: 12,
|
|
InLeapYear: inLeapYear,
|
|
};
|
|
}
|
|
return NonISOCalendarISOToDate(calendar, isoDate);
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-calendarextrafields */
|
|
export function CalendarExtraFields(
|
|
calendar: CalendarType,
|
|
_fields: readonly CalendarFieldsRecordEnumerationKey[],
|
|
): CalendarFieldsRecordEnumerationKey[] {
|
|
if (calendar === 'iso8601') {
|
|
return [];
|
|
}
|
|
mark_OtherCalendarNotImplemented();
|
|
unreachable_OtherCalendarNotImplemented();
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-nonisofieldkeystoignore */
|
|
export function NonISOFieldKeysToIgnore(
|
|
_calendar: CalendarType,
|
|
_keys: readonly CalendarFieldsRecordEnumerationKey[],
|
|
): CalendarFieldsRecordEnumerationKey[] {
|
|
mark_OtherCalendarNotImplemented();
|
|
unreachable_OtherCalendarNotImplemented();
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-calendarfieldkeystoignore */
|
|
export function CalendarFieldKeysToIgnore(
|
|
calendar: CalendarType,
|
|
keys: readonly CalendarFieldsRecordEnumerationKey[],
|
|
): CalendarFieldsRecordEnumerationKey[] {
|
|
if (calendar === 'iso8601') {
|
|
const ignoredKeys: CalendarFieldsRecordEnumerationKey[] = [];
|
|
for (const key of keys) {
|
|
ignoredKeys.push(key);
|
|
if (key === 'month') {
|
|
ignoredKeys.push('month-code');
|
|
} else if (key === 'month-code') {
|
|
ignoredKeys.push('month');
|
|
}
|
|
}
|
|
return ignoredKeys;
|
|
}
|
|
return NonISOFieldKeysToIgnore(calendar, keys);
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-nonisoresolvefields */
|
|
export function NonISOResolveFields(
|
|
_calendar: CalendarType,
|
|
_fields: CalendarFieldsRecord,
|
|
_type: 'date' | 'year-month' | 'month-day',
|
|
): CalendarFieldsRecord {
|
|
mark_OtherCalendarNotImplemented();
|
|
unreachable_OtherCalendarNotImplemented();
|
|
}
|
|
|
|
/** https://tc39.es/proposal-temporal/#sec-temporal-calendarresolvefields */
|
|
export function* CalendarResolveFields(
|
|
calendar: CalendarType,
|
|
fields: CalendarFieldsRecord,
|
|
type: 'date' | 'year-month' | 'month-day',
|
|
): PlainEvaluator<void> {
|
|
if (calendar === 'iso8601') {
|
|
if ((type === 'date' || type === 'year-month') && fields.Year === undefined) {
|
|
return Throw.TypeError('"year" is required');
|
|
}
|
|
if ((type === 'date' || type === 'month-day') && fields.Day === undefined) {
|
|
return Throw.TypeError('"day" is required');
|
|
}
|
|
const month = fields.Month;
|
|
const monthCode = fields.MonthCode;
|
|
if (monthCode === undefined) {
|
|
if (month === undefined) {
|
|
return Throw.TypeError('"month-code" or "month" is required');
|
|
}
|
|
}
|
|
Assert(typeof monthCode === 'string');
|
|
const parsedMonthCode = Q(yield* ParseMonthCode(monthCode));
|
|
if (parsedMonthCode.IsLeapMonth) {
|
|
return Throw.RangeError('Invalid leap month');
|
|
}
|
|
if (parsedMonthCode.MonthNumber > 12) {
|
|
return Throw.RangeError('Invalid month');
|
|
}
|
|
if (month !== undefined && month !== parsedMonthCode.MonthNumber) {
|
|
return Throw.RangeError('Invalid month');
|
|
}
|
|
fields.Month = parsedMonthCode.MonthNumber;
|
|
} else {
|
|
Q(NonISOResolveFields(calendar, fields, type));
|
|
}
|
|
}
|