mirror of
https://github.com/bendtherules/ask262.git
synced 2026-08-18 13:21:55 +00:00
Merge commit '6e92d2345e8231a44556ce7d416451f5f3e6c398' as 'engine262'
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
// Addition/Edition to the main spec.
|
||||
// Code here should move elsewhere after Temporal is merged.
|
||||
|
||||
import type { ISODateTimeRecord } from '../../intrinsics/Temporal/PlainDateTime.mts';
|
||||
import { ParseTimeZoneIdentifier } from '../../parser/TemporalParser.mts';
|
||||
import { HourFromTime, MinFromTime, SecFromTime } from '../date-objects.mts';
|
||||
import { R as MathematicalValue } from '../spec-types.mjs';
|
||||
import { __ts_cast__ } from '../../helpers.mts';
|
||||
import { FormatTimeString, ToIntegerWithTruncation } from './temporal.mts';
|
||||
import { FormatOffsetTimeZoneIdentifier, type TimeZoneIdentifierRecord } from './time-zone.mts';
|
||||
import { mark_TimeZoneAwareNotImplemented, temporal_todo } from './not-implemented.mts';
|
||||
import {
|
||||
Assert,
|
||||
Get,
|
||||
JSStringValue,
|
||||
MakeDate,
|
||||
MakeDay,
|
||||
MakeTime,
|
||||
ObjectValue, OrdinaryObjectCreate, Q, R, Throw, TimeValueToISODateTimeRecord, ToBoolean, ToNumber, ToString, UndefinedValue, Value, X, type PlainEvaluator, type PropertyKeyValue,
|
||||
} from '#self';
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-year-week-record-specification-type */
|
||||
export interface YearWeekRecord {
|
||||
readonly Week: number | undefined;
|
||||
readonly Year: number | undefined;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-tointegerifintegral */
|
||||
export function* ToIntegerIfIntegral(argument: Value): PlainEvaluator<number> {
|
||||
const number = Q(yield* ToNumber(argument));
|
||||
if (!Number.isInteger(MathematicalValue(number))) {
|
||||
return Throw.RangeError('$1 is not an integral number', argument);
|
||||
}
|
||||
return R(number);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-getoptionsobject */
|
||||
export function GetOptionsObject(options: Value) {
|
||||
if (options instanceof UndefinedValue) {
|
||||
return OrdinaryObjectCreate(Value.null);
|
||||
}
|
||||
if (options instanceof ObjectValue) {
|
||||
return options;
|
||||
}
|
||||
return Throw.TypeError('$1 is not an object', options);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-getoption */
|
||||
export function GetOption<const T extends readonly string[], D extends T[number] | undefined>(options: ObjectValue, property: PropertyKeyValue | string, type: 'string', values: T | undefined, defaultValue: '~required~' | D): PlainEvaluator<D | T[number]>;
|
||||
export function GetOption<D extends boolean | undefined>(options: ObjectValue, property: PropertyKeyValue | string, type: 'boolean', values: undefined, defaultValue: '~required~' | D): PlainEvaluator<D>;
|
||||
export function* GetOption(options: ObjectValue, property: PropertyKeyValue | string, type: 'boolean' | 'string', values: readonly string[] | undefined, defaultValue: '~required~' | string | boolean | undefined): PlainEvaluator<string | boolean> {
|
||||
if (typeof property === 'string') {
|
||||
property = Value(property);
|
||||
}
|
||||
let value = Q(yield* Get(options, property));
|
||||
if (value === Value.undefined) {
|
||||
if (defaultValue === '~required~') {
|
||||
let propertyNameToString: string;
|
||||
if (typeof property === 'string') {
|
||||
propertyNameToString = property;
|
||||
} else if (property instanceof JSStringValue) {
|
||||
propertyNameToString = property.stringValue();
|
||||
} else if (property.Description instanceof JSStringValue) {
|
||||
propertyNameToString = `Symbol(${property.Description.stringValue()})`;
|
||||
} else {
|
||||
propertyNameToString = 'Symbol';
|
||||
}
|
||||
return Throw.RangeError('"$1" is required on object $2', propertyNameToString, options);
|
||||
}
|
||||
return defaultValue!;
|
||||
}
|
||||
if (type === 'boolean') {
|
||||
value = Q(ToBoolean(value));
|
||||
} else {
|
||||
Assert(type === 'string');
|
||||
value = Q(yield* ToString(value));
|
||||
}
|
||||
if (values !== undefined) {
|
||||
const str = (value as JSStringValue).stringValue();
|
||||
if (!values.includes(str)) {
|
||||
return Throw.RangeError('"$1" on object $2 is not valid ($3)', property, options, str);
|
||||
}
|
||||
}
|
||||
return value instanceof JSStringValue ? value.stringValue() : value.booleanValue();
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-getroundingmodeoption */
|
||||
export function* GetRoundingModeOption(
|
||||
options: ObjectValue,
|
||||
fallback: RoundingMode,
|
||||
): PlainEvaluator<RoundingMode> {
|
||||
const allowedStrings = ['ceil', 'floor', 'expand', 'trunc', 'halfCeil', 'halfFloor', 'halfExpand', 'halfTrunc', 'halfEven'] as const;
|
||||
const stringFallback = ({
|
||||
[RoundingMode.Ceil]: 'ceil',
|
||||
[RoundingMode.Floor]: 'floor',
|
||||
[RoundingMode.Expand]: 'expand',
|
||||
[RoundingMode.Trunc]: 'trunc',
|
||||
[RoundingMode.HalfCeil]: 'halfCeil',
|
||||
[RoundingMode.HalfFloor]: 'halfFloor',
|
||||
[RoundingMode.HalfExpand]: 'halfExpand',
|
||||
[RoundingMode.HalfTrunc]: 'halfTrunc',
|
||||
[RoundingMode.HalfEven]: 'halfEven',
|
||||
} as const)[fallback];
|
||||
const stringValue = Q(yield* GetOption(options, Value('roundingMode'), 'string', allowedStrings, stringFallback));
|
||||
return {
|
||||
ceil: RoundingMode.Ceil,
|
||||
floor: RoundingMode.Floor,
|
||||
expand: RoundingMode.Expand,
|
||||
trunc: RoundingMode.Trunc,
|
||||
halfCeil: RoundingMode.HalfCeil,
|
||||
halfFloor: RoundingMode.HalfFloor,
|
||||
halfExpand: RoundingMode.HalfExpand,
|
||||
halfTrunc: RoundingMode.HalfTrunc,
|
||||
halfEven: RoundingMode.HalfEven,
|
||||
}[stringValue];
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#table-temporal-rounding-modes */
|
||||
export enum RoundingMode {
|
||||
Ceil,
|
||||
Floor,
|
||||
Expand,
|
||||
Trunc,
|
||||
HalfCeil,
|
||||
HalfFloor,
|
||||
HalfExpand,
|
||||
HalfTrunc,
|
||||
HalfEven
|
||||
}
|
||||
/** https://tc39.es/proposal-temporal/#table-unsigned-rounding-modes */
|
||||
export enum UnsignedRoundingMode {
|
||||
Infinity, Zero, HalfInfinity, HalfZero, HalfEven
|
||||
}
|
||||
/** https://tc39.es/proposal-temporal/#sec-getroundingincrementoption */
|
||||
export function* GetRoundingIncrementOption(
|
||||
options: ObjectValue,
|
||||
): PlainEvaluator<number> {
|
||||
const value = Q(yield* Get(options, Value('roundingIncrement')));
|
||||
if (value === Value.undefined) {
|
||||
return 1;
|
||||
}
|
||||
const integerIncrement = Q(yield* ToIntegerWithTruncation(value));
|
||||
if (integerIncrement < 1 || integerIncrement > 10 ** 9) {
|
||||
return Throw.RangeError('"roundingIncrement" ($1) is out of range', integerIncrement);
|
||||
}
|
||||
return integerIncrement;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-getutcepochnanoseconds */
|
||||
export function GetUTCEpochNanoseconds(
|
||||
isoDateTime: ISODateTimeRecord,
|
||||
): bigint {
|
||||
const date = MakeDay(Value(isoDateTime.ISODate.Year), Value(isoDateTime.ISODate.Month - 1), Value(isoDateTime.ISODate.Day));
|
||||
const time = MakeTime(Value(isoDateTime.Time.Hour), Value(isoDateTime.Time.Minute), Value(isoDateTime.Time.Second), Value(isoDateTime.Time.Millisecond));
|
||||
const ms = R(MakeDate(date, time));
|
||||
Assert(Math.floor(ms) === ms);
|
||||
return BigInt(ms) * BigInt(10e6) + BigInt(isoDateTime.Time.Microsecond) * BigInt(10e3) + BigInt(isoDateTime.Time.Nanosecond);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-time-zone-identifiers */
|
||||
export type TimeZoneIdentifier = string & { readonly TimeZoneIdentifier: never; };
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-getnamedtimezoneepochnanoseconds */
|
||||
export function GetNamedTimeZoneEpochNanoseconds(
|
||||
timeZoneIdentifier: TimeZoneIdentifier,
|
||||
isoDateTime: ISODateTimeRecord,
|
||||
): bigint[] {
|
||||
mark_TimeZoneAwareNotImplemented();
|
||||
Assert(timeZoneIdentifier === 'UTC');
|
||||
const epochNanoseconds = GetUTCEpochNanoseconds(isoDateTime);
|
||||
return [epochNanoseconds];
|
||||
}
|
||||
|
||||
/** https://tc39.es/ecma262/#sec-getnamedtimezoneoffsetnanoseconds */
|
||||
export function GetNamedTimeZoneOffsetNanoseconds(timeZoneIdentifier: string, _epochNanoseconds: bigint) {
|
||||
mark_TimeZoneAwareNotImplemented();
|
||||
Assert(timeZoneIdentifier === 'UTC');
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-systemtimezoneidentifier */
|
||||
export function SystemTimeZoneIdentifier(): TimeZoneIdentifier {
|
||||
mark_TimeZoneAwareNotImplemented();
|
||||
// 1. If the implementation only supports the UTC time zone, return "UTC".
|
||||
return 'UTC' as TimeZoneIdentifier;
|
||||
// 2. Let systemTimeZoneString be the String representing the host environment's current time zone as a time zone identifier in normalized format, either a primary time zone identifier or an offset time zone identifier.
|
||||
// 3. Return systemTimeZoneString.
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-localtime */
|
||||
export function LocalTime_TemporalEdited(t: number): number {
|
||||
const systemTimeZoneIdentifier = SystemTimeZoneIdentifier();
|
||||
const parseResult = X(ParseTimeZoneIdentifier(systemTimeZoneIdentifier));
|
||||
let offsetNs: number;
|
||||
if (parseResult.OffsetMinutes !== undefined) {
|
||||
offsetNs = parseResult.OffsetMinutes * (60 * 1e9);
|
||||
} else {
|
||||
offsetNs = GetNamedTimeZoneOffsetNanoseconds(systemTimeZoneIdentifier, BigInt(t * 1e6));
|
||||
}
|
||||
const offsetMs = Math.trunc(offsetNs / 1e6);
|
||||
return t + offsetMs;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-utc-t */
|
||||
export function UTC_TemporalEdited(t: number): number {
|
||||
if (!Number.isFinite(t)) {
|
||||
return NaN;
|
||||
}
|
||||
const systemTimeZoneIdentifier = SystemTimeZoneIdentifier();
|
||||
const parseResult = X(ParseTimeZoneIdentifier(systemTimeZoneIdentifier));
|
||||
let offsetNs: number;
|
||||
if (parseResult.OffsetMinutes !== undefined) {
|
||||
offsetNs = parseResult.OffsetMinutes * (60 * 1e9);
|
||||
} else {
|
||||
const isoDateTime = TimeValueToISODateTimeRecord(t);
|
||||
const possibleInstants = GetNamedTimeZoneEpochNanoseconds(systemTimeZoneIdentifier, isoDateTime);
|
||||
let disambiguatedInstant: bigint;
|
||||
if (possibleInstants.length > 0) {
|
||||
disambiguatedInstant = possibleInstants[0];
|
||||
} else {
|
||||
// TODO(temporal): review
|
||||
// ii. Let possibleInstantsBefore be GetNamedTimeZoneEpochNanoseconds(systemTimeZoneIdentifier, ℝ(YearFromTime(tBefore)), ℝ(MonthFromTime(tBefore)) + 1, ℝ(DateFromTime(tBefore)), ℝ(HourFromTime(tBefore)), ℝ(MinFromTime(tBefore)), ℝ(SecFromTime(tBefore)), ℝ(msFromTime(tBefore)), 0, 0TimeValueToISODateTimeRecord(tBefore)), where tBefore is the largest integral Number < t for which possibleInstantsBefore is not empty (i.e., tBefore represents the last local time before the transition).
|
||||
let tBefore = Math.floor(t) - 1;
|
||||
let possibleInstantsBefore: bigint[] = [];
|
||||
while (possibleInstantsBefore.length === 0) {
|
||||
possibleInstantsBefore = GetNamedTimeZoneEpochNanoseconds(systemTimeZoneIdentifier, TimeValueToISODateTimeRecord(tBefore));
|
||||
tBefore -= 1;
|
||||
}
|
||||
// iii. Let disambiguatedInstant be the last element of possibleInstantsBefore.
|
||||
disambiguatedInstant = possibleInstantsBefore[possibleInstantsBefore.length - 1];
|
||||
}
|
||||
offsetNs = GetNamedTimeZoneOffsetNanoseconds(systemTimeZoneIdentifier, disambiguatedInstant);
|
||||
}
|
||||
const offsetMs = Math.trunc(offsetNs / 1e6);
|
||||
return t - offsetMs;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-timestring */
|
||||
export function TimeString(tv: number): string {
|
||||
const timeString = FormatTimeString(R(HourFromTime(Value(tv))), R(MinFromTime(Value(tv))), R(SecFromTime(Value(tv))), 0, 0);
|
||||
return `${timeString} GMT`;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-timezoneestring */
|
||||
export function TimeZoneString_TemporalEdited(tv: number): string {
|
||||
const systemTimeZoneIdentifier = SystemTimeZoneIdentifier();
|
||||
let offsetMinutes = X(ParseTimeZoneIdentifier(systemTimeZoneIdentifier)).OffsetMinutes;
|
||||
if (offsetMinutes === undefined) {
|
||||
const offsetNs = GetNamedTimeZoneOffsetNanoseconds(systemTimeZoneIdentifier, BigInt(tv * 1e6));
|
||||
offsetMinutes = Math.trunc(offsetNs / (60 * 1e9));
|
||||
}
|
||||
const offsetString = FormatOffsetTimeZoneIdentifier(offsetMinutes, 'unseparated');
|
||||
const tzName = '';
|
||||
return offsetString + tzName;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-isoffsettimezoneidentifier */
|
||||
export function IsOffsetTimeZoneIdentifier(_offsetString: string): boolean {
|
||||
temporal_todo();
|
||||
}
|
||||
|
||||
/** https://tc39.es/ecma262/#sec-tozeropaddeddecimalstring */
|
||||
export function ToZeroPaddedDecimalString(n: number, minLength: number) {
|
||||
return n.toString().padStart(minLength, '0');
|
||||
}
|
||||
|
||||
/** https://tc39.es/ecma262/#sec-availablenamedtimezoneidentifiers */
|
||||
export function AvailableNamedTimeZoneIdentifiers(): TimeZoneIdentifierRecord[] {
|
||||
mark_TimeZoneAwareNotImplemented();
|
||||
return [{
|
||||
Identifier: 'UTC' as TimeZoneIdentifier,
|
||||
PrimaryIdentifier: 'UTC' as TimeZoneIdentifier,
|
||||
}];
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export * from './calendar.mts';
|
||||
export * from './duration.mts';
|
||||
export * from './instant.mts';
|
||||
export * from './now.mts';
|
||||
export * from './plain-date-time.mts';
|
||||
export * from './plain-date.mts';
|
||||
export * from './plain-month-day.mts';
|
||||
export * from './plain-time.mts';
|
||||
export * from './plain-year-month.mts';
|
||||
export * from './temporal.mts';
|
||||
export * from './time-zone.mts';
|
||||
export * from './zoned-datetime.mts';
|
||||
@@ -0,0 +1,724 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,177 @@
|
||||
import type { TemporalDurationObject } from '../../intrinsics/Temporal/Duration.mts';
|
||||
import { type TemporalInstantObject, isTemporalInstantObject } from '../../intrinsics/Temporal/Instant.mts';
|
||||
import { isTemporalZonedDateTimeObject } from '../../intrinsics/Temporal/ZonedDateTime.mts';
|
||||
import { ParseISODateTime, ParseDateTimeUTCOffset } from '../../parser/TemporalParser.mts';
|
||||
import {
|
||||
GetUTCEpochNanoseconds, RoundingMode, type TimeZoneIdentifier, GetOptionsObject,
|
||||
} from './addition.mts';
|
||||
import {
|
||||
type FunctionObject, type ValueEvaluator, Assert, surroundingAgent, Q, OrdinaryCreateFromConstructor, type Mutable, Value, ObjectValue, X, ToPrimitive, JSStringValue, Throw, CheckISODaysRange, type TimeDuration, type PlainCompletion, AddTimeDurationToEpochNanoseconds, type TimeUnit, type InternalDurationRecord, TimeDurationFromEpochNanosecondsDifference, RoundTimeDuration, CombineDateAndTimeDuration, ZeroDateDuration, Table21_LengthInNanoSeconds, RoundNumberToIncrementAsIfPositive, GetISODateTimeFor, GetOffsetNanosecondsFor, FormatDateTimeUTCOffsetRounded, GetDifferenceSettings, TemporalUnit, TemporalDurationFromInternal, CreateNegatedTemporalDuration, ToTemporalDuration, DefaultTemporalLargestUnit, TemporalUnitCategory, ToInternalDurationRecordWith24HourDays,
|
||||
BalanceISODateTime,
|
||||
ISODateTimeToString,
|
||||
} from '#self';
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#eqn-nsPerDay */
|
||||
export const nsPerDay = 8.64e13;
|
||||
/** https://tc39.es/proposal-temporal/#eqn-nsMaxInstant */
|
||||
export const nsMaxInstant = 8.64e21;
|
||||
/** https://tc39.es/proposal-temporal/#eqn-nsMinInstant */
|
||||
export const nsMinInstant = -8.64e21;
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-isvalidepochnanoseconds */
|
||||
export function IsValidEpochNanoseconds(epochNanoseconds: bigint | number): boolean {
|
||||
if (epochNanoseconds < nsMinInstant || epochNanoseconds > nsMaxInstant) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-createtemporalinstant */
|
||||
export function* CreateTemporalInstant(epochNanoseconds: bigint, newTarget?: FunctionObject): ValueEvaluator<TemporalInstantObject> {
|
||||
Assert(IsValidEpochNanoseconds(epochNanoseconds));
|
||||
if (newTarget === undefined) {
|
||||
newTarget = surroundingAgent.intrinsic('%Temporal.Instant%');
|
||||
}
|
||||
const object = Q(yield* OrdinaryCreateFromConstructor(newTarget, '%Temporal.Instant.prototype%', [
|
||||
'InitializedTemporalInstant',
|
||||
'EpochNanoseconds',
|
||||
])) as Mutable<TemporalInstantObject>;
|
||||
object.EpochNanoseconds = epochNanoseconds;
|
||||
return object;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-totemporalinstant */
|
||||
export function* ToTemporalInstant(item: Value): ValueEvaluator<TemporalInstantObject> {
|
||||
if (item instanceof ObjectValue) {
|
||||
if (isTemporalInstantObject(item) || isTemporalZonedDateTimeObject(item)) {
|
||||
return X(CreateTemporalInstant(item.EpochNanoseconds));
|
||||
}
|
||||
item = Q(yield* ToPrimitive(item, 'string'));
|
||||
}
|
||||
if (!(item instanceof JSStringValue)) {
|
||||
return Throw.TypeError('$1 is not a string', item);
|
||||
}
|
||||
const parsed = Q(ParseISODateTime(item.stringValue(), ['TemporalInstantString']));
|
||||
// Assert: Either parsed.[[TimeZone]].[[OffsetString]] is not empty or parsed.[[TimeZone]].[[Z]] is true, but not both.
|
||||
{
|
||||
const a = parsed.TimeZone.OffsetString !== undefined;
|
||||
const b = parsed.TimeZone.Z;
|
||||
Assert((a || b) && !(a && b));
|
||||
}
|
||||
const OffsetString = parsed.TimeZone.OffsetString!;
|
||||
const offsetNanoseconds = parsed.TimeZone.Z ? 0 : X(ParseDateTimeUTCOffset(OffsetString));
|
||||
const time = parsed.Time;
|
||||
Assert(time !== 'start-of-day');
|
||||
const balanced = BalanceISODateTime(parsed.Year!, parsed.Month, parsed.Day, time.Hour, time.Minute, time.Second, time.Millisecond, time.Microsecond, time.Nanosecond - offsetNanoseconds);
|
||||
Q(CheckISODaysRange(balanced.ISODate));
|
||||
const epochNanoseconds = GetUTCEpochNanoseconds(balanced);
|
||||
if (!IsValidEpochNanoseconds(epochNanoseconds)) {
|
||||
return Throw.RangeError('$1 is not a valid epoch nanoseconds', epochNanoseconds);
|
||||
}
|
||||
return X(CreateTemporalInstant(epochNanoseconds));
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-compareepochnanoseconds */
|
||||
export function CompareEpochNanoseconds(epochNanosecondsOne: bigint, epochNanosecondsTwo: bigint): -1 | 0 | 1 {
|
||||
if (epochNanosecondsOne > epochNanosecondsTwo) {
|
||||
return 1;
|
||||
}
|
||||
if (epochNanosecondsOne < epochNanosecondsTwo) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-addinstant */
|
||||
export function AddInstant(epochNanoseconds: bigint, timeDuration: TimeDuration): PlainCompletion<bigint> {
|
||||
const result = AddTimeDurationToEpochNanoseconds(timeDuration, epochNanoseconds);
|
||||
if (!IsValidEpochNanoseconds(result)) {
|
||||
return Throw.RangeError('$1 is not a valid epoch nanoseconds', result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-differenceinstant */
|
||||
export function DifferenceInstant(
|
||||
ns1: bigint,
|
||||
ns2: bigint,
|
||||
roundingIncrement: number,
|
||||
smallestUnit: TimeUnit,
|
||||
roundingMode: RoundingMode,
|
||||
): InternalDurationRecord {
|
||||
let timeDuration = TimeDurationFromEpochNanosecondsDifference(ns2, ns1);
|
||||
timeDuration = X(RoundTimeDuration(timeDuration, roundingIncrement, smallestUnit, roundingMode));
|
||||
return CombineDateAndTimeDuration(ZeroDateDuration(), timeDuration);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-roundtemporalinstant */
|
||||
export function RoundTemporalInstant(
|
||||
ns: bigint,
|
||||
increment: number,
|
||||
unit: TimeUnit,
|
||||
roundingMode: RoundingMode,
|
||||
): bigint {
|
||||
const unitLength = Table21_LengthInNanoSeconds[unit];
|
||||
const incrementNs = increment * unitLength;
|
||||
return BigInt(RoundNumberToIncrementAsIfPositive(Number(ns), incrementNs, roundingMode));
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-temporalinstant-tostring */
|
||||
export function TemporalInstantToString(
|
||||
instant: TemporalInstantObject,
|
||||
timeZone: TimeZoneIdentifier | undefined,
|
||||
precision: number | 'minute' | 'auto',
|
||||
): string {
|
||||
let outputTimeZone = timeZone;
|
||||
if (outputTimeZone === undefined) {
|
||||
outputTimeZone = 'UTC' as TimeZoneIdentifier;
|
||||
}
|
||||
const epochNs = instant.EpochNanoseconds;
|
||||
const isoDateTime = GetISODateTimeFor(outputTimeZone, epochNs);
|
||||
const dateTimeString = ISODateTimeToString(isoDateTime, 'iso8601', precision, 'never');
|
||||
let timeZoneString;
|
||||
if (timeZone === undefined) {
|
||||
timeZoneString = 'Z';
|
||||
} else {
|
||||
const offsetNanoseconds = GetOffsetNanosecondsFor(outputTimeZone, epochNs);
|
||||
timeZoneString = FormatDateTimeUTCOffsetRounded(offsetNanoseconds);
|
||||
}
|
||||
return dateTimeString + timeZoneString;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-differencetemporalinstant */
|
||||
export function* DifferenceTemporalInstant(
|
||||
operation: 'since' | 'until',
|
||||
instant: TemporalInstantObject,
|
||||
_other: Value,
|
||||
options: Value,
|
||||
): ValueEvaluator<TemporalDurationObject> {
|
||||
const other = Q(yield* ToTemporalInstant(_other));
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
const settings = Q(yield* GetDifferenceSettings(operation, resolvedOptions, 'time', [], TemporalUnit.Nanosecond, TemporalUnit.Second));
|
||||
const internalDuration = DifferenceInstant(instant.EpochNanoseconds, other.EpochNanoseconds, settings.RoundingIncrement, settings.SmallestUnit as TimeUnit, settings.RoundingMode);
|
||||
let result = X(TemporalDurationFromInternal(internalDuration, settings.LargestUnit));
|
||||
if (operation === 'since') {
|
||||
result = CreateNegatedTemporalDuration(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-adddurationtoinstant */
|
||||
export function* AddDurationToInstant(
|
||||
operation: 'add' | 'subtract',
|
||||
instant: TemporalInstantObject,
|
||||
temporalDurationLike: Value,
|
||||
): ValueEvaluator<TemporalInstantObject> {
|
||||
let duration = Q(yield* ToTemporalDuration(temporalDurationLike));
|
||||
if (operation === 'subtract') {
|
||||
duration = CreateNegatedTemporalDuration(duration);
|
||||
}
|
||||
const largestUnit = DefaultTemporalLargestUnit(duration);
|
||||
if (TemporalUnitCategory(largestUnit) === 'date') {
|
||||
return Throw.RangeError('Cannot add a date to an instant');
|
||||
}
|
||||
const internalDuration = ToInternalDurationRecordWith24HourDays(duration);
|
||||
const ns = Q(AddInstant(instant.EpochNanoseconds, internalDuration.Time));
|
||||
return X(CreateTemporalInstant(ns));
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export function mark_TimeZoneAwareNotImplemented() {
|
||||
'Time zone aware operations are not implemented in this engine.';
|
||||
}
|
||||
|
||||
export function mark_OtherCalendarNotImplemented() {
|
||||
'Other calendar than iso8601 are not implemented in this engine.';
|
||||
}
|
||||
|
||||
export function unreachable_OtherCalendarNotImplemented(): never {
|
||||
throw new Error('Calendar other than ISO8601 is not supported, but this error should never triggered by the user code.');
|
||||
}
|
||||
|
||||
export function temporal_todo(): never {
|
||||
throw new Error('This Temporal operation is not implemented yet.');
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { ISODateTimeRecord } from '../../intrinsics/Temporal/PlainDateTime.mts';
|
||||
import { SystemTimeZoneIdentifier } from './addition.mts';
|
||||
import { temporal_todo } from './not-implemented.mts';
|
||||
import {
|
||||
ObjectValue, GetGlobalObject, Value, type PlainCompletion, Q, ToTemporalTimeZoneIdentifier, GetISODateTimeFor,
|
||||
} from '#self';
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-hostsystemutcepochnanoseconds */
|
||||
export function HostSystemUTCEpochNanoseconds(_global: ObjectValue): number {
|
||||
temporal_todo();
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-systemutcepochmilliseconds */
|
||||
export function SystemUTCEpochMilliseconds(): number {
|
||||
const global = GetGlobalObject();
|
||||
const nowNs = HostSystemUTCEpochNanoseconds(global);
|
||||
return Math.floor(nowNs / (10 ** 6));
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-systemutcepochnanoseconds */
|
||||
export function SystemUTCEpochNanoseconds(): bigint {
|
||||
const global = GetGlobalObject();
|
||||
const nowNs = HostSystemUTCEpochNanoseconds(global);
|
||||
return BigInt(nowNs);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-systemdatetime */
|
||||
export function SystemDateTime(temporalTimeZoneLike: Value): PlainCompletion<ISODateTimeRecord> {
|
||||
let timeZone;
|
||||
if (temporalTimeZoneLike === Value.undefined) {
|
||||
timeZone = SystemTimeZoneIdentifier();
|
||||
} else {
|
||||
timeZone = Q(ToTemporalTimeZoneIdentifier(temporalTimeZoneLike));
|
||||
}
|
||||
const epochNs = SystemUTCEpochNanoseconds();
|
||||
return GetISODateTimeFor(timeZone, epochNs);
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
import type { TemporalDurationObject } from '../../intrinsics/Temporal/Duration.mts';
|
||||
import { type ISODateRecord, isTemporalPlainDateObject } from '../../intrinsics/Temporal/PlainDate.mts';
|
||||
import { type ISODateTimeRecord, type TemporalPlainDateTimeObject, isTemporalPlainDateTimeObject } from '../../intrinsics/Temporal/PlainDateTime.mts';
|
||||
import { isTemporalZonedDateTimeObject } from '../../intrinsics/Temporal/ZonedDateTime.mts';
|
||||
import { ParseISODateTime } from '../../parser/TemporalParser.mts';
|
||||
import { abs } from '../math.mts';
|
||||
import {
|
||||
GetOptionsObject,
|
||||
GetUTCEpochNanoseconds, ToZeroPaddedDecimalString, type RoundingMode,
|
||||
} from './addition.mts';
|
||||
import {
|
||||
CreateISODateRecord, R, YearFromTime, MonthFromTime, DateFromTime, CreateTimeRecord, HourFromTime, MinFromTime, SecFromTime, msFromTime, type TimeRecord, ISODateToEpochDays, nsMinInstant, nsPerDay, nsMaxInstant, type CalendarType, type CalendarFieldsRecord, type PlainEvaluator, Q, CalendarDateFromFields, RegulateTime, Value, ObjectValue, GetTemporalOverflowOption, X, GetISODateTimeFor, MidnightTimeRecord, GetTemporalCalendarIdentifierWithISODefault, PrepareCalendarFields, JSStringValue, Throw, CanonicalizeCalendar, BalanceTime, AddDaysToISODate, type FunctionObject, surroundingAgent, OrdinaryCreateFromConstructor, type Mutable, PadISOYear, FormatTimeString, FormatCalendarAnnotation, CompareISODate, CompareTimeRecord, type TimeUnit, TemporalUnit, Assert, RoundTime, type InternalDurationRecord, DifferenceTime, TimeDurationSign, Add24HourDaysToTimeDuration, LargerOfTwoTemporalUnits, CalendarDateUntil, type DateUnit, CombineDateAndTimeDuration, type PlainCompletion, ZeroDateDuration, type TimeDuration, RoundRelativeDuration, TotalRelativeDuration, type ValueEvaluator, CalendarEquals, GetDifferenceSettings, CreateTemporalDuration, TemporalDurationFromInternal, CreateNegatedTemporalDuration, ToTemporalDuration, ToInternalDurationRecordWith24HourDays, AddTime, AdjustDateDurationRecord, CalendarDateAdd,
|
||||
} from '#self';
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-timevaluetoisodatetimerecord */
|
||||
export function TimeValueToISODateTimeRecord(t: number): ISODateTimeRecord {
|
||||
const isoDate = CreateISODateRecord(
|
||||
R(YearFromTime(t)),
|
||||
R(MonthFromTime(t)) + 1,
|
||||
R(DateFromTime(t)),
|
||||
);
|
||||
const time = CreateTimeRecord(R(HourFromTime(t)), R(MinFromTime(t)), R(SecFromTime(t)), R(msFromTime(t)), 0, 0);
|
||||
return { ISODate: isoDate, Time: time };
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-combineisodateandtimerecord */
|
||||
export function CombineISODateAndTimeRecord(isoDate: ISODateRecord, time: TimeRecord): ISODateTimeRecord {
|
||||
return { ISODate: isoDate, Time: time };
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-isodatetimewithinlimits */
|
||||
export function ISODateTimeWithinLimits(isoDateTime: ISODateTimeRecord): boolean {
|
||||
if (abs(ISODateToEpochDays(isoDateTime.ISODate.Year, isoDateTime.ISODate.Month - 1, isoDateTime.ISODate.Day)) > 1e8 + 1) {
|
||||
return false;
|
||||
}
|
||||
const ns = GetUTCEpochNanoseconds(isoDateTime);
|
||||
if (ns <= nsMinInstant - nsPerDay) {
|
||||
return false;
|
||||
}
|
||||
if (ns >= nsMaxInstant + nsPerDay) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-interprettemporaldatetimefields */
|
||||
export function* InterpretTemporalDateTimeFields(calendar: CalendarType, fields: CalendarFieldsRecord, overflow: 'constrain' | 'reject'): PlainEvaluator<ISODateTimeRecord> {
|
||||
const isoDate = Q(yield* CalendarDateFromFields(calendar, fields, overflow));
|
||||
const time = Q(RegulateTime(fields.Hour!, fields.Minute!, fields.Second!, fields.Millisecond!, fields.Microsecond!, fields.Nanosecond!, overflow));
|
||||
return CombineISODateAndTimeRecord(isoDate, time);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-totemporaldatetime */
|
||||
export function* ToTemporalDateTime(item: Value, options: Value = Value.undefined): PlainEvaluator<TemporalPlainDateTimeObject> {
|
||||
if (item instanceof ObjectValue) {
|
||||
if (isTemporalPlainDateTimeObject(item)) {
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
return X(CreateTemporalDateTime(item.ISODateTime, item.Calendar));
|
||||
}
|
||||
if (isTemporalZonedDateTimeObject(item)) {
|
||||
const isoDateTime = GetISODateTimeFor(item.TimeZone, item.EpochNanoseconds);
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
return X(CreateTemporalDateTime(isoDateTime, item.Calendar));
|
||||
}
|
||||
if (isTemporalPlainDateObject(item)) {
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
const isoDateTime = CombineISODateAndTimeRecord(item.ISODate, MidnightTimeRecord());
|
||||
return Q(yield* CreateTemporalDateTime(isoDateTime, item.Calendar));
|
||||
}
|
||||
const calendar = Q(yield* GetTemporalCalendarIdentifierWithISODefault(item));
|
||||
const fields = Q(yield* PrepareCalendarFields(calendar, item, ['year', 'month', 'month-code', 'day'], ['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'], []));
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
const overflow = Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
const result = Q(yield* InterpretTemporalDateTimeFields(calendar, fields, overflow));
|
||||
return Q(yield* CreateTemporalDateTime(result, calendar));
|
||||
}
|
||||
if (!(item instanceof JSStringValue)) {
|
||||
return Throw.TypeError('$1 is not a string', item);
|
||||
}
|
||||
const result = Q(ParseISODateTime(item.stringValue(), ['TemporalDateTimeString[~Zoned]']));
|
||||
const time = result.Time === 'start-of-day' ? MidnightTimeRecord() : result.Time;
|
||||
const calendar = result.Calendar ?? 'iso8601';
|
||||
const calendarType = Q(CanonicalizeCalendar(calendar));
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
const isoDate = CreateISODateRecord(result.Year!, result.Month, result.Day);
|
||||
const isoDateTime = CombineISODateAndTimeRecord(isoDate, time);
|
||||
return Q(yield* CreateTemporalDateTime(isoDateTime, calendarType));
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-balanceisodatetime */
|
||||
export function BalanceISODateTime(year: number, month: number, day: number, hour: number, minute: number, second: number, millisecond: number, microsecond: number, nanosecond: number): ISODateTimeRecord {
|
||||
const balancedTime = BalanceTime(hour, minute, second, millisecond, microsecond, nanosecond);
|
||||
const balancedDate = AddDaysToISODate(CreateISODateRecord(year, month, day), balancedTime.Days);
|
||||
return CombineISODateAndTimeRecord(balancedDate, balancedTime);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-createtemporaldatetime */
|
||||
export function* CreateTemporalDateTime(isoDateTime: ISODateTimeRecord, calendar: CalendarType, newTarget?: FunctionObject): PlainEvaluator<TemporalPlainDateTimeObject> {
|
||||
if (!ISODateTimeWithinLimits(isoDateTime)) {
|
||||
return Throw.RangeError('PlainDateTime outside of range');
|
||||
}
|
||||
if (newTarget === undefined) {
|
||||
newTarget = surroundingAgent.intrinsic('%Temporal.PlainDateTime%');
|
||||
}
|
||||
const object = Q(yield* OrdinaryCreateFromConstructor(newTarget, '%Temporal.PlainDateTime.prototype%', [
|
||||
'InitializedTemporalDateTime',
|
||||
'ISODateTime',
|
||||
'Calendar',
|
||||
])) as Mutable<TemporalPlainDateTimeObject>;
|
||||
object.ISODateTime = isoDateTime;
|
||||
object.Calendar = calendar;
|
||||
return object;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-isodatetimetostring */
|
||||
export function ISODateTimeToString(isoDateTime: ISODateTimeRecord, calendar: CalendarType, precision: number | 'minute' | 'auto', showCalendar: 'auto' | 'always' | 'never' | 'critical'): string {
|
||||
const yearString = PadISOYear(isoDateTime.ISODate.Year);
|
||||
const monthString = ToZeroPaddedDecimalString(isoDateTime.ISODate.Month, 2);
|
||||
const dayString = ToZeroPaddedDecimalString(isoDateTime.ISODate.Day, 2);
|
||||
const subSecondNanoseconds = isoDateTime.Time.Millisecond * 1e6 + isoDateTime.Time.Microsecond * 1e3 + isoDateTime.Time.Nanosecond;
|
||||
const timeString = FormatTimeString(isoDateTime.Time.Hour, isoDateTime.Time.Minute, isoDateTime.Time.Second, subSecondNanoseconds, precision);
|
||||
const calendarString = FormatCalendarAnnotation(calendar, showCalendar);
|
||||
return `${yearString}-${monthString}-${dayString}T${timeString}${calendarString}`;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-compareisodatetime */
|
||||
export function CompareISODateTime(isoDateTime1: ISODateTimeRecord, isoDateTime2: ISODateTimeRecord): 1 | -1 | 0 {
|
||||
const dateResult = CompareISODate(isoDateTime1.ISODate, isoDateTime2.ISODate);
|
||||
if (dateResult !== 0) {
|
||||
return dateResult;
|
||||
}
|
||||
return CompareTimeRecord(isoDateTime1.Time, isoDateTime2.Time);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-roundisodatetime */
|
||||
export function RoundISODateTime(isoDateTime: ISODateTimeRecord, increment: number, unit: TimeUnit | TemporalUnit.Day, roundingMode: RoundingMode): ISODateTimeRecord {
|
||||
Assert(ISODateTimeWithinLimits(isoDateTime));
|
||||
const roundedTime = RoundTime(isoDateTime.Time, increment, unit, roundingMode);
|
||||
const balanceResult = AddDaysToISODate(isoDateTime.ISODate, roundedTime.Days);
|
||||
return CombineISODateAndTimeRecord(balanceResult, roundedTime);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-differenceisodatetime */
|
||||
export function DifferenceISODateTime(isoDateTime1: ISODateTimeRecord, isoDateTime2: ISODateTimeRecord, calendar: CalendarType, largestUnit: TemporalUnit): InternalDurationRecord {
|
||||
Assert(ISODateTimeWithinLimits(isoDateTime1));
|
||||
Assert(ISODateTimeWithinLimits(isoDateTime2));
|
||||
let timeDuration = DifferenceTime(isoDateTime1.Time, isoDateTime2.Time);
|
||||
const timeSign = TimeDurationSign(timeDuration);
|
||||
const dateSign = CompareISODate(isoDateTime1.ISODate, isoDateTime2.ISODate);
|
||||
let adjustedDate = isoDateTime2.ISODate;
|
||||
if (timeSign === dateSign) {
|
||||
adjustedDate = AddDaysToISODate(adjustedDate, timeSign);
|
||||
timeDuration = X(Add24HourDaysToTimeDuration(timeDuration, -timeSign));
|
||||
}
|
||||
const dateLargestUnit = LargerOfTwoTemporalUnits(TemporalUnit.Day, largestUnit);
|
||||
const dateDifference = CalendarDateUntil(calendar, isoDateTime1.ISODate, adjustedDate, dateLargestUnit as DateUnit);
|
||||
if (largestUnit !== dateLargestUnit) {
|
||||
timeDuration = X(Add24HourDaysToTimeDuration(timeDuration, dateDifference.Days));
|
||||
dateDifference.Days = 0;
|
||||
}
|
||||
return CombineDateAndTimeDuration(dateDifference, timeDuration);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-differenceplaindatetimewithrounding */
|
||||
export function DifferencePlainDateTimeWithRounding(isoDateTime1: ISODateTimeRecord, isoDateTime2: ISODateTimeRecord, calendar: CalendarType, largestUnit: TemporalUnit, roundingIncrement: number, smallestUnit: TemporalUnit, roundingMode: RoundingMode): PlainCompletion<InternalDurationRecord> {
|
||||
if (CompareISODateTime(isoDateTime1, isoDateTime2) === 0) {
|
||||
return CombineDateAndTimeDuration(ZeroDateDuration(), 0 as TimeDuration);
|
||||
}
|
||||
if (!ISODateTimeWithinLimits(isoDateTime1) || !ISODateTimeWithinLimits(isoDateTime2)) {
|
||||
return Throw.RangeError('PlainDateTime outside of range');
|
||||
}
|
||||
const diff = DifferenceISODateTime(isoDateTime1, isoDateTime2, calendar, largestUnit);
|
||||
if (smallestUnit === TemporalUnit.Nanosecond && roundingIncrement === 1) {
|
||||
return diff;
|
||||
}
|
||||
const originEpochNs = GetUTCEpochNanoseconds(isoDateTime1);
|
||||
const destEpochNs = GetUTCEpochNanoseconds(isoDateTime2);
|
||||
return RoundRelativeDuration(diff, originEpochNs, destEpochNs, isoDateTime1, undefined, calendar, largestUnit, roundingIncrement, smallestUnit, roundingMode);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-differenceplaindatetimewithtotal */
|
||||
export function DifferencePlainDateTimeWithTotal(isoDateTime1: ISODateTimeRecord, isoDateTime2: ISODateTimeRecord, calendar: CalendarType, unit: TemporalUnit): PlainCompletion<number> {
|
||||
if (CompareISODateTime(isoDateTime1, isoDateTime2) === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (!ISODateTimeWithinLimits(isoDateTime1) || !ISODateTimeWithinLimits(isoDateTime2)) {
|
||||
return Throw.RangeError('PlainDateTime outside of range');
|
||||
}
|
||||
const diff = DifferenceISODateTime(isoDateTime1, isoDateTime2, calendar, unit);
|
||||
if (unit === TemporalUnit.Nanosecond) {
|
||||
return diff.Time;
|
||||
}
|
||||
const originEpochNs = GetUTCEpochNanoseconds(isoDateTime1);
|
||||
const destEpochNs = GetUTCEpochNanoseconds(isoDateTime2);
|
||||
return TotalRelativeDuration(diff, originEpochNs, destEpochNs, isoDateTime1, undefined, calendar, unit);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-differencetemporalplaindatetime */
|
||||
export function* DifferenceTemporalPlainDateTime(operation: 'since' | 'until', dateTime: TemporalPlainDateTimeObject, _other: Value, options: Value): ValueEvaluator<TemporalDurationObject> {
|
||||
const other = Q(yield* ToTemporalDateTime(_other));
|
||||
if (!CalendarEquals(dateTime.Calendar, other.Calendar)) {
|
||||
return Throw.RangeError('Calendars are not equal');
|
||||
}
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
const settings = Q(yield* GetDifferenceSettings(operation, resolvedOptions, 'datetime', [], TemporalUnit.Nanosecond, TemporalUnit.Day));
|
||||
if (CompareISODateTime(dateTime.ISODateTime, other.ISODateTime) === 0) {
|
||||
return X(CreateTemporalDuration(0, 0, 0, 0, 0, 0, 0, 0, 0, 0));
|
||||
}
|
||||
const internalDuration = Q(DifferencePlainDateTimeWithRounding(dateTime.ISODateTime, other.ISODateTime, dateTime.Calendar, settings.LargestUnit, settings.RoundingIncrement, settings.SmallestUnit, settings.RoundingMode));
|
||||
let result = X(TemporalDurationFromInternal(internalDuration, settings.LargestUnit));
|
||||
if (operation === 'since') {
|
||||
result = CreateNegatedTemporalDuration(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-adddurationtodatetime */
|
||||
export function* AddDurationToDateTime(operation: 'add' | 'subtract', dateTime: TemporalPlainDateTimeObject, temporalDurationLike: Value, options: Value): ValueEvaluator<TemporalPlainDateTimeObject> {
|
||||
let duration = Q(yield* ToTemporalDuration(temporalDurationLike));
|
||||
if (operation === 'subtract') {
|
||||
duration = CreateNegatedTemporalDuration(duration);
|
||||
}
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
const overflow = Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
const internalDuration = ToInternalDurationRecordWith24HourDays(duration);
|
||||
const timeResult = AddTime(dateTime.ISODateTime.Time, internalDuration.Time);
|
||||
const dateDuration = Q(AdjustDateDurationRecord(internalDuration.Date, timeResult.Days));
|
||||
const addedDate = Q(CalendarDateAdd(dateTime.Calendar, dateTime.ISODateTime.ISODate, dateDuration, overflow));
|
||||
const result = CombineISODateAndTimeRecord(addedDate, timeResult);
|
||||
return Q(yield* CreateTemporalDateTime(result, dateTime.Calendar));
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import type { TemporalDurationObject } from '../../intrinsics/Temporal/Duration.mts';
|
||||
import { type ISODateRecord, type TemporalPlainDateObject, isTemporalPlainDateObject } from '../../intrinsics/Temporal/PlainDate.mts';
|
||||
import { isTemporalPlainDateTimeObject } from '../../intrinsics/Temporal/PlainDateTime.mts';
|
||||
import { isTemporalZonedDateTimeObject } from '../../intrinsics/Temporal/ZonedDateTime.mts';
|
||||
import { ParseISODateTime } from '../../parser/TemporalParser.mts';
|
||||
import { abs } from '../math.mts';
|
||||
import { GetOptionsObject, GetUTCEpochNanoseconds, ToZeroPaddedDecimalString } from './addition.mts';
|
||||
import {
|
||||
Assert, type CalendarType, type FunctionObject, type ValueEvaluator, Throw, surroundingAgent, Q, OrdinaryCreateFromConstructor, type Mutable, Value, ObjectValue, GetTemporalOverflowOption, X, GetISODateTimeFor, GetTemporalCalendarIdentifierWithISODefault, PrepareCalendarFields, CalendarDateFromFields, JSStringValue, CanonicalizeCalendar, CalendarISOToDate, type PlainCompletion, ISODaysInMonth, ISODateToEpochDays, EpochDaysToEpochMs, EpochTimeToEpochYear, EpochTimeToMonthInYear, EpochTimeToDate, FormatCalendarAnnotation, CalendarEquals, GetDifferenceSettings, TemporalUnit, CreateTemporalDuration, CalendarDateUntil, type DateUnit, CombineDateAndTimeDuration, type TimeDuration, RoundRelativeDuration, TemporalDurationFromInternal, CreateNegatedTemporalDuration, ToTemporalDuration, ToDateDurationRecordWithoutTime, CalendarDateAdd,
|
||||
BalanceISOYearMonth,
|
||||
MidnightTimeRecord,
|
||||
NoonTimeRecord,
|
||||
CombineISODateAndTimeRecord,
|
||||
ISODateTimeWithinLimits,
|
||||
} from '#self';
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-create-iso-date-record */
|
||||
export function CreateISODateRecord(y: number, m: number, d: number): ISODateRecord {
|
||||
Assert(IsValidISODate(y, m, d));
|
||||
return { Year: y, Month: m, Day: d };
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-createtemporaldate */
|
||||
export function* CreateTemporalDate(isoDate: ISODateRecord, calendar: CalendarType, NewTarget?: FunctionObject): ValueEvaluator<TemporalPlainDateObject> {
|
||||
if (!ISODateWithinLimits(isoDate)) {
|
||||
return Throw.RangeError('$1-$2-$3 is not a valid date', isoDate.Year, isoDate.Month, isoDate.Day);
|
||||
}
|
||||
if (NewTarget === undefined) {
|
||||
NewTarget = surroundingAgent.intrinsic('%Temporal.PlainDate%');
|
||||
}
|
||||
const object = Q(yield* OrdinaryCreateFromConstructor(NewTarget, '%Temporal.PlainDate.prototype%', [
|
||||
'InitializedTemporalDate',
|
||||
'ISODate',
|
||||
'Calendar',
|
||||
])) as Mutable<TemporalPlainDateObject>;
|
||||
object.ISODate = isoDate;
|
||||
object.Calendar = calendar;
|
||||
return object;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-totemporaldate */
|
||||
export function* ToTemporalDate(item: Value, options: Value = Value.undefined): ValueEvaluator<TemporalPlainDateObject> {
|
||||
if (item instanceof ObjectValue) {
|
||||
if (isTemporalPlainDateObject(item)) {
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
return X(CreateTemporalDate(item.ISODate, item.Calendar));
|
||||
}
|
||||
if (isTemporalZonedDateTimeObject(item)) {
|
||||
const isoDateTime = GetISODateTimeFor(item.TimeZone, item.EpochNanoseconds);
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
return X(CreateTemporalDate(isoDateTime.ISODate, item.Calendar));
|
||||
}
|
||||
if (isTemporalPlainDateTimeObject(item)) {
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
return X(CreateTemporalDate(item.ISODateTime.ISODate, item.Calendar));
|
||||
}
|
||||
const calendar = Q(yield* GetTemporalCalendarIdentifierWithISODefault(item));
|
||||
const fields = Q(yield* PrepareCalendarFields(calendar, item, ['year', 'month', 'month-code', 'day'], [], []));
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
const overflow = Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
const isoDate = Q(yield* CalendarDateFromFields(calendar, fields, overflow));
|
||||
return X(CreateTemporalDate(isoDate, calendar));
|
||||
}
|
||||
if (!(item instanceof JSStringValue)) {
|
||||
return Throw.TypeError('$1 is not a string', item);
|
||||
}
|
||||
const result = Q(ParseISODateTime(item.stringValue(), ['TemporalDateTimeString[~Zoned]']));
|
||||
const calendar = result.Calendar ?? 'iso8601';
|
||||
const calendarType = Q(CanonicalizeCalendar(calendar));
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
const isoDate = CreateISODateRecord(result.Year!, result.Month, result.Day);
|
||||
return X(CreateTemporalDate(isoDate, calendarType));
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-comparesurpasses */
|
||||
export function CompareSurpasses(sign: 1 | -1, year: number, monthOrCode: number | string, day: number, target: { Year: number; Month: number; MonthCode: string; Day: number }): boolean {
|
||||
if (year !== target.Year) {
|
||||
if (sign * (year - target.Year) > 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (typeof monthOrCode === 'string' && monthOrCode !== target.MonthCode) {
|
||||
if (sign > 0) {
|
||||
// If monthOrCode is lexicographically greater than target.[[MonthCode]], return true.
|
||||
if (monthOrCode > target.MonthCode) {
|
||||
return true;
|
||||
}
|
||||
} else if (target.MonthCode > monthOrCode) {
|
||||
// If target.[[MonthCode]] is lexicographically greater than monthOrCode, return true.
|
||||
return true;
|
||||
}
|
||||
} else if (typeof monthOrCode === 'number' && monthOrCode !== target.Month) {
|
||||
if (sign * (monthOrCode - target.Month) > 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (day !== target.Day) {
|
||||
if (sign * (day - target.Day) > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-isodatesurpasses */
|
||||
export function ISODateSurpasses(sign: 1 | -1, baseDate: ISODateRecord, isoDate2: ISODateRecord, years: number, month: number, weeks: number, days: number): boolean {
|
||||
const parts = CalendarISOToDate('iso8601', baseDate);
|
||||
const target = CalendarISOToDate('iso8601', isoDate2);
|
||||
const y0 = parts.Year + years;
|
||||
if (CompareSurpasses(sign, y0, parts.MonthCode, parts.Day, target)) {
|
||||
return true;
|
||||
}
|
||||
if (month === 0) {
|
||||
return false;
|
||||
}
|
||||
const m0 = parts.Month + month;
|
||||
const monthsAdded = BalanceISOYearMonth(y0, m0);
|
||||
if (CompareSurpasses(sign, monthsAdded.Year, monthsAdded.Month, parts.Day, target)) {
|
||||
return true;
|
||||
}
|
||||
if (weeks === 0 && days === 0) {
|
||||
return false;
|
||||
}
|
||||
const regulatedDate = X(RegulateISODate(monthsAdded.Year, monthsAdded.Month, parts.Day, 'constrain'));
|
||||
const daysInWeek = 7;
|
||||
const balancedDate = AddDaysToISODate(regulatedDate, daysInWeek * weeks + days);
|
||||
return CompareSurpasses(sign, balancedDate.Year, balancedDate.Month, balancedDate.Day, target);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-regulateisodate */
|
||||
export function RegulateISODate(year: number, month: number, day: number, overflow: 'constrain' | 'reject'): PlainCompletion<ISODateRecord> {
|
||||
if (overflow === 'constrain') {
|
||||
month = Math.max(1, Math.min(12, month));
|
||||
const daysInMonth = ISODaysInMonth(year, month);
|
||||
day = Math.max(1, Math.min(daysInMonth, day));
|
||||
} else {
|
||||
Assert(overflow === 'reject');
|
||||
if (!IsValidISODate(year, month, day)) {
|
||||
return Throw.RangeError('$1-$2-$3 is not a valid date', year, month, day);
|
||||
}
|
||||
}
|
||||
return CreateISODateRecord(year, month, day);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-isvalidisodate */
|
||||
export function IsValidISODate(year: number, month: number, day: number): boolean {
|
||||
if (month < 1 || month > 12) {
|
||||
return false;
|
||||
}
|
||||
const daysInMonth = ISODaysInMonth(year, month);
|
||||
if (day < 1 || day > daysInMonth) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-adddaystoisodate */
|
||||
export function AddDaysToISODate(isoDate: ISODateRecord, days: number): ISODateRecord {
|
||||
const epochDays = ISODateToEpochDays(isoDate.Year, isoDate.Month - 1, isoDate.Day) + days;
|
||||
const ms = EpochDaysToEpochMs(epochDays, 0);
|
||||
return CreateISODateRecord(EpochTimeToEpochYear(ms), EpochTimeToMonthInYear(ms) + 1, EpochTimeToDate(ms));
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-padisoyear */
|
||||
export function PadISOYear(y: number): string {
|
||||
if (y >= 0 && y <= 9999) {
|
||||
return ToZeroPaddedDecimalString(y, 4);
|
||||
}
|
||||
const yearSign = y > 0 ? '+' : '-';
|
||||
const year = ToZeroPaddedDecimalString(abs(y), 6);
|
||||
return yearSign + year;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-temporaldatetostring */
|
||||
export function TemporalDateToString(temporalDate: TemporalPlainDateObject, showCalendar: 'auto' | 'always' | 'never' | 'critical'): string {
|
||||
const year = PadISOYear(temporalDate.ISODate.Year);
|
||||
const month = ToZeroPaddedDecimalString(temporalDate.ISODate.Month, 2);
|
||||
const day = ToZeroPaddedDecimalString(temporalDate.ISODate.Day, 2);
|
||||
const calendar = FormatCalendarAnnotation(temporalDate.Calendar, showCalendar);
|
||||
return `${year}-${month}-${day}${calendar}`;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-isodatewithinlimits */
|
||||
export function ISODateWithinLimits(isoDate: ISODateRecord): boolean {
|
||||
const isoDateTime = CombineISODateAndTimeRecord(isoDate, NoonTimeRecord());
|
||||
return ISODateTimeWithinLimits(isoDateTime);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-compareisodate */
|
||||
export function CompareISODate(isoDate1: ISODateRecord, isoDate2: ISODateRecord): 1 | -1 | 0 {
|
||||
if (isoDate1.Year > isoDate2.Year) return 1;
|
||||
if (isoDate1.Year < isoDate2.Year) return -1;
|
||||
if (isoDate1.Month > isoDate2.Month) return 1;
|
||||
if (isoDate1.Month < isoDate2.Month) return -1;
|
||||
if (isoDate1.Day > isoDate2.Day) return 1;
|
||||
if (isoDate1.Day < isoDate2.Day) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-differencetemporalplaindate */
|
||||
export function* DifferenceTemporalPlainDate(operation: 'since' | 'until', temporalDate: TemporalPlainDateObject, _other: Value, options: Value): ValueEvaluator<TemporalDurationObject> {
|
||||
const other = Q(yield* ToTemporalDate(_other));
|
||||
if (!CalendarEquals(temporalDate.Calendar, other.Calendar)) {
|
||||
return Throw.RangeError('Calendars are not equal');
|
||||
}
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
const settings = Q(yield* GetDifferenceSettings(operation, resolvedOptions, 'date', [], TemporalUnit.Day, TemporalUnit.Day));
|
||||
if (CompareISODate(temporalDate.ISODate, other.ISODate) === 0) {
|
||||
return X(CreateTemporalDuration(0, 0, 0, 0, 0, 0, 0, 0, 0, 0));
|
||||
}
|
||||
const dateDifference = CalendarDateUntil(temporalDate.Calendar, temporalDate.ISODate, other.ISODate, settings.LargestUnit as DateUnit);
|
||||
let duration = CombineDateAndTimeDuration(dateDifference, 0 as TimeDuration);
|
||||
if (settings.SmallestUnit !== TemporalUnit.Day || settings.RoundingIncrement !== 1) {
|
||||
const isoDateTime = CombineISODateAndTimeRecord(temporalDate.ISODate, MidnightTimeRecord());
|
||||
const originEpochNs = GetUTCEpochNanoseconds(isoDateTime);
|
||||
const isoDateTimeOther = CombineISODateAndTimeRecord(other.ISODate, MidnightTimeRecord());
|
||||
const destEpochNs = GetUTCEpochNanoseconds(isoDateTimeOther);
|
||||
duration = Q(RoundRelativeDuration(duration, originEpochNs, destEpochNs, isoDateTime, undefined, temporalDate.Calendar, settings.LargestUnit, settings.RoundingIncrement, settings.SmallestUnit, settings.RoundingMode));
|
||||
}
|
||||
let result = X(TemporalDurationFromInternal(duration, TemporalUnit.Day));
|
||||
if (operation === 'since') {
|
||||
result = CreateNegatedTemporalDuration(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-adddurationtodate */
|
||||
export function* AddDurationToDate(operation: 'add' | 'subtract', temporalDate: TemporalPlainDateObject, temporalDurationLike: Value, options: Value): ValueEvaluator<TemporalPlainDateObject> {
|
||||
const calendar = temporalDate.Calendar;
|
||||
let duration = Q(yield* ToTemporalDuration(temporalDurationLike));
|
||||
if (operation === 'subtract') {
|
||||
duration = CreateNegatedTemporalDuration(duration);
|
||||
}
|
||||
const dateDuration = ToDateDurationRecordWithoutTime(duration);
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
const overflow = Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
const result = Q(CalendarDateAdd(calendar, temporalDate.ISODate, dateDuration, overflow));
|
||||
return X(CreateTemporalDate(result, calendar));
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { ISODateRecord } from '../../intrinsics/Temporal/PlainDate.mts';
|
||||
import { type TemporalPlainMonthDayObject, isTemporalPlainMonthDayObject } from '../../intrinsics/Temporal/PlainMonthDay.mts';
|
||||
import { ParseISODateTime } from '../../parser/TemporalParser.mts';
|
||||
import { GetOptionsObject, ToZeroPaddedDecimalString } from './addition.mts';
|
||||
import {
|
||||
Value, type ValueEvaluator, ObjectValue, Q, GetTemporalOverflowOption, X, GetTemporalCalendarIdentifierWithISODefault, PrepareCalendarFields, CalendarMonthDayFromFields, JSStringValue, Throw, CanonicalizeCalendar, CreateISODateRecord, ISODateWithinLimits, ISODateToFields, type CalendarType, type FunctionObject, surroundingAgent, OrdinaryCreateFromConstructor, type Mutable, PadISOYear, FormatCalendarAnnotation,
|
||||
} from '#self';
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-totemporalmonthday */
|
||||
export function* ToTemporalMonthDay(
|
||||
item: Value,
|
||||
options: Value = Value.undefined,
|
||||
): ValueEvaluator<TemporalPlainMonthDayObject> {
|
||||
if (item instanceof ObjectValue) {
|
||||
if (isTemporalPlainMonthDayObject(item)) {
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
return X(CreateTemporalMonthDay(item.ISODate, item.Calendar));
|
||||
}
|
||||
const calendar = Q(yield* GetTemporalCalendarIdentifierWithISODefault(item));
|
||||
const fields = Q(yield* PrepareCalendarFields(calendar, item, ['year', 'month', 'month-code', 'day'], [], []));
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
const overflow = Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
const isoDate = Q(yield* CalendarMonthDayFromFields(calendar, fields, overflow));
|
||||
return X(CreateTemporalMonthDay(isoDate, calendar));
|
||||
}
|
||||
if (!(item instanceof JSStringValue)) {
|
||||
return Throw.TypeError('$1 is not a string', item);
|
||||
}
|
||||
const result = Q(ParseISODateTime(item.stringValue(), ['TemporalMonthDayString']));
|
||||
const calendar = result.Calendar ?? 'iso8601';
|
||||
const calendarType = Q(CanonicalizeCalendar(calendar));
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
if (calendarType === 'iso8601') {
|
||||
const referenceISOYear = 1972;
|
||||
const isoDate = CreateISODateRecord(referenceISOYear, result.Month, result.Day);
|
||||
return X(CreateTemporalMonthDay(isoDate, calendarType));
|
||||
}
|
||||
let isoDate = CreateISODateRecord(result.Year!, result.Month, result.Day);
|
||||
if (!ISODateWithinLimits(isoDate)) {
|
||||
return Throw.RangeError('PlainMonthDay out of range');
|
||||
}
|
||||
const result2 = Q(ISODateToFields(calendarType, isoDate, 'month-day'));
|
||||
isoDate = Q(yield* CalendarMonthDayFromFields(calendarType, result2, 'constrain'));
|
||||
return X(CreateTemporalMonthDay(isoDate, calendarType));
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-createtemporalmonthday */
|
||||
export function* CreateTemporalMonthDay(
|
||||
isoDate: ISODateRecord,
|
||||
calendar: CalendarType,
|
||||
newTarget?: FunctionObject,
|
||||
): ValueEvaluator<TemporalPlainMonthDayObject> {
|
||||
if (!ISODateWithinLimits(isoDate)) {
|
||||
return Throw.RangeError('PlainMonthDay out of range');
|
||||
}
|
||||
if (newTarget === undefined) {
|
||||
newTarget = surroundingAgent.intrinsic('%Temporal.PlainMonthDay%');
|
||||
}
|
||||
const object = Q(yield* OrdinaryCreateFromConstructor(newTarget, '%Temporal.PlainMonthDay.prototype%', [
|
||||
'InitializedTemporalMonthDay',
|
||||
'ISODate',
|
||||
'Calendar',
|
||||
])) as Mutable<TemporalPlainMonthDayObject>;
|
||||
object.ISODate = isoDate;
|
||||
object.Calendar = calendar;
|
||||
return object;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-temporalmonthdaytostring */
|
||||
export function TemporalMonthDayToString(
|
||||
monthDay: TemporalPlainMonthDayObject,
|
||||
showCalendar: 'auto' | 'always' | 'never' | 'critical',
|
||||
): string {
|
||||
const month = ToZeroPaddedDecimalString(monthDay.ISODate.Month, 2);
|
||||
const day = ToZeroPaddedDecimalString(monthDay.ISODate.Day, 2);
|
||||
let result = `${month}-${day}`;
|
||||
if ((showCalendar === 'always' || showCalendar === 'critical') || monthDay.Calendar !== 'iso8601') {
|
||||
const year = PadISOYear(monthDay.ISODate.Year);
|
||||
result = `${year}-${result}`;
|
||||
}
|
||||
const calendarString = FormatCalendarAnnotation(monthDay.Calendar, showCalendar);
|
||||
return result + calendarString;
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
import type { TemporalDurationObject } from '../../intrinsics/Temporal/Duration.mts';
|
||||
import { isTemporalPlainDateTimeObject } from '../../intrinsics/Temporal/PlainDateTime.mts';
|
||||
import { type TemporalPlainTimeObject, isTemporalPlainTimeObject } from '../../intrinsics/Temporal/PlainTime.mts';
|
||||
import { isTemporalZonedDateTimeObject } from '../../intrinsics/Temporal/ZonedDateTime.mts';
|
||||
import { ParseISODateTime } from '../../parser/TemporalParser.mts';
|
||||
import { abs } from '../math.mts';
|
||||
import { GetOptionsObject, type RoundingMode } from './addition.mts';
|
||||
import {
|
||||
Assert, type TimeDuration, TimeDurationFromComponents, nsPerDay, Value, type ValueEvaluator, ObjectValue, Q, GetTemporalOverflowOption, X, GetISODateTimeFor, JSStringValue, Throw, type PlainEvaluator, UndefinedValue, type PlainCompletion, type FunctionObject, surroundingAgent, OrdinaryCreateFromConstructor, type Mutable, Get, ToIntegerWithTruncation, FormatTimeString, type TimeUnit, TemporalUnit, Table21_LengthInNanoSeconds, RoundNumberToIncrement, GetDifferenceSettings, RoundTimeDuration, CombineDateAndTimeDuration, ZeroDateDuration, TemporalDurationFromInternal, CreateNegatedTemporalDuration, ToTemporalDuration, ToInternalDurationRecord,
|
||||
} from '#self';
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-time-records */
|
||||
export interface TimeRecord {
|
||||
readonly Days: number;
|
||||
readonly Hour: number;
|
||||
readonly Minute: number;
|
||||
readonly Second: number;
|
||||
readonly Millisecond: number;
|
||||
readonly Microsecond: number;
|
||||
readonly Nanosecond: number;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-createtimerecord */
|
||||
export function CreateTimeRecord(hour: number, minute: number, second: number, millisecond: number, microsecond: number, nanosecond: number, deltaDays = 0): TimeRecord {
|
||||
Assert(IsValidTime(hour, minute, second, millisecond, microsecond, nanosecond));
|
||||
return {
|
||||
Days: deltaDays,
|
||||
Hour: hour,
|
||||
Minute: minute,
|
||||
Second: second,
|
||||
Millisecond: millisecond,
|
||||
Microsecond: microsecond,
|
||||
Nanosecond: nanosecond,
|
||||
};
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-midnighttimerecord */
|
||||
export function MidnightTimeRecord(): TimeRecord {
|
||||
return {
|
||||
Days: 0,
|
||||
Hour: 0,
|
||||
Minute: 0,
|
||||
Second: 0,
|
||||
Millisecond: 0,
|
||||
Microsecond: 0,
|
||||
Nanosecond: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-noontimerecord */
|
||||
export function NoonTimeRecord(): TimeRecord {
|
||||
return {
|
||||
Days: 0,
|
||||
Hour: 12,
|
||||
Minute: 0,
|
||||
Second: 0,
|
||||
Millisecond: 0,
|
||||
Microsecond: 0,
|
||||
Nanosecond: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-differencetime */
|
||||
export function DifferenceTime(time1: TimeRecord, time2: TimeRecord): TimeDuration {
|
||||
const hours = time2.Hour - time1.Hour;
|
||||
const minutes = time2.Minute - time1.Minute;
|
||||
const seconds = time2.Second - time1.Second;
|
||||
const milliseconds = time2.Millisecond - time1.Millisecond;
|
||||
const microseconds = time2.Microsecond - time1.Microsecond;
|
||||
const nanoseconds = time2.Nanosecond - time1.Nanosecond;
|
||||
const timeDuration = TimeDurationFromComponents(hours, minutes, seconds, milliseconds, microseconds, nanoseconds);
|
||||
Assert(abs(timeDuration) < nsPerDay);
|
||||
return timeDuration;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-totemporaltime */
|
||||
export function* ToTemporalTime(item: Value, options: Value = Value.undefined): ValueEvaluator<TemporalPlainTimeObject> {
|
||||
let result;
|
||||
if (item instanceof ObjectValue) {
|
||||
if (isTemporalPlainTimeObject(item)) {
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
return X(CreateTemporalTime(item.Time));
|
||||
}
|
||||
if (isTemporalPlainDateTimeObject(item)) {
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
return X(CreateTemporalTime(item.ISODateTime.Time));
|
||||
}
|
||||
if (isTemporalZonedDateTimeObject(item)) {
|
||||
const isoDateTime = GetISODateTimeFor(item.TimeZone, item.EpochNanoseconds);
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
return X(CreateTemporalTime(isoDateTime.Time));
|
||||
}
|
||||
const result2 = Q(yield* ToTemporalTimeRecord(item));
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
const overflow = Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
result = Q(RegulateTime(result2.Hour!, result2.Minute!, result2.Second!, result2.Millisecond!, result2.Microsecond!, result2.Nanosecond!, overflow));
|
||||
} else {
|
||||
if (!(item instanceof JSStringValue)) {
|
||||
return Throw.TypeError('Invalid time string $1', item);
|
||||
}
|
||||
const parseResult = Q(ParseISODateTime(item.stringValue(), ['TemporalTimeString']));
|
||||
Assert(parseResult.Time !== 'start-of-day');
|
||||
result = parseResult.Time;
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
}
|
||||
return X(CreateTemporalTime(result));
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-totimerecordormidnight */
|
||||
export function* ToTimeRecordOrMidnight(item: Value): PlainEvaluator<TimeRecord> {
|
||||
if (item instanceof UndefinedValue) {
|
||||
return MidnightTimeRecord();
|
||||
}
|
||||
const plainTime = Q(yield* ToTemporalTime(item));
|
||||
return plainTime.Time;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-regulatetime */
|
||||
export function RegulateTime(hour: number, minute: number, second: number, millisecond: number, microsecond: number, nanosecond: number, overflow: 'constrain' | 'reject'): PlainCompletion<TimeRecord> {
|
||||
if (overflow === 'constrain') {
|
||||
hour = Math.max(0, Math.min(23, hour));
|
||||
minute = Math.max(0, Math.min(59, minute));
|
||||
second = Math.max(0, Math.min(59, second));
|
||||
millisecond = Math.max(0, Math.min(999, millisecond));
|
||||
microsecond = Math.max(0, Math.min(999, microsecond));
|
||||
nanosecond = Math.max(0, Math.min(999, nanosecond));
|
||||
} else {
|
||||
Assert(overflow === 'reject');
|
||||
if (!IsValidTime(hour, minute, second, millisecond, microsecond, nanosecond)) {
|
||||
return Throw.RangeError('Invalid time');
|
||||
}
|
||||
}
|
||||
return CreateTimeRecord(hour, minute, second, millisecond, microsecond, nanosecond);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-isvalidtime */
|
||||
export function IsValidTime(hour: number, minute: number, second: number, millisecond: number, microsecond: number, nanosecond: number): boolean {
|
||||
if (hour < 0 || hour > 23) return false;
|
||||
if (minute < 0 || minute > 59) return false;
|
||||
if (second < 0 || second > 59) return false;
|
||||
if (millisecond < 0 || millisecond > 999) return false;
|
||||
if (microsecond < 0 || microsecond > 999) return false;
|
||||
if (nanosecond < 0 || nanosecond > 999) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-balancetime */
|
||||
export function BalanceTime(hour: number, minute: number, second: number, millisecond: number, microsecond: number, nanosecond: number): TimeRecord {
|
||||
microsecond += Math.floor(nanosecond / 1000);
|
||||
nanosecond %= 1000;
|
||||
millisecond += Math.floor(microsecond / 1000);
|
||||
microsecond %= 1000;
|
||||
second += Math.floor(millisecond / 1000);
|
||||
millisecond %= 1000;
|
||||
minute += Math.floor(second / 60);
|
||||
second %= 60;
|
||||
hour += Math.floor(minute / 60);
|
||||
minute %= 60;
|
||||
const deltaDays = Math.floor(hour / 24);
|
||||
hour %= 24;
|
||||
return CreateTimeRecord(hour, minute, second, millisecond, microsecond, nanosecond, deltaDays);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-createtemporaltime */
|
||||
export function* CreateTemporalTime(time: TimeRecord, newTarget?: FunctionObject): ValueEvaluator<TemporalPlainTimeObject> {
|
||||
if (newTarget === undefined) {
|
||||
newTarget = surroundingAgent.intrinsic('%Temporal.PlainTime%');
|
||||
}
|
||||
const object = Q(yield* OrdinaryCreateFromConstructor(newTarget, '%Temporal.PlainTime.prototype%', [
|
||||
'InitializedTemporalTime',
|
||||
'Time',
|
||||
])) as Mutable<TemporalPlainTimeObject>;
|
||||
object.Time = time;
|
||||
return object;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#table-temporal-temporaltimelike-record-fields */
|
||||
export interface TemporalTimeLike {
|
||||
Hour: number | undefined;
|
||||
Minute: number | undefined;
|
||||
Second: number | undefined;
|
||||
Millisecond: number | undefined;
|
||||
Microsecond: number | undefined;
|
||||
Nanosecond: number | undefined;
|
||||
}
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-totemporaltimerecord */
|
||||
export function* ToTemporalTimeRecord(temporalTimeLike: ObjectValue, completeness: 'partial' | 'complete' = 'complete'): PlainEvaluator<TemporalTimeLike> {
|
||||
const result: Mutable<TemporalTimeLike> = {
|
||||
Hour: undefined,
|
||||
Minute: undefined,
|
||||
Second: undefined,
|
||||
Millisecond: undefined,
|
||||
Microsecond: undefined,
|
||||
Nanosecond: undefined,
|
||||
};
|
||||
if (completeness === 'complete') {
|
||||
result.Hour = 0;
|
||||
result.Minute = 0;
|
||||
result.Second = 0;
|
||||
result.Millisecond = 0;
|
||||
result.Microsecond = 0;
|
||||
result.Nanosecond = 0;
|
||||
}
|
||||
let any = false;
|
||||
const hour = Q(yield* Get(temporalTimeLike, Value('hour')));
|
||||
if (!(hour instanceof UndefinedValue)) {
|
||||
result.Hour = Q(yield* ToIntegerWithTruncation(hour));
|
||||
any = true;
|
||||
}
|
||||
const microsecond = Q(yield* Get(temporalTimeLike, Value('microsecond')));
|
||||
if (!(microsecond instanceof UndefinedValue)) {
|
||||
result.Microsecond = Q(yield* ToIntegerWithTruncation(microsecond));
|
||||
any = true;
|
||||
}
|
||||
const millisecond = Q(yield* Get(temporalTimeLike, Value('millisecond')));
|
||||
if (!(millisecond instanceof UndefinedValue)) {
|
||||
result.Millisecond = Q(yield* ToIntegerWithTruncation(millisecond));
|
||||
any = true;
|
||||
}
|
||||
const minute = Q(yield* Get(temporalTimeLike, Value('minute')));
|
||||
if (!(minute instanceof UndefinedValue)) {
|
||||
result.Minute = Q(yield* ToIntegerWithTruncation(minute));
|
||||
any = true;
|
||||
}
|
||||
const nanosecond = Q(yield* Get(temporalTimeLike, Value('nanosecond')));
|
||||
if (!(nanosecond instanceof UndefinedValue)) {
|
||||
result.Nanosecond = Q(yield* ToIntegerWithTruncation(nanosecond));
|
||||
any = true;
|
||||
}
|
||||
const second = Q(yield* Get(temporalTimeLike, Value('second')));
|
||||
if (!(second instanceof UndefinedValue)) {
|
||||
result.Second = Q(yield* ToIntegerWithTruncation(second));
|
||||
any = true;
|
||||
}
|
||||
if (!any) {
|
||||
return Throw.TypeError('$1 does not look like a TemporalTimeLike object', temporalTimeLike);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-timerecordtostring */
|
||||
export function TimeRecordToString(time: TimeRecord, precision: number | 'minute' | 'auto'): string {
|
||||
const subSecondNanoseconds = time.Millisecond * 1e6 + time.Microsecond * 1e3 + time.Nanosecond;
|
||||
return FormatTimeString(time.Hour, time.Minute, time.Second, subSecondNanoseconds, precision);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-comparetimerecord */
|
||||
export function CompareTimeRecord(time1: TimeRecord, time2: TimeRecord): -1 | 0 | 1 {
|
||||
if (time1.Hour > time2.Hour) return 1;
|
||||
if (time1.Hour < time2.Hour) return -1;
|
||||
if (time1.Minute > time2.Minute) return 1;
|
||||
if (time1.Minute < time2.Minute) return -1;
|
||||
if (time1.Second > time2.Second) return 1;
|
||||
if (time1.Second < time2.Second) return -1;
|
||||
if (time1.Millisecond > time2.Millisecond) return 1;
|
||||
if (time1.Millisecond < time2.Millisecond) return -1;
|
||||
if (time1.Microsecond > time2.Microsecond) return 1;
|
||||
if (time1.Microsecond < time2.Microsecond) return -1;
|
||||
if (time1.Nanosecond > time2.Nanosecond) return 1;
|
||||
if (time1.Nanosecond < time2.Nanosecond) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-addtime */
|
||||
export function AddTime(time: TimeRecord, timeDuration: TimeDuration): TimeRecord {
|
||||
return BalanceTime(time.Hour, time.Minute, time.Second, time.Millisecond, time.Microsecond, time.Nanosecond + Number(timeDuration));
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-roundtime */
|
||||
export function RoundTime(time: TimeRecord, increment: number, unit: TimeUnit | TemporalUnit.Day, roundingMode: RoundingMode): TimeRecord {
|
||||
let quantity: number;
|
||||
if (unit === TemporalUnit.Day || unit === TemporalUnit.Hour) {
|
||||
quantity = (((((time.Hour * 60 + time.Minute) * 60 + time.Second) * 1000 + time.Millisecond) * 1000 + time.Microsecond) * 1000 + time.Nanosecond);
|
||||
} else if (unit === TemporalUnit.Minute) {
|
||||
quantity = ((((time.Minute * 60 + time.Second) * 1000 + time.Millisecond) * 1000 + time.Microsecond) * 1000 + time.Nanosecond);
|
||||
} else if (unit === TemporalUnit.Second) {
|
||||
quantity = (((time.Second * 1000 + time.Millisecond) * 1000 + time.Microsecond) * 1000 + time.Nanosecond);
|
||||
} else if (unit === TemporalUnit.Millisecond) {
|
||||
quantity = ((time.Millisecond * 1000 + time.Microsecond) * 1000 + time.Nanosecond);
|
||||
} else if (unit === TemporalUnit.Microsecond) {
|
||||
quantity = time.Microsecond * 1000 + time.Nanosecond;
|
||||
} else {
|
||||
Assert(unit === TemporalUnit.Nanosecond);
|
||||
quantity = time.Nanosecond;
|
||||
}
|
||||
const unitLength = Table21_LengthInNanoSeconds[unit];
|
||||
const result = RoundNumberToIncrement(quantity, increment * unitLength, roundingMode) / unitLength;
|
||||
if (unit === TemporalUnit.Day) return CreateTimeRecord(0, 0, 0, 0, 0, 0, result);
|
||||
if (unit === TemporalUnit.Hour) return BalanceTime(result, 0, 0, 0, 0, 0);
|
||||
if (unit === TemporalUnit.Minute) return BalanceTime(time.Hour, result, 0, 0, 0, 0);
|
||||
if (unit === TemporalUnit.Second) return BalanceTime(time.Hour, time.Minute, result, 0, 0, 0);
|
||||
if (unit === TemporalUnit.Millisecond) return BalanceTime(time.Hour, time.Minute, time.Second, result, 0, 0);
|
||||
if (unit === TemporalUnit.Microsecond) return BalanceTime(time.Hour, time.Minute, time.Second, time.Millisecond, result, 0);
|
||||
Assert(unit === TemporalUnit.Nanosecond);
|
||||
return BalanceTime(time.Hour, time.Minute, time.Second, time.Millisecond, time.Microsecond, result);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-differencetemporalplaintime */
|
||||
export function* DifferenceTemporalPlainTime(operation: 'since' | 'until', temporalTime: TemporalPlainTimeObject, _other: Value, options: Value): ValueEvaluator<TemporalDurationObject> {
|
||||
const other = Q(yield* ToTemporalTime(_other));
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
const settings = Q(yield* GetDifferenceSettings(operation, resolvedOptions, 'time', [], TemporalUnit.Nanosecond, TemporalUnit.Hour));
|
||||
let timeDuration = DifferenceTime(temporalTime.Time, other.Time);
|
||||
// TODO(temporal): unsafe cast of settings.SmallestUnit
|
||||
timeDuration = X(RoundTimeDuration(timeDuration, settings.RoundingIncrement, settings.SmallestUnit as TimeUnit, settings.RoundingMode));
|
||||
const duration = CombineDateAndTimeDuration(ZeroDateDuration(), timeDuration);
|
||||
let result = X(TemporalDurationFromInternal(duration, settings.LargestUnit));
|
||||
if (operation === 'since') {
|
||||
result = CreateNegatedTemporalDuration(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-adddurationtotime */
|
||||
export function* AddDurationToTime(operation: 'add' | 'subtract', temporalTime: TemporalPlainTimeObject, temporalDurationLike: Value): ValueEvaluator<TemporalPlainTimeObject> {
|
||||
let duration = Q(yield* ToTemporalDuration(temporalDurationLike));
|
||||
if (operation === 'subtract') duration = CreateNegatedTemporalDuration(duration);
|
||||
const internalDuration = ToInternalDurationRecord(duration);
|
||||
const result = AddTime(temporalTime.Time, internalDuration.Time);
|
||||
return X(CreateTemporalTime(result));
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import type { TemporalDurationObject } from '../../intrinsics/Temporal/Duration.mts';
|
||||
import type { ISODateRecord } from '../../intrinsics/Temporal/PlainDate.mts';
|
||||
import { type TemporalPlainYearMonthObject, isTemporalPlainYearMonthObject, type ISOYearMonthRecord } from '../../intrinsics/Temporal/PlainYearMonth.mts';
|
||||
import { ParseISODateTime } from '../../parser/TemporalParser.mts';
|
||||
import { GetOptionsObject, GetUTCEpochNanoseconds, ToZeroPaddedDecimalString } from './addition.mts';
|
||||
import {
|
||||
Value, type ValueEvaluator, ObjectValue, Q, GetTemporalOverflowOption, X, GetTemporalCalendarIdentifierWithISODefault, PrepareCalendarFields, CalendarYearMonthFromFields, JSStringValue, Throw, CanonicalizeCalendar, CreateISODateRecord, ISODateToFields, type CalendarType, type FunctionObject, surroundingAgent, OrdinaryCreateFromConstructor, type Mutable, PadISOYear, FormatCalendarAnnotation, CalendarEquals, GetDifferenceSettings, TemporalUnit, CompareISODate, CreateTemporalDuration, CalendarDateFromFields, CalendarDateUntil, type DateUnit, AdjustDateDurationRecord, CombineDateAndTimeDuration, type TimeDuration, RoundRelativeDuration, TemporalDurationFromInternal, CreateNegatedTemporalDuration, ToTemporalDuration, ToInternalDurationRecord, CalendarDateAdd,
|
||||
CombineISODateAndTimeRecord,
|
||||
MidnightTimeRecord,
|
||||
} from '#self';
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-totemporalyearmonth */
|
||||
export function* ToTemporalYearMonth(
|
||||
item: Value,
|
||||
options: Value = Value.undefined,
|
||||
): ValueEvaluator<TemporalPlainYearMonthObject> {
|
||||
if (item instanceof ObjectValue) {
|
||||
if (isTemporalPlainYearMonthObject(item)) {
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
return X(CreateTemporalYearMonth(item.ISODate, item.Calendar));
|
||||
}
|
||||
const calendar = Q(yield* GetTemporalCalendarIdentifierWithISODefault(item));
|
||||
const fields = Q(yield* PrepareCalendarFields(calendar, item, ['year', 'month', 'month-code'], [], []));
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
const overflow = Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
const isoDate = Q(yield* CalendarYearMonthFromFields(calendar, fields, overflow));
|
||||
return X(CreateTemporalYearMonth(isoDate, calendar));
|
||||
}
|
||||
if (!(item instanceof JSStringValue)) {
|
||||
return Throw.TypeError('$1 is not a string', item);
|
||||
}
|
||||
const result = Q(ParseISODateTime(item.stringValue(), ['TemporalYearMonthString']));
|
||||
const calendar = result.Calendar ?? 'iso8601';
|
||||
const calendarType = Q(CanonicalizeCalendar(calendar));
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
let isoDate = CreateISODateRecord(result.Year!, result.Month, result.Day);
|
||||
if (!ISOYearMonthWithinLimits(isoDate)) {
|
||||
return Throw.RangeError('PlainYearMonth out of range');
|
||||
}
|
||||
const result2 = ISODateToFields(calendarType, isoDate, 'year-month');
|
||||
isoDate = Q(yield* CalendarYearMonthFromFields(calendarType, result2, 'constrain'));
|
||||
return X(CreateTemporalYearMonth(isoDate, calendarType));
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-isoyearmonthwithinlimits */
|
||||
export function ISOYearMonthWithinLimits(
|
||||
isoDate: ISODateRecord,
|
||||
): boolean {
|
||||
if (isoDate.Year < -271821 || isoDate.Year > 275760) return false;
|
||||
if (isoDate.Year === -271821 && isoDate.Month < 4) return false;
|
||||
if (isoDate.Year === 275760 && isoDate.Month > 9) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-balanceisoyearmonth */
|
||||
export function BalanceISOYearMonth(
|
||||
year: number,
|
||||
month: number,
|
||||
): ISOYearMonthRecord {
|
||||
year += Math.floor((month - 1) / 12);
|
||||
month = ((month - 1) % 12) + 1;
|
||||
return {
|
||||
Year: year,
|
||||
Month: month,
|
||||
};
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-createtemporalyearmonth */
|
||||
export function* CreateTemporalYearMonth(
|
||||
isoDate: ISODateRecord,
|
||||
calendar: CalendarType,
|
||||
newTarget?: FunctionObject,
|
||||
): ValueEvaluator<TemporalPlainYearMonthObject> {
|
||||
if (!ISOYearMonthWithinLimits(isoDate)) {
|
||||
return Throw.RangeError('PlainYearMonth out of range');
|
||||
}
|
||||
if (newTarget === undefined) {
|
||||
newTarget = surroundingAgent.intrinsic('%Temporal.PlainYearMonth%');
|
||||
}
|
||||
const object = Q(yield* OrdinaryCreateFromConstructor(newTarget, '%Temporal.PlainYearMonth.prototype%', [
|
||||
'InitializedTemporalYearMonth',
|
||||
'ISODate',
|
||||
'Calendar',
|
||||
])) as Mutable<TemporalPlainYearMonthObject>;
|
||||
object.ISODate = isoDate;
|
||||
object.Calendar = calendar;
|
||||
return object;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-temporalyearmonthtostring */
|
||||
export function TemporalYearMonthToString(
|
||||
yearMonth: TemporalPlainYearMonthObject,
|
||||
showCalendar: 'auto' | 'always' | 'never' | 'critical',
|
||||
): string {
|
||||
const year = PadISOYear(yearMonth.ISODate.Year);
|
||||
const month = ToZeroPaddedDecimalString(yearMonth.ISODate.Month, 2);
|
||||
let result = `${year}-${month}`;
|
||||
if (showCalendar === 'always' || showCalendar === 'critical' || yearMonth.Calendar !== 'iso8601') {
|
||||
const day = ToZeroPaddedDecimalString(yearMonth.ISODate.Day, 2);
|
||||
result = `${result}-${day}`;
|
||||
}
|
||||
const calendarString = FormatCalendarAnnotation(yearMonth.Calendar, showCalendar);
|
||||
return result + calendarString;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-differencetemporalplainyearmonth */
|
||||
export function* DifferenceTemporalPlainYearMonth(
|
||||
operation: 'since' | 'until',
|
||||
yearMonth: TemporalPlainYearMonthObject,
|
||||
_other: Value,
|
||||
options: Value,
|
||||
): ValueEvaluator<TemporalDurationObject> {
|
||||
const other = Q(yield* ToTemporalYearMonth(_other));
|
||||
const calendar = yearMonth.Calendar;
|
||||
if (!CalendarEquals(calendar, other.Calendar)) {
|
||||
return Throw.RangeError('PlainYearMonth calendars do not match');
|
||||
}
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
const settings = Q(yield* GetDifferenceSettings(
|
||||
operation,
|
||||
resolvedOptions,
|
||||
'date',
|
||||
[TemporalUnit.Week, TemporalUnit.Day],
|
||||
TemporalUnit.Month,
|
||||
TemporalUnit.Year,
|
||||
));
|
||||
if (CompareISODate(yearMonth.ISODate, other.ISODate) === 0) {
|
||||
return X(CreateTemporalDuration(0, 0, 0, 0, 0, 0, 0, 0, 0, 0));
|
||||
}
|
||||
const thisFields = ISODateToFields(calendar, yearMonth.ISODate, 'year-month');
|
||||
thisFields.Day = 1;
|
||||
const thisDate = Q(yield* CalendarDateFromFields(calendar, thisFields, 'constrain'));
|
||||
const otherFields = ISODateToFields(calendar, other.ISODate, 'year-month');
|
||||
otherFields.Day = 1;
|
||||
const otherDate = Q(yield* CalendarDateFromFields(calendar, otherFields, 'constrain'));
|
||||
// TODO(temporal): unsafe cast of settings.LargestUnit
|
||||
const dateDifference = CalendarDateUntil(calendar, thisDate, otherDate, settings.LargestUnit as DateUnit);
|
||||
const yearsMonthsDifference = X(AdjustDateDurationRecord(dateDifference, 0, 0));
|
||||
let duration = CombineDateAndTimeDuration(yearsMonthsDifference, 0 as TimeDuration);
|
||||
if (settings.SmallestUnit !== TemporalUnit.Month || settings.RoundingIncrement !== 1) {
|
||||
const isoDateTime = CombineISODateAndTimeRecord(thisDate, MidnightTimeRecord());
|
||||
const originEpochNs = GetUTCEpochNanoseconds(isoDateTime);
|
||||
const isoDateTimeOther = CombineISODateAndTimeRecord(otherDate, MidnightTimeRecord());
|
||||
const destEpochNs = GetUTCEpochNanoseconds(isoDateTimeOther);
|
||||
duration = Q(RoundRelativeDuration(
|
||||
duration,
|
||||
originEpochNs,
|
||||
destEpochNs,
|
||||
isoDateTime,
|
||||
undefined,
|
||||
calendar,
|
||||
settings.LargestUnit,
|
||||
settings.RoundingIncrement,
|
||||
settings.SmallestUnit,
|
||||
settings.RoundingMode,
|
||||
));
|
||||
}
|
||||
let result = X(TemporalDurationFromInternal(duration, TemporalUnit.Day));
|
||||
if (operation === 'since') {
|
||||
result = CreateNegatedTemporalDuration(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-adddurationtoyearmonth */
|
||||
export function* AddDurationToYearMonth(
|
||||
operation: 'add' | 'subtract',
|
||||
yearMonth: TemporalPlainYearMonthObject,
|
||||
temporalDurationLike: Value,
|
||||
options: Value,
|
||||
): ValueEvaluator<TemporalPlainYearMonthObject> {
|
||||
let duration = Q(yield* ToTemporalDuration(temporalDurationLike));
|
||||
if (operation === 'subtract') {
|
||||
duration = CreateNegatedTemporalDuration(duration);
|
||||
}
|
||||
const internalDuration = ToInternalDurationRecord(duration);
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
const overflow = Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
const durationToAdd = internalDuration.Date;
|
||||
if (durationToAdd.Weeks !== 0 || durationToAdd.Days !== 0 || internalDuration.Time !== 0) {
|
||||
return Throw.RangeError('Invalid duration');
|
||||
}
|
||||
const calendar = yearMonth.Calendar;
|
||||
const fields = ISODateToFields(calendar, yearMonth.ISODate, 'year-month');
|
||||
fields.Day = 1;
|
||||
const date = Q(yield* CalendarDateFromFields(calendar, fields, 'constrain'));
|
||||
const addedDate = Q(CalendarDateAdd(calendar, date, durationToAdd, overflow));
|
||||
const addedDateFields = ISODateToFields(calendar, addedDate, 'year-month');
|
||||
const isoDate = Q(yield* CalendarYearMonthFromFields(calendar, addedDateFields, overflow));
|
||||
return X(CreateTemporalYearMonth(isoDate, calendar));
|
||||
}
|
||||
@@ -0,0 +1,877 @@
|
||||
import { ParseDateTimeUTCOffset, ParseISODateTime } from '../../parser/TemporalParser.mts';
|
||||
import { R } from '../spec-types.mjs';
|
||||
import { type ISODateRecord, type TemporalPlainDateObject, isTemporalPlainDateObject } from '../../intrinsics/Temporal/PlainDate.mts';
|
||||
import { isTemporalPlainDateTimeObject } from '../../intrinsics/Temporal/PlainDateTime.mts';
|
||||
import { type TemporalZonedDateTimeObject, isTemporalZonedDateTimeObject } from '../../intrinsics/Temporal/ZonedDateTime.mts';
|
||||
import {
|
||||
GetOption, GetRoundingIncrementOption, GetRoundingModeOption, ToZeroPaddedDecimalString, UnsignedRoundingMode, type TimeZoneIdentifier,
|
||||
} from './addition.mts';
|
||||
import { RoundingMode } from './addition.mts';
|
||||
import {
|
||||
CalendarISOToDate, CanonicalizeCalendar, GetTemporalCalendarIdentifierWithISODefault, PrepareCalendarFields, type CalendarFieldsRecord, type CalendarType,
|
||||
} from './calendar.mts';
|
||||
import { ToTemporalTimeZoneIdentifier } from './time-zone.mts';
|
||||
import {
|
||||
ToPrimitive, ToNumber, Throw, CreateISODateRecord, CreateTemporalDate, CreateTemporalZonedDateTime, InterpretISODateTimeOffset, InterpretTemporalDateTimeFields, nsPerDay, type ISODateTimeMatchBehaviour, type ISODateTimeOffsetBehaviour,
|
||||
Value, ObjectValue, JSStringValue, NumberValue, UndefinedValue, Q, surroundingAgent, Get, ToString, type PlainCompletion, type PlainEvaluator, Assert, type PropertyKeyValue, X,
|
||||
} from '#self';
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-isodatetoepochdays */
|
||||
// TODO(temporal): Review
|
||||
export function ISODateToEpochDays(year: number, month: number, date: number): number {
|
||||
const resolvedYear = year + Math.floor(month / 12);
|
||||
const resolvedMonth = ((month % 12) + 12) % 12;
|
||||
// Find a time t such that EpochTimeToEpochYear(t) = resolvedYear, EpochTimeToMonthInYear(t) = resolvedMonth, and EpochTimeToDate(t) = 1.
|
||||
const y = resolvedYear;
|
||||
const m = resolvedMonth;
|
||||
let t = EpochDayNumberForYear(y);
|
||||
const isLeap = MathematicalDaysInYear(y) === 366;
|
||||
const monthDays = [
|
||||
31,
|
||||
isLeap ? 29 : 28,
|
||||
31, 30, 31, 30,
|
||||
31, 31, 30, 31, 30, 31,
|
||||
];
|
||||
for (let i = 0; i < m; i += 1) {
|
||||
t += monthDays[i];
|
||||
}
|
||||
Assert(EpochTimeToEpochYear(t) === resolvedYear && EpochTimeToMonthInYear(t) === resolvedMonth && EpochTimeToDate(t) === 1);
|
||||
|
||||
return EpochTimeToDayNumber(t) + date - 1;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-epochdaystoepochms */
|
||||
export function EpochDaysToEpochMs(day: number, time: number): number {
|
||||
return day * 86400000 + time;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#eqn-EpochTimeToDayNumber */
|
||||
export function EpochTimeToDayNumber(t: number): number {
|
||||
return Math.floor(t / 86400000);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-mathematicaldaysinyear */
|
||||
export function MathematicalDaysInYear(y: number): number {
|
||||
if (y % 4 !== 0) {
|
||||
return 365;
|
||||
}
|
||||
if (y % 100 !== 0) {
|
||||
return 366;
|
||||
}
|
||||
if (y % 400 !== 0) {
|
||||
return 365;
|
||||
}
|
||||
return 366;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-epochdaynumberforyear */
|
||||
export function EpochDayNumberForYear(y: number): number {
|
||||
return 365 * (y - 1970)
|
||||
+ Math.floor((y - 1969) / 4)
|
||||
- Math.floor((y - 1901) / 100)
|
||||
+ Math.floor((y - 1601) / 400);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-epochtimeforyear */
|
||||
export function EpochTimeForYear(y: number): number {
|
||||
return 86400000 * EpochDayNumberForYear(y);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-epochtimetoepochyear */
|
||||
// TODO(temporal): Review
|
||||
export function EpochTimeToEpochYear(t: number): number {
|
||||
// EpochTimeToEpochYear(t) = the largest integral Number y (closest to +∞) such that EpochTimeForYear(y) ≤ t
|
||||
let lower = -271821;
|
||||
let upper = 275760;
|
||||
while (lower < upper) {
|
||||
const mid = Math.floor((lower + upper + 1) / 2);
|
||||
if (EpochTimeForYear(mid) <= t) {
|
||||
lower = mid;
|
||||
} else {
|
||||
upper = mid - 1;
|
||||
}
|
||||
}
|
||||
return lower;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-mathematicalinleapyear */
|
||||
export function MathematicalInLeapYear(t: number): number {
|
||||
return MathematicalDaysInYear(EpochTimeToEpochYear(t)) === 366 ? 1 : 0;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-epochtimetomonthinyear */
|
||||
export function EpochTimeToMonthInYear(t: number): number {
|
||||
const dayInYear = EpochTimeToDayInYear(t);
|
||||
const leap = MathematicalInLeapYear(t);
|
||||
if (dayInYear >= 0 && dayInYear < 31) return 0;
|
||||
if (dayInYear >= 31 && dayInYear < 59 + leap) return 1;
|
||||
if (59 + leap <= dayInYear && dayInYear < 90 + leap) return 2;
|
||||
if (90 + leap <= dayInYear && dayInYear < 120 + leap) return 3;
|
||||
if (120 + leap <= dayInYear && dayInYear < 151 + leap) return 4;
|
||||
if (151 + leap <= dayInYear && dayInYear < 181 + leap) return 5;
|
||||
if (181 + leap <= dayInYear && dayInYear < 212 + leap) return 6;
|
||||
if (212 + leap <= dayInYear && dayInYear < 243 + leap) return 7;
|
||||
if (243 + leap <= dayInYear && dayInYear < 273 + leap) return 8;
|
||||
if (273 + leap <= dayInYear && dayInYear < 304 + leap) return 9;
|
||||
if (304 + leap <= dayInYear && dayInYear < 334 + leap) return 10;
|
||||
return 11;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-epochtimetodayinyear */
|
||||
export function EpochTimeToDayInYear(t: number): number {
|
||||
return EpochTimeToDayNumber(t) - EpochDayNumberForYear(EpochTimeToEpochYear(t));
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-epochtimetodate */
|
||||
export function EpochTimeToDate(t: number): number {
|
||||
const m = EpochTimeToMonthInYear(t);
|
||||
const dayInYear = EpochTimeToDayInYear(t);
|
||||
const leap = MathematicalInLeapYear(t) ? 1 : 0;
|
||||
if (m === 0) return dayInYear + 1;
|
||||
if (m === 1) return dayInYear - 30;
|
||||
if (m === 2) return dayInYear - 58 - leap;
|
||||
if (m === 3) return dayInYear - 89 - leap;
|
||||
if (m === 4) return dayInYear - 119 - leap;
|
||||
if (m === 5) return dayInYear - 150 - leap;
|
||||
if (m === 6) return dayInYear - 180 - leap;
|
||||
if (m === 7) return dayInYear - 211 - leap;
|
||||
if (m === 8) return dayInYear - 242 - leap;
|
||||
if (m === 9) return dayInYear - 272 - leap;
|
||||
if (m === 10) return dayInYear - 303 - leap;
|
||||
return dayInYear - 333 - leap;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-epochtimetoweekday */
|
||||
export function EpochTimeToWeekDay(t: number): number {
|
||||
return (EpochTimeToDayNumber(t) + 4) % 7;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-checkisodaysrange */
|
||||
export function CheckISODaysRange(isoDate: ISODateRecord): PlainCompletion<void> {
|
||||
const days = Math.abs(ISODateToEpochDays(isoDate.Year, isoDate.Month - 1, isoDate.Day));
|
||||
if (days > 1e8) {
|
||||
return Throw.RangeError('ISODate is out of range');
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-units */
|
||||
export enum TemporalUnit {
|
||||
Year, Month, Week, Day,
|
||||
Hour, Minute, Second, Millisecond, Microsecond, Nanosecond
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#table-temporal-units */
|
||||
export type TimeUnit = TemporalUnit.Hour | TemporalUnit.Minute | TemporalUnit.Second | TemporalUnit.Millisecond | TemporalUnit.Microsecond | TemporalUnit.Nanosecond;
|
||||
|
||||
export function __IsTimeUnit(unit: TemporalUnit): unit is TimeUnit {
|
||||
return (unit === TemporalUnit.Hour
|
||||
|| unit === TemporalUnit.Minute
|
||||
|| unit === TemporalUnit.Second
|
||||
|| unit === TemporalUnit.Millisecond
|
||||
|| unit === TemporalUnit.Microsecond
|
||||
|| unit === TemporalUnit.Nanosecond
|
||||
);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#table-temporal-units */
|
||||
export type DateUnit = TemporalUnit.Year | TemporalUnit.Month | TemporalUnit.Week | TemporalUnit.Day;
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#table-temporal-units */
|
||||
export const Table21_LengthInNanoSeconds = {
|
||||
[TemporalUnit.Day]: 8.64e13 satisfies typeof nsPerDay,
|
||||
[TemporalUnit.Hour]: 3.6e12,
|
||||
[TemporalUnit.Minute]: 6e10,
|
||||
[TemporalUnit.Second]: 1e9,
|
||||
[TemporalUnit.Millisecond]: 1e6,
|
||||
[TemporalUnit.Microsecond]: 1e3,
|
||||
[TemporalUnit.Nanosecond]: 1,
|
||||
} as const;
|
||||
|
||||
export const Table21_CategoryByValue = {
|
||||
[TemporalUnit.Year]: 'date',
|
||||
[TemporalUnit.Month]: 'date',
|
||||
[TemporalUnit.Week]: 'date',
|
||||
[TemporalUnit.Day]: 'date',
|
||||
[TemporalUnit.Hour]: 'time',
|
||||
[TemporalUnit.Minute]: 'time',
|
||||
[TemporalUnit.Second]: 'time',
|
||||
[TemporalUnit.Millisecond]: 'time',
|
||||
[TemporalUnit.Microsecond]: 'time',
|
||||
[TemporalUnit.Nanosecond]: 'time',
|
||||
} as const;
|
||||
|
||||
export function __IsDateUnit(unit: TemporalUnit): unit is DateUnit {
|
||||
return (unit === TemporalUnit.Year
|
||||
|| unit === TemporalUnit.Month
|
||||
|| unit === TemporalUnit.Week
|
||||
|| unit === TemporalUnit.Day
|
||||
);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-gettemporaloverflowoption */
|
||||
export function* GetTemporalOverflowOption(options: ObjectValue): PlainEvaluator<'constrain' | 'reject'> {
|
||||
const stringValue = Q(yield* GetOption(options, 'overflow', 'string', ['constrain', 'reject'], 'constrain'));
|
||||
if (stringValue === 'constrain') {
|
||||
return 'constrain';
|
||||
}
|
||||
return 'reject';
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-gettemporaldisambiguationoption */
|
||||
export function* GetTemporalDisambiguationOption(options: ObjectValue): PlainEvaluator<'compatible' | 'earlier' | 'later' | 'reject'> {
|
||||
const stringValue = Q(yield* GetOption(options, 'disambiguation', 'string', ['compatible', 'earlier', 'later', 'reject'], 'compatible'));
|
||||
if (stringValue === 'compatible') return 'compatible';
|
||||
if (stringValue === 'earlier') return 'earlier';
|
||||
if (stringValue === 'later') return 'later';
|
||||
return 'reject';
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-negateroundingmode */
|
||||
export function NegateRoundingMode(roundingMode: RoundingMode): RoundingMode {
|
||||
switch (roundingMode) {
|
||||
case RoundingMode.Ceil: return RoundingMode.Floor;
|
||||
case RoundingMode.Floor: return RoundingMode.Ceil;
|
||||
case RoundingMode.HalfCeil: return RoundingMode.HalfFloor;
|
||||
case RoundingMode.HalfFloor: return RoundingMode.HalfCeil;
|
||||
default: return roundingMode;
|
||||
}
|
||||
}
|
||||
|
||||
export type TemporalOffsetOption = 'prefer' | 'use' | 'ignore' | 'reject';
|
||||
/** https://tc39.es/proposal-temporal/#sec-gettemporaloffsetoption */
|
||||
export function* GetTemporalOffsetOption(options: ObjectValue, fallback: TemporalOffsetOption): PlainEvaluator<TemporalOffsetOption> {
|
||||
// step 1 to 4
|
||||
const stringFallback = fallback;
|
||||
const stringValue = Q(yield* GetOption(options, 'offset', 'string', ['prefer', 'use', 'ignore', 'reject'], stringFallback));
|
||||
if (stringValue === 'prefer') return 'prefer';
|
||||
if (stringValue === 'use') return 'use';
|
||||
if (stringValue === 'ignore') return 'ignore';
|
||||
return 'reject';
|
||||
}
|
||||
|
||||
export type ShowCalendarNameOption = 'auto' | 'always' | 'never' | 'critical';
|
||||
/** https://tc39.es/proposal-temporal/#sec-gettemporalshowcalendarnameoption */
|
||||
export function* GetTemporalShowCalendarNameOption(options: ObjectValue): PlainEvaluator<ShowCalendarNameOption> {
|
||||
const stringValue = Q(yield* GetOption(options, 'calendarName', 'string', ['auto', 'always', 'never', 'critical'], 'auto'));
|
||||
if (stringValue === 'always') return 'always';
|
||||
if (stringValue === 'never') return 'never';
|
||||
if (stringValue === 'critical') return 'critical';
|
||||
return 'auto';
|
||||
}
|
||||
|
||||
export type ShowTimeZoneNameOption = 'auto' | 'never' | 'critical';
|
||||
/** https://tc39.es/proposal-temporal/#sec-gettemporalshowtimezonenameoption */
|
||||
export function* GetTemporalShowTimeZoneNameOption(options: ObjectValue): PlainEvaluator<ShowTimeZoneNameOption> {
|
||||
const stringValue = Q(yield* GetOption(options, 'timeZoneName', 'string', ['auto', 'never', 'critical'], 'auto'));
|
||||
if (stringValue === 'never') return 'never';
|
||||
if (stringValue === 'critical') return 'critical';
|
||||
return 'auto';
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-gettemporalshowoffsetoption */
|
||||
export function* GetTemporalShowOffsetOption(options: ObjectValue): PlainEvaluator<'auto' | 'never'> {
|
||||
const stringValue = Q(yield* GetOption(options, 'offset', 'string', ['auto', 'never'], 'auto'));
|
||||
if (stringValue === 'never') return 'never';
|
||||
return 'auto';
|
||||
}
|
||||
|
||||
export type DirectionOption = 'next' | 'previous';
|
||||
/** https://tc39.es/proposal-temporal/#sec-getdirectionoption */
|
||||
export function* GetDirectionOption(options: ObjectValue): PlainEvaluator<DirectionOption> {
|
||||
const stringValue = Q(yield* GetOption(options, 'direction', 'string', ['next', 'previous'], '~required~'));
|
||||
if (stringValue === 'next') return 'next';
|
||||
return 'previous';
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-validatetemporalroundingincrement */
|
||||
export function ValidateTemporalRoundingIncrement(increment: number, dividend: number, inclusive: boolean): PlainCompletion<void> {
|
||||
let maximum;
|
||||
if (inclusive) {
|
||||
maximum = dividend;
|
||||
} else {
|
||||
Assert(dividend > 1);
|
||||
maximum = dividend - 1;
|
||||
}
|
||||
if (increment > maximum) {
|
||||
return surroundingAgent.Throw('RangeError', 'OutOfRange', increment);
|
||||
}
|
||||
if (dividend % increment !== 0) {
|
||||
return surroundingAgent.Throw('RangeError', 'OutOfRange', increment);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-gettemporalfractionalseconddigitsoption */
|
||||
export function* GetTemporalFractionalSecondDigitsOption(options: ObjectValue): PlainEvaluator<'auto' | number> {
|
||||
const digitsValue = Q(yield* Get(options, Value('fractionalSecondDigits')));
|
||||
if (digitsValue instanceof UndefinedValue) {
|
||||
return 'auto';
|
||||
}
|
||||
if (!(digitsValue instanceof NumberValue)) {
|
||||
if (Q(yield* ToString(digitsValue)).stringValue() !== 'auto') {
|
||||
return surroundingAgent.Throw('RangeError', 'OutOfRange', digitsValue);
|
||||
}
|
||||
return 'auto';
|
||||
}
|
||||
if (digitsValue.isNaN() || digitsValue.isInfinity()) {
|
||||
return surroundingAgent.Throw('RangeError', 'OutOfRange', digitsValue);
|
||||
}
|
||||
const digitCount = Math.floor(R(digitsValue));
|
||||
if (digitCount < 0 || digitCount > 9) {
|
||||
return surroundingAgent.Throw('RangeError', 'OutOfRange', digitsValue);
|
||||
}
|
||||
return digitCount;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-tosecondsstringprecisionrecord */
|
||||
export function ToSecondsStringPrecisionRecord(
|
||||
smallestUnit: Exclude<TimeUnit, TemporalUnit.Hour> | 'unset',
|
||||
fractionalDigitCount: 'auto' | number,
|
||||
): {
|
||||
Precision: TemporalUnit.Minute | 'auto' | number,
|
||||
Unit: TemporalUnit.Minute | TemporalUnit.Second | TemporalUnit.Millisecond | TemporalUnit.Microsecond | TemporalUnit.Nanosecond,
|
||||
Increment: 1 | 10 | 100
|
||||
} {
|
||||
if (smallestUnit === TemporalUnit.Minute) {
|
||||
return { Precision: TemporalUnit.Minute, Unit: TemporalUnit.Minute, Increment: 1 };
|
||||
}
|
||||
if (smallestUnit === TemporalUnit.Second) {
|
||||
return { Precision: 0, Unit: TemporalUnit.Second, Increment: 1 };
|
||||
}
|
||||
if (smallestUnit === TemporalUnit.Millisecond) {
|
||||
return { Precision: 3, Unit: TemporalUnit.Millisecond, Increment: 1 };
|
||||
}
|
||||
if (smallestUnit === TemporalUnit.Microsecond) {
|
||||
return { Precision: 6, Unit: TemporalUnit.Microsecond, Increment: 1 };
|
||||
}
|
||||
if (smallestUnit === TemporalUnit.Nanosecond) {
|
||||
return { Precision: 9, Unit: TemporalUnit.Nanosecond, Increment: 1 };
|
||||
}
|
||||
Assert(smallestUnit === 'unset');
|
||||
if (fractionalDigitCount === 'auto') {
|
||||
return { Precision: 'auto', Unit: TemporalUnit.Nanosecond, Increment: 1 };
|
||||
}
|
||||
if (fractionalDigitCount === 0) {
|
||||
return { Precision: 0, Unit: TemporalUnit.Second, Increment: 1 };
|
||||
}
|
||||
if (fractionalDigitCount >= 1 && fractionalDigitCount <= 3) {
|
||||
return { Precision: fractionalDigitCount, Unit: TemporalUnit.Millisecond, Increment: 10 ** (3 - fractionalDigitCount) as 1 | 10 | 100 };
|
||||
}
|
||||
if (fractionalDigitCount >= 4 && fractionalDigitCount <= 6) {
|
||||
return { Precision: fractionalDigitCount, Unit: TemporalUnit.Microsecond, Increment: 10 ** (6 - fractionalDigitCount) as 1 | 10 | 100 };
|
||||
}
|
||||
Assert(fractionalDigitCount >= 7 && fractionalDigitCount <= 9);
|
||||
return { Precision: fractionalDigitCount, Unit: TemporalUnit.Nanosecond, Increment: 10 ** (9 - fractionalDigitCount) as 1 | 10 | 100 };
|
||||
}
|
||||
|
||||
const table21 = [
|
||||
{
|
||||
Value: TemporalUnit.Year, Singular: 'year', Plural: 'years',
|
||||
},
|
||||
{
|
||||
Value: TemporalUnit.Month, Singular: 'month', Plural: 'months',
|
||||
},
|
||||
{
|
||||
Value: TemporalUnit.Week, Singular: 'week', Plural: 'weeks',
|
||||
},
|
||||
{
|
||||
Value: TemporalUnit.Day, Singular: 'day', Plural: 'days',
|
||||
},
|
||||
{
|
||||
Value: TemporalUnit.Hour, Singular: 'hour', Plural: 'hours',
|
||||
},
|
||||
{
|
||||
Value: TemporalUnit.Minute, Singular: 'minute', Plural: 'minutes',
|
||||
},
|
||||
{
|
||||
Value: TemporalUnit.Second, Singular: 'second', Plural: 'seconds',
|
||||
},
|
||||
{
|
||||
Value: TemporalUnit.Millisecond, Singular: 'millisecond', Plural: 'milliseconds',
|
||||
},
|
||||
{
|
||||
Value: TemporalUnit.Microsecond, Singular: 'microsecond', Plural: 'microseconds',
|
||||
},
|
||||
{
|
||||
Value: TemporalUnit.Nanosecond, Singular: 'nanosecond', Plural: 'nanoseconds',
|
||||
},
|
||||
] as const;
|
||||
/** https://tc39.es/proposal-temporal/#sec-gettemporalunitvaluedoption */
|
||||
export function* GetTemporalUnitValuedOption(
|
||||
options: ObjectValue,
|
||||
key: PropertyKeyValue | string,
|
||||
defaultV: 'required' | 'unset',
|
||||
): PlainEvaluator<TemporalUnit | 'unset' | 'auto'> {
|
||||
// 1. Let allowedStrings be a List containing all values in the "Singular property name" and "Plural property name" columns of Table 21, except the header row.
|
||||
const allowedStrings = table21.map<string>((row) => row.Singular).concat(table21.map((row) => row.Plural)).concat('auto');
|
||||
const defaultValue = defaultV === 'unset' ? undefined : defaultV;
|
||||
const value = Q(yield* GetOption(options, key, 'string', allowedStrings, defaultValue));
|
||||
if (value === undefined) {
|
||||
return 'unset';
|
||||
}
|
||||
if (value === 'auto') {
|
||||
return 'auto';
|
||||
}
|
||||
// 9. Return the value in the "Value" column of Table 21 corresponding to the row with value in its "Singular property name" or "Plural property name" column.
|
||||
const returnValue = table21.find((row) => row.Singular === value || row.Plural === value)?.Value;
|
||||
Assert(returnValue !== undefined);
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-validatetemporalunitvaluedoption */
|
||||
export function ValidateTemporalUnitValue(value: TemporalUnit | 'unset' | 'auto', unitGroup: 'date' | 'time' | 'datetime', extraValues?: Array<TemporalUnit | 'auto'>): PlainCompletion<void> {
|
||||
if (value === 'unset') return undefined;
|
||||
if (extraValues?.includes(value)) return undefined;
|
||||
const category = Table21_CategoryByValue[value as TemporalUnit];
|
||||
if (!category) {
|
||||
return Throw.RangeError('Invalid TemporalUnit value $1', value);
|
||||
}
|
||||
if (category === 'date' && (unitGroup === 'datetime' || unitGroup === 'date')) return undefined;
|
||||
if (category === 'time' && (unitGroup === 'datetime' || unitGroup === 'time')) return undefined;
|
||||
return Throw.RangeError('Invalid TemporalUnit value $1', value);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-gettemporalrelativetooption */
|
||||
export function* GetTemporalRelativeToOption(options: ObjectValue): PlainEvaluator<{
|
||||
PlainRelativeTo?: TemporalPlainDateObject,
|
||||
ZonedRelativeTo?: TemporalZonedDateTimeObject,
|
||||
}> {
|
||||
const value = Q(yield* Get(options, Value('relativeTo')));
|
||||
if (value instanceof UndefinedValue) {
|
||||
return { PlainRelativeTo: undefined, ZonedRelativeTo: undefined };
|
||||
}
|
||||
let offsetBehaviour: ISODateTimeOffsetBehaviour = 'option';
|
||||
let matchBehaviour: ISODateTimeMatchBehaviour = 'match-exactly';
|
||||
let timeZone: TimeZoneIdentifier | 'unset';
|
||||
let isoDate;
|
||||
let time;
|
||||
let calendar: CalendarType | undefined;
|
||||
let offsetString;
|
||||
if (value instanceof ObjectValue) {
|
||||
if (isTemporalZonedDateTimeObject(value)) {
|
||||
return { PlainRelativeTo: undefined, ZonedRelativeTo: value };
|
||||
}
|
||||
if (isTemporalPlainDateObject(value)) {
|
||||
return { PlainRelativeTo: value, ZonedRelativeTo: undefined };
|
||||
}
|
||||
if (isTemporalPlainDateTimeObject(value)) {
|
||||
const plainDate = X(CreateTemporalDate(value.ISODateTime.ISODate, value.Calendar));
|
||||
return { PlainRelativeTo: plainDate, ZonedRelativeTo: undefined };
|
||||
}
|
||||
calendar = Q(yield* GetTemporalCalendarIdentifierWithISODefault(value));
|
||||
const fields = Q(yield* PrepareCalendarFields(calendar, value, ['year', 'month', 'month-code', 'day'], ['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond', 'offset', 'time-zone'], []));
|
||||
const result = Q(yield* InterpretTemporalDateTimeFields(calendar, fields, 'constrain'));
|
||||
timeZone = fields.TimeZone as TimeZoneIdentifier;
|
||||
offsetString = fields.OffsetString;
|
||||
if (offsetString === undefined) {
|
||||
offsetBehaviour = 'wall';
|
||||
}
|
||||
isoDate = result.ISODate;
|
||||
time = result.Time;
|
||||
} else {
|
||||
if (!(value instanceof JSStringValue)) {
|
||||
return surroundingAgent.Throw('TypeError', 'NotAString', value);
|
||||
}
|
||||
const result = Q(ParseISODateTime(value.stringValue(), ['TemporalDateTimeString[+Zoned]', 'TemporalDateTimeString[~Zoned]']));
|
||||
offsetString = result.TimeZone.OffsetString;
|
||||
const annotation = result.TimeZone.TimeZoneAnnotation;
|
||||
if (!annotation) {
|
||||
timeZone = 'unset';
|
||||
} else {
|
||||
timeZone = Q(ToTemporalTimeZoneIdentifier(annotation));
|
||||
if (result.TimeZone.Z === true) {
|
||||
offsetBehaviour = 'exact';
|
||||
} else if (!offsetString) {
|
||||
offsetBehaviour = 'wall';
|
||||
}
|
||||
matchBehaviour = 'match-minutes';
|
||||
}
|
||||
let _calendar = result.Calendar;
|
||||
if (!_calendar) {
|
||||
_calendar = 'iso8601';
|
||||
}
|
||||
calendar = Q(CanonicalizeCalendar(_calendar));
|
||||
isoDate = CreateISODateRecord(result.Year!, result.Month, result.Day);
|
||||
time = result.Time;
|
||||
}
|
||||
if (timeZone === 'unset') {
|
||||
const plainDate = Q(yield* CreateTemporalDate(isoDate, calendar));
|
||||
return { PlainRelativeTo: plainDate, ZonedRelativeTo: undefined };
|
||||
}
|
||||
let offsetNs;
|
||||
if (offsetBehaviour === 'option') {
|
||||
offsetNs = X(ParseDateTimeUTCOffset(offsetString!));
|
||||
} else {
|
||||
offsetNs = 0;
|
||||
}
|
||||
const epochNanoseconds = Q(InterpretISODateTimeOffset(isoDate, time, offsetBehaviour, offsetNs, timeZone, 'compatible', 'reject', matchBehaviour));
|
||||
const zonedRelativeTo = X(CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar));
|
||||
return { PlainRelativeTo: undefined, ZonedRelativeTo: zonedRelativeTo };
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-largeroftwotemporalunits */
|
||||
export function LargerOfTwoTemporalUnits(u1: TemporalUnit, u2: TemporalUnit): TemporalUnit {
|
||||
const order = [
|
||||
TemporalUnit.Year,
|
||||
TemporalUnit.Month,
|
||||
TemporalUnit.Week,
|
||||
TemporalUnit.Day,
|
||||
TemporalUnit.Hour,
|
||||
TemporalUnit.Minute,
|
||||
TemporalUnit.Second,
|
||||
TemporalUnit.Millisecond,
|
||||
TemporalUnit.Microsecond,
|
||||
TemporalUnit.Nanosecond,
|
||||
];
|
||||
for (const unit of order) {
|
||||
if (u1 === unit) {
|
||||
return unit;
|
||||
}
|
||||
if (u2 === unit) {
|
||||
return unit;
|
||||
}
|
||||
}
|
||||
Assert(false, 'unreachable');
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-iscalendarunit */
|
||||
export function IsCalendarUnit(unit: TemporalUnit): unit is TemporalUnit.Year | TemporalUnit.Month | TemporalUnit.Week {
|
||||
return unit === TemporalUnit.Year || unit === TemporalUnit.Month || unit === TemporalUnit.Week;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporalunitcategory */
|
||||
export function TemporalUnitCategory(unit: TemporalUnit): 'date' | 'time' {
|
||||
if (unit === TemporalUnit.Year || unit === TemporalUnit.Month || unit === TemporalUnit.Week || unit === TemporalUnit.Day) {
|
||||
return 'date';
|
||||
}
|
||||
return 'time';
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-maximumtemporaldurationroundingincrement */
|
||||
export function MaximumTemporalDurationRoundingIncrement(unit: TemporalUnit): 24 | 60 | 1000 | 'unset' {
|
||||
switch (unit) {
|
||||
case TemporalUnit.Hour: return 24;
|
||||
case TemporalUnit.Minute: return 60;
|
||||
case TemporalUnit.Second: return 60;
|
||||
case TemporalUnit.Millisecond: return 1000;
|
||||
case TemporalUnit.Microsecond: return 1000;
|
||||
case TemporalUnit.Nanosecond: return 1000;
|
||||
default: return 'unset';
|
||||
}
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-ispartialtemporalobject */
|
||||
export function* IsPartialTemporalObject(value: Value): PlainEvaluator<boolean> {
|
||||
if (!(value instanceof ObjectValue)) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
'InitializedTemporalDate' in value
|
||||
|| 'InitializedTemporalDateTime' in value
|
||||
|| 'InitializedTemporalMonthDay' in value
|
||||
|| 'InitializedTemporalTime' in value
|
||||
|| 'InitializedTemporalYearMonth' in value
|
||||
|| 'InitializedTemporalZonedDateTime' in value
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const calendarProperty = Q(yield* Get(value, Value('calendar')));
|
||||
if (!(calendarProperty instanceof UndefinedValue)) {
|
||||
return false;
|
||||
}
|
||||
const timeZoneProperty = Q(yield* Get(value, Value('timeZone')));
|
||||
if (!(timeZoneProperty instanceof UndefinedValue)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-formatfractionalseconds */
|
||||
export function FormatFractionalSeconds(subSecondNanoseconds: number, precision: number | 'auto'): string {
|
||||
if (precision === 'auto') {
|
||||
if (subSecondNanoseconds === 0) {
|
||||
return '';
|
||||
}
|
||||
let fractionString = ToZeroPaddedDecimalString(subSecondNanoseconds, 9);
|
||||
// Set fractionString to the longest prefix of fractionString ending with a code unit other than 0x0030 (DIGIT ZERO).
|
||||
fractionString = fractionString.replace(/0+$/, '');
|
||||
return `.${fractionString}`;
|
||||
} else {
|
||||
if (precision === 0) {
|
||||
return '';
|
||||
}
|
||||
let fractionString = ToZeroPaddedDecimalString(subSecondNanoseconds, 9);
|
||||
fractionString = fractionString.slice(0, precision);
|
||||
return `.${fractionString}`;
|
||||
}
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-formattimestring */
|
||||
export function FormatTimeString(
|
||||
hour: number,
|
||||
minute: number,
|
||||
second: number,
|
||||
subSecondNanoseconds: number,
|
||||
precision: number | 'minute' | 'auto',
|
||||
style?: 'separated' | 'unseparated',
|
||||
): string {
|
||||
const separator = style === 'unseparated' ? '' : ':';
|
||||
const hh = ToZeroPaddedDecimalString(hour, 2);
|
||||
const mm = ToZeroPaddedDecimalString(minute, 2);
|
||||
if (precision === 'minute') {
|
||||
return hh + separator + mm;
|
||||
}
|
||||
const ss = ToZeroPaddedDecimalString(second, 2);
|
||||
const subSecondsPart = FormatFractionalSeconds(subSecondNanoseconds, precision);
|
||||
return hh + separator + mm + separator + ss + subSecondsPart;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-getunsignedroundingmode */
|
||||
export function GetUnsignedRoundingMode(
|
||||
roundingMode: RoundingMode,
|
||||
sign: 'negative' | 'positive',
|
||||
): UnsignedRoundingMode {
|
||||
const table = {
|
||||
[RoundingMode.Ceil]: { positive: UnsignedRoundingMode.Infinity, negative: UnsignedRoundingMode.Zero },
|
||||
[RoundingMode.Floor]: { positive: UnsignedRoundingMode.Zero, negative: UnsignedRoundingMode.Infinity },
|
||||
[RoundingMode.Expand]: { positive: UnsignedRoundingMode.Infinity, negative: UnsignedRoundingMode.Infinity },
|
||||
[RoundingMode.Trunc]: { positive: UnsignedRoundingMode.Zero, negative: UnsignedRoundingMode.Zero },
|
||||
[RoundingMode.HalfCeil]: { positive: UnsignedRoundingMode.HalfInfinity, negative: UnsignedRoundingMode.HalfZero },
|
||||
[RoundingMode.HalfFloor]: { positive: UnsignedRoundingMode.HalfZero, negative: UnsignedRoundingMode.HalfInfinity },
|
||||
[RoundingMode.HalfExpand]: { positive: UnsignedRoundingMode.HalfInfinity, negative: UnsignedRoundingMode.HalfInfinity },
|
||||
[RoundingMode.HalfTrunc]: { positive: UnsignedRoundingMode.HalfZero, negative: UnsignedRoundingMode.HalfZero },
|
||||
[RoundingMode.HalfEven]: { positive: UnsignedRoundingMode.HalfEven, negative: UnsignedRoundingMode.HalfEven },
|
||||
} as const;
|
||||
return table[roundingMode][sign];
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-applyunsignedroundingmode */
|
||||
export function ApplyUnsignedRoundingMode(
|
||||
x: number,
|
||||
r1: number,
|
||||
r2: number,
|
||||
unsignedRoundingMode?: UnsignedRoundingMode,
|
||||
): number {
|
||||
if (x === r1) {
|
||||
return r1;
|
||||
}
|
||||
Assert(r1 < x && x < r2);
|
||||
Assert(unsignedRoundingMode !== undefined);
|
||||
if (unsignedRoundingMode === UnsignedRoundingMode.Zero) {
|
||||
return r1;
|
||||
}
|
||||
if (unsignedRoundingMode === UnsignedRoundingMode.Infinity) {
|
||||
return r2;
|
||||
}
|
||||
const d1 = x - r1;
|
||||
const d2 = r2 - x;
|
||||
if (d1 < d2) {
|
||||
return r1;
|
||||
}
|
||||
if (d2 < d1) {
|
||||
return r2;
|
||||
}
|
||||
Assert(d1 === d2);
|
||||
if (unsignedRoundingMode === UnsignedRoundingMode.HalfZero) {
|
||||
return r1;
|
||||
}
|
||||
if (unsignedRoundingMode === UnsignedRoundingMode.HalfInfinity) {
|
||||
return r2;
|
||||
}
|
||||
Assert(unsignedRoundingMode === UnsignedRoundingMode.HalfEven);
|
||||
const cardinality = (r1 / (r2 - r1)) % 2;
|
||||
if (cardinality === 0) {
|
||||
return r1;
|
||||
}
|
||||
return r2;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-roundnumbertoincrement */
|
||||
export function RoundNumberToIncrement(
|
||||
x: number,
|
||||
increment: number,
|
||||
roundingMode: RoundingMode,
|
||||
): number {
|
||||
let quotient = x / increment;
|
||||
let isNegative: 'negative' | 'positive';
|
||||
if (quotient < 0) {
|
||||
isNegative = 'negative';
|
||||
quotient = -quotient;
|
||||
} else {
|
||||
isNegative = 'positive';
|
||||
}
|
||||
const unsignedRoundingMode = GetUnsignedRoundingMode(roundingMode, isNegative);
|
||||
// Let r1 be the largest integer such that r1 ≤ quotient.
|
||||
const r1 = Math.floor(quotient);
|
||||
// Let r2 be the smallest integer such that r2 > quotient.
|
||||
const r2 = r1 + 1;
|
||||
let rounded = ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode);
|
||||
if (isNegative === 'negative') {
|
||||
rounded = -rounded;
|
||||
}
|
||||
return rounded * increment;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-roundnumbertoincrementasifpositive */
|
||||
export function RoundNumberToIncrementAsIfPositive(
|
||||
x: number,
|
||||
increment: number,
|
||||
roundingMode: RoundingMode,
|
||||
): number {
|
||||
const quotient = x / increment;
|
||||
const unsignedRoundingMode = GetUnsignedRoundingMode(roundingMode, 'positive');
|
||||
// Let r1 be the largest integer such that r1 ≤ quotient.
|
||||
const r1 = Math.floor(quotient);
|
||||
// Let r2 be the smallest integer such that r2 > quotient.
|
||||
const r2 = r1 + 1;
|
||||
const rounded = ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode);
|
||||
return rounded * increment;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-topositiveintegerwithtruncation */
|
||||
export function* ToPositiveIntegerWithTruncation(argument: Value): PlainEvaluator<number> {
|
||||
const integer = Q(yield* ToIntegerWithTruncation(argument));
|
||||
if (integer <= 0) {
|
||||
return surroundingAgent.Throw('RangeError', 'OutOfRange', integer);
|
||||
}
|
||||
return integer;
|
||||
}
|
||||
|
||||
// TODO: Review
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-tointegerwithtruncation */
|
||||
export function* ToIntegerWithTruncation(argument: Value): PlainEvaluator<number> {
|
||||
const number = R(Q(yield* ToNumber(argument)));
|
||||
if (Number.isNaN(number) || number === Infinity || number === -Infinity) {
|
||||
return surroundingAgent.Throw('RangeError', 'OutOfRange', number);
|
||||
}
|
||||
return Math.trunc(number);
|
||||
}
|
||||
|
||||
// TODO: Review
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-tomonthcode */
|
||||
export function* ToMonthCode(argument: Value): PlainEvaluator<string> {
|
||||
const monthCode = Q(yield* ToPrimitive(argument, 'string'));
|
||||
if (!(monthCode instanceof JSStringValue)) {
|
||||
return surroundingAgent.Throw('TypeError', 'NotAString', monthCode);
|
||||
}
|
||||
const s = monthCode.stringValue();
|
||||
if (s.length !== 3 && s.length !== 4) {
|
||||
return surroundingAgent.Throw('RangeError', 'OutOfRange', s);
|
||||
}
|
||||
if (s.charCodeAt(0) !== 0x004D) {
|
||||
return surroundingAgent.Throw('RangeError', 'OutOfRange', s);
|
||||
}
|
||||
if (s.charCodeAt(1) < 0x0030 || s.charCodeAt(1) > 0x0039) {
|
||||
return surroundingAgent.Throw('RangeError', 'OutOfRange', s);
|
||||
}
|
||||
if (s.charCodeAt(2) < 0x0030 || s.charCodeAt(2) > 0x0039) {
|
||||
return surroundingAgent.Throw('RangeError', 'OutOfRange', s);
|
||||
}
|
||||
if (s.length === 4 && s.charCodeAt(3) !== 0x004C) {
|
||||
return surroundingAgent.Throw('RangeError', 'OutOfRange', s);
|
||||
}
|
||||
const monthCodeDigits = s.slice(1, 3);
|
||||
const monthCodeInteger = Number(monthCodeDigits);
|
||||
if (monthCodeInteger === 0 && s.length !== 4) {
|
||||
return surroundingAgent.Throw('RangeError', 'OutOfRange', s);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-tooffsetstring */
|
||||
export function* ToOffsetString(argument: Value): PlainEvaluator<string> {
|
||||
const offset = Q(yield* ToPrimitive(argument, 'string'));
|
||||
if (!(offset instanceof JSStringValue)) {
|
||||
return surroundingAgent.Throw('TypeError', 'NotAString', offset);
|
||||
}
|
||||
Q(ParseDateTimeUTCOffset(offset.stringValue()));
|
||||
return offset.stringValue();
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-isodatetofields */
|
||||
export function ISODateToFields(
|
||||
calendar: CalendarType,
|
||||
isoDate: ISODateRecord,
|
||||
type: 'date' | 'year-month' | 'month-day',
|
||||
): CalendarFieldsRecord {
|
||||
const fields: CalendarFieldsRecord = {
|
||||
Day: undefined,
|
||||
Era: undefined,
|
||||
EraYear: undefined,
|
||||
Hour: undefined,
|
||||
Microsecond: undefined,
|
||||
Millisecond: undefined,
|
||||
Minute: undefined,
|
||||
Month: undefined,
|
||||
MonthCode: undefined,
|
||||
Nanosecond: undefined,
|
||||
OffsetString: undefined,
|
||||
Second: undefined,
|
||||
TimeZone: undefined,
|
||||
Year: undefined,
|
||||
};
|
||||
const calendarDate = CalendarISOToDate(calendar, isoDate);
|
||||
fields.MonthCode = calendarDate.MonthCode;
|
||||
if (type === 'month-day' || type === 'date') {
|
||||
fields.Day = calendarDate.Day;
|
||||
}
|
||||
if (type === 'year-month' || type === 'date') {
|
||||
fields.Year = calendarDate.Year;
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-getdifferencesettings */
|
||||
export function* GetDifferenceSettings(
|
||||
operation: 'since' | 'until',
|
||||
options: ObjectValue,
|
||||
unitGroup: 'date' | 'time' | 'datetime',
|
||||
disallowedUnits: readonly TemporalUnit[],
|
||||
fallbackSmallestUnit: TemporalUnit,
|
||||
smallestLargestDefaultUnit: TemporalUnit,
|
||||
): PlainEvaluator<{
|
||||
SmallestUnit: TemporalUnit,
|
||||
LargestUnit: TemporalUnit,
|
||||
RoundingMode: RoundingMode,
|
||||
RoundingIncrement: number
|
||||
}> {
|
||||
let largestUnit = Q(yield* GetTemporalUnitValuedOption(options, 'largestUnit', 'unset'));
|
||||
const roundingIncrement = Q(yield* GetRoundingIncrementOption(options));
|
||||
let roundingMode = Q(yield* GetRoundingModeOption(options, RoundingMode.Trunc));
|
||||
let smallestUnit = Q(yield* GetTemporalUnitValuedOption(options, 'smallestUnit', 'unset'));
|
||||
Q(ValidateTemporalUnitValue(smallestUnit, unitGroup, ['auto']));
|
||||
if (largestUnit === 'unset') {
|
||||
largestUnit = 'auto';
|
||||
}
|
||||
if (disallowedUnits.includes(largestUnit as TemporalUnit)) {
|
||||
return surroundingAgent.Throw('RangeError', 'OutOfRange', largestUnit);
|
||||
}
|
||||
Q(ValidateTemporalUnitValue(smallestUnit, unitGroup));
|
||||
if (smallestUnit === 'unset') {
|
||||
smallestUnit = fallbackSmallestUnit;
|
||||
}
|
||||
if (disallowedUnits.includes(smallestUnit as TemporalUnit)) {
|
||||
return surroundingAgent.Throw('RangeError', 'OutOfRange', smallestUnit);
|
||||
}
|
||||
const defaultLargestUnit = LargerOfTwoTemporalUnits(smallestLargestDefaultUnit, smallestUnit as TemporalUnit);
|
||||
if (largestUnit === 'auto') {
|
||||
largestUnit = defaultLargestUnit;
|
||||
}
|
||||
if (LargerOfTwoTemporalUnits(largestUnit, smallestUnit as TemporalUnit) !== largestUnit) {
|
||||
return surroundingAgent.Throw('RangeError', 'OutOfRange', largestUnit);
|
||||
}
|
||||
const maximum = MaximumTemporalDurationRoundingIncrement(smallestUnit as TemporalUnit);
|
||||
if (maximum !== 'unset') {
|
||||
Q(ValidateTemporalRoundingIncrement(roundingIncrement, maximum, false));
|
||||
}
|
||||
if (operation === 'since') {
|
||||
roundingMode = NegateRoundingMode(roundingMode);
|
||||
}
|
||||
return {
|
||||
SmallestUnit: smallestUnit as TemporalUnit,
|
||||
LargestUnit: largestUnit,
|
||||
RoundingMode: roundingMode,
|
||||
RoundingIncrement: roundingIncrement,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
import type { ISODateRecord } from '../../intrinsics/Temporal/PlainDate.mts';
|
||||
import type { ISODateTimeRecord } from '../../intrinsics/Temporal/PlainDateTime.mts';
|
||||
import { ParseTemporalTimeZoneString, ParseTimeZoneIdentifier } from '../../parser/TemporalParser.mts';
|
||||
import {
|
||||
HourFromTime, MinFromTime, SecFromTime, msFromTime,
|
||||
} from '../date-objects.mts';
|
||||
import { isTemporalZonedDateTimeObject } from '../../intrinsics/Temporal/ZonedDateTime.mts';
|
||||
import { R } from '../spec-types.mjs';
|
||||
import { abs } from '../math.mts';
|
||||
import {
|
||||
IsOffsetTimeZoneIdentifier, GetNamedTimeZoneEpochNanoseconds, GetUTCEpochNanoseconds, RoundingMode,
|
||||
AvailableNamedTimeZoneIdentifiers,
|
||||
GetNamedTimeZoneOffsetNanoseconds,
|
||||
} from './addition.mts';
|
||||
import type { TimeZoneIdentifier } from './addition.mts';
|
||||
import {
|
||||
RoundNumberToIncrement, EpochTimeToDate, EpochTimeToEpochYear, EpochTimeToMonthInYear, CheckISODaysRange,
|
||||
FormatTimeString,
|
||||
} from './temporal.mts';
|
||||
import {
|
||||
Assert, JSStringValue, ObjectValue, Value, type PlainCompletion, Q,
|
||||
Throw,
|
||||
X,
|
||||
AddDaysToISODate,
|
||||
AddTime,
|
||||
BalanceISODateTime,
|
||||
CombineISODateAndTimeRecord,
|
||||
CreateISODateRecord,
|
||||
CreateTimeRecord,
|
||||
IsValidEpochNanoseconds,
|
||||
MidnightTimeRecord,
|
||||
nsPerDay,
|
||||
TimeDurationFromComponents,
|
||||
} from '#self';
|
||||
|
||||
// https://tc39.es/proposal-temporal/#sec-temporal-getavailablenamedtimezoneidentifier
|
||||
export function GetAvailableNamedTimeZoneIdentifier(timeZoneIdentifier: TimeZoneIdentifier): TimeZoneIdentifierRecord | undefined {
|
||||
for (const record of AvailableNamedTimeZoneIdentifiers()) {
|
||||
if (record.Identifier.toLowerCase() === timeZoneIdentifier.toLowerCase()) {
|
||||
return record;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** https://tc39.es/ecma262/#sec-time-zone-identifier-record */
|
||||
export interface TimeZoneIdentifierRecord {
|
||||
readonly Identifier: TimeZoneIdentifier;
|
||||
readonly PrimaryIdentifier: TimeZoneIdentifier;
|
||||
}
|
||||
|
||||
// https://tc39.es/proposal-temporal/#sec-temporal-getisopartsfromepoch
|
||||
export function GetISOPartsFromEpoch(epochNanoseconds: number): ISODateTimeRecord {
|
||||
Assert(IsValidEpochNanoseconds(epochNanoseconds));
|
||||
const remainderNs = epochNanoseconds % 1e6;
|
||||
const epochMilliseconds = (epochNanoseconds - remainderNs) / 1e6;
|
||||
const year = EpochTimeToEpochYear(epochMilliseconds);
|
||||
const month = EpochTimeToMonthInYear(epochMilliseconds) + 1;
|
||||
const day = EpochTimeToDate(epochMilliseconds);
|
||||
const hour = R(HourFromTime(Value(epochMilliseconds)));
|
||||
const minute = R(MinFromTime(Value(epochMilliseconds)));
|
||||
const second = R(SecFromTime(Value(epochMilliseconds)));
|
||||
const millisecond = R(msFromTime(Value(epochMilliseconds)));
|
||||
const microsecond = Math.floor(remainderNs / 1000);
|
||||
Assert(microsecond < 1000);
|
||||
const nanosecond = remainderNs % 1000;
|
||||
const isoDate = CreateISODateRecord(year, month, day);
|
||||
const time = CreateTimeRecord(hour, minute, second, millisecond, microsecond, nanosecond);
|
||||
return CombineISODateAndTimeRecord(isoDate, time);
|
||||
}
|
||||
|
||||
// https://tc39.es/proposal-temporal/#sec-temporal-getnamedtimezonenexttransition
|
||||
export function GetNamedTimeZoneNextTransition(timeZoneIdentifier: TimeZoneIdentifier, _epochNanoseconds: bigint): bigint | null {
|
||||
Assert(timeZoneIdentifier === 'UTC');
|
||||
return null;
|
||||
}
|
||||
|
||||
// https://tc39.es/proposal-temporal/#sec-temporal-getnamedtimezoneprevioustransition
|
||||
export function GetNamedTimeZonePreviousTransition(timeZoneIdentifier: TimeZoneIdentifier, _epochNanoseconds: bigint): bigint | null {
|
||||
Assert(timeZoneIdentifier === 'UTC');
|
||||
return null;
|
||||
}
|
||||
|
||||
// https://tc39.es/proposal-temporal/#sec-temporal-formatoffsettimezoneidentifier
|
||||
export function FormatOffsetTimeZoneIdentifier(offsetMinutes: number, style: 'separated' | 'unseparated' = 'separated'): TimeZoneIdentifier {
|
||||
const sign = offsetMinutes >= 0 ? '+' : '-';
|
||||
const absoluteMinutes = Math.abs(offsetMinutes);
|
||||
const hour = Math.floor(absoluteMinutes / 60);
|
||||
const minute = absoluteMinutes % 60;
|
||||
const timeString = FormatTimeString(hour, minute, 0, 0, 'minute', style);
|
||||
return sign + timeString as TimeZoneIdentifier;
|
||||
}
|
||||
|
||||
// https://tc39.es/proposal-temporal/#sec-temporal-formatutcoffsetnanoseconds
|
||||
export function FormatUTCOffsetNanoseconds(offsetNanoseconds: number): string {
|
||||
const sign = offsetNanoseconds >= 0 ? '+' : '-';
|
||||
const absoluteNanoseconds = Math.abs(offsetNanoseconds);
|
||||
const hour = Math.floor(absoluteNanoseconds / (3600 * 1e9));
|
||||
const minute = Math.floor(absoluteNanoseconds / (60 * 1e9)) % 60;
|
||||
const second = Math.floor(absoluteNanoseconds / 1e9) % 60;
|
||||
const subSecondNanoseconds = absoluteNanoseconds % 1e9;
|
||||
const precision: 'minute' | 'auto' = second === 0 && subSecondNanoseconds === 0 ? 'minute' : 'auto';
|
||||
const timeString = FormatTimeString(hour, minute, second, subSecondNanoseconds, precision);
|
||||
return sign + timeString;
|
||||
}
|
||||
|
||||
// https://tc39.es/proposal-temporal/#sec-temporal-formatdatetimeutcoffsetrounded
|
||||
export function FormatDateTimeUTCOffsetRounded(offsetNanoseconds: number): string {
|
||||
offsetNanoseconds = RoundNumberToIncrement(offsetNanoseconds, 60 * 1e9, RoundingMode.HalfExpand);
|
||||
const offsetMinutes = offsetNanoseconds / (60 * 1e9);
|
||||
return FormatOffsetTimeZoneIdentifier(offsetMinutes);
|
||||
}
|
||||
|
||||
// https://tc39.es/proposal-temporal/#sec-temporal-totemporaltimezoneidentifier
|
||||
export function ToTemporalTimeZoneIdentifier(temporalTimeZoneLike: Value | string): PlainCompletion<TimeZoneIdentifier> {
|
||||
if (temporalTimeZoneLike instanceof ObjectValue && isTemporalZonedDateTimeObject(temporalTimeZoneLike)) {
|
||||
return temporalTimeZoneLike.TimeZone;
|
||||
}
|
||||
if (!(temporalTimeZoneLike instanceof JSStringValue) && typeof temporalTimeZoneLike !== 'string') {
|
||||
return Throw.TypeError('$1 is not a string', temporalTimeZoneLike);
|
||||
}
|
||||
const temporalTimeZoneLikeString = temporalTimeZoneLike instanceof JSStringValue ? temporalTimeZoneLike.stringValue() : temporalTimeZoneLike;
|
||||
const parseResult = Q(ParseTemporalTimeZoneString(temporalTimeZoneLikeString));
|
||||
const offsetMinutes = parseResult.OffsetMinutes;
|
||||
if (offsetMinutes !== undefined) {
|
||||
return FormatOffsetTimeZoneIdentifier(offsetMinutes);
|
||||
}
|
||||
const name = parseResult.Name;
|
||||
const timeZoneIdentifierRecord = GetAvailableNamedTimeZoneIdentifier(name! as TimeZoneIdentifier);
|
||||
if (timeZoneIdentifierRecord === undefined) {
|
||||
return Throw.RangeError('Invalid time zone identifier: $1', temporalTimeZoneLikeString);
|
||||
}
|
||||
return timeZoneIdentifierRecord.Identifier;
|
||||
}
|
||||
|
||||
// https://tc39.es/proposal-temporal/#sec-temporal-getoffsetnanosecondsfor
|
||||
export function GetOffsetNanosecondsFor(timeZone: TimeZoneIdentifier, epochNs: bigint): number {
|
||||
const parseResult = X(ParseTimeZoneIdentifier(timeZone));
|
||||
if (parseResult.OffsetMinutes !== undefined) {
|
||||
return parseResult.OffsetMinutes * (60 * 1e9);
|
||||
}
|
||||
return GetNamedTimeZoneOffsetNanoseconds(parseResult.Name!, epochNs);
|
||||
}
|
||||
|
||||
// https://tc39.es/proposal-temporal/#sec-temporal-getisodatetimefor
|
||||
export function GetISODateTimeFor(timeZone: TimeZoneIdentifier, epochNs: bigint): ISODateTimeRecord {
|
||||
const offsetNanoseconds = GetOffsetNanosecondsFor(timeZone, epochNs);
|
||||
const result = GetISOPartsFromEpoch(Number(epochNs));
|
||||
return BalanceISODateTime(
|
||||
result.ISODate.Year,
|
||||
result.ISODate.Month,
|
||||
result.ISODate.Day,
|
||||
result.Time.Hour,
|
||||
result.Time.Minute,
|
||||
result.Time.Second,
|
||||
result.Time.Millisecond,
|
||||
result.Time.Microsecond,
|
||||
result.Time.Nanosecond + offsetNanoseconds,
|
||||
);
|
||||
}
|
||||
|
||||
// https://tc39.es/proposal-temporal/#sec-temporal-getepochnanosecondsfor
|
||||
export function GetEpochNanosecondsFor(
|
||||
timeZone: TimeZoneIdentifier,
|
||||
isoDateTime: ISODateTimeRecord,
|
||||
disambiguation: 'compatible' | 'earlier' | 'later' | 'reject',
|
||||
): PlainCompletion<bigint> {
|
||||
const possibleEpochNs = Q(GetPossibleEpochNanoseconds(timeZone, isoDateTime));
|
||||
return DisambiguatePossibleEpochNanoseconds(possibleEpochNs, timeZone, isoDateTime, disambiguation);
|
||||
}
|
||||
|
||||
// https://tc39.es/proposal-temporal/#sec-temporal-disambiguatepossibleepochnanoseconds
|
||||
export function DisambiguatePossibleEpochNanoseconds(
|
||||
possibleEpochNs: readonly bigint[],
|
||||
timeZone: TimeZoneIdentifier,
|
||||
isoDateTime: ISODateTimeRecord,
|
||||
disambiguation: 'compatible' | 'earlier' | 'later' | 'reject',
|
||||
): PlainCompletion<bigint> {
|
||||
let n = possibleEpochNs.length;
|
||||
if (n === 1) {
|
||||
return possibleEpochNs[0];
|
||||
}
|
||||
if (n !== 0) {
|
||||
if (disambiguation === 'earlier' || disambiguation === 'compatible') {
|
||||
return possibleEpochNs[0];
|
||||
}
|
||||
if (disambiguation === 'later') {
|
||||
return possibleEpochNs[n - 1];
|
||||
}
|
||||
Assert(disambiguation === 'reject');
|
||||
return Throw.RangeError('Multiple possible epoch nanoseconds');
|
||||
}
|
||||
Assert(n === 0);
|
||||
if (disambiguation === 'reject') {
|
||||
return Throw.RangeError('No possible epoch nanoseconds');
|
||||
}
|
||||
const before: ISODateTimeRecord = null!;
|
||||
Assert(!!before, 'TODO(temporal): 6. Let before be the latest possible ISO Date-Time Record for which CompareISODateTime(before, isoDateTime) = -1 and ! GetPossibleEpochNanoseconds(timeZone, before) is not empty.');
|
||||
const after: ISODateTimeRecord = null!;
|
||||
Assert(!!after, 'TODO(temporal): 7. Let after be the earliest possible ISO Date-Time Record for which CompareISODateTime(after, isoDateTime) = 1 and ! GetPossibleEpochNanoseconds(timeZone, after) is not empty.');
|
||||
const beforePossible = X(GetPossibleEpochNanoseconds(timeZone, before));
|
||||
Assert(beforePossible.length === 1);
|
||||
const afterPossible = X(GetPossibleEpochNanoseconds(timeZone, after));
|
||||
Assert(afterPossible.length === 1);
|
||||
const offsetBefore = GetOffsetNanosecondsFor(timeZone, beforePossible[0]);
|
||||
const offsetAfter = GetOffsetNanosecondsFor(timeZone, afterPossible[0]);
|
||||
const naneseconds = offsetAfter - offsetBefore;
|
||||
Assert(abs(naneseconds) <= nsPerDay);
|
||||
if (disambiguation === 'earlier') {
|
||||
const timeDuration = TimeDurationFromComponents(0, 0, 0, 0, 0, -naneseconds);
|
||||
const earlierTime = AddTime(isoDateTime.Time, timeDuration);
|
||||
const earlierDate = AddDaysToISODate(isoDateTime.ISODate, earlierTime.Days);
|
||||
const earlierDateTime = CombineISODateAndTimeRecord(earlierDate, earlierTime);
|
||||
possibleEpochNs = Q(GetPossibleEpochNanoseconds(timeZone, earlierDateTime));
|
||||
Assert(possibleEpochNs.length > 0);
|
||||
return possibleEpochNs[0];
|
||||
}
|
||||
Assert(disambiguation === 'compatible' || disambiguation === 'later');
|
||||
const timeDuration = TimeDurationFromComponents(0, 0, 0, 0, 0, naneseconds);
|
||||
const laterTime = AddTime(isoDateTime.Time, timeDuration);
|
||||
const laterDate = AddDaysToISODate(isoDateTime.ISODate, laterTime.Days);
|
||||
const laterDateTime = CombineISODateAndTimeRecord(laterDate, laterTime);
|
||||
possibleEpochNs = Q(GetPossibleEpochNanoseconds(timeZone, laterDateTime));
|
||||
n = possibleEpochNs.length;
|
||||
Assert(n > 0);
|
||||
return possibleEpochNs[n - 1];
|
||||
}
|
||||
|
||||
// https://tc39.es/proposal-temporal/#sec-temporal-getpossibleepochnanoseconds
|
||||
export function GetPossibleEpochNanoseconds(
|
||||
timeZone: TimeZoneIdentifier,
|
||||
isoDateTime: ISODateTimeRecord,
|
||||
): PlainCompletion<bigint[]> {
|
||||
const parseResult = X(ParseTimeZoneIdentifier(timeZone));
|
||||
let possibleEpochNanoseconds: bigint[];
|
||||
if (parseResult.OffsetMinutes !== undefined) {
|
||||
const balanced = BalanceISODateTime(
|
||||
isoDateTime.ISODate.Year,
|
||||
isoDateTime.ISODate.Month,
|
||||
isoDateTime.ISODate.Day,
|
||||
isoDateTime.Time.Hour,
|
||||
isoDateTime.Time.Minute - parseResult.OffsetMinutes,
|
||||
isoDateTime.Time.Second,
|
||||
isoDateTime.Time.Millisecond,
|
||||
isoDateTime.Time.Microsecond,
|
||||
isoDateTime.Time.Nanosecond,
|
||||
);
|
||||
Q(CheckISODaysRange(balanced.ISODate));
|
||||
const epochNanoseconds = GetUTCEpochNanoseconds(balanced);
|
||||
possibleEpochNanoseconds = [epochNanoseconds];
|
||||
} else {
|
||||
possibleEpochNanoseconds = GetNamedTimeZoneEpochNanoseconds(parseResult.Name! as TimeZoneIdentifier, isoDateTime);
|
||||
}
|
||||
for (const epochNanoseconds of possibleEpochNanoseconds) {
|
||||
if (!IsValidEpochNanoseconds(epochNanoseconds)) {
|
||||
return Throw.RangeError('$1 is not a valid epoch nanoseconds', epochNanoseconds);
|
||||
}
|
||||
}
|
||||
return possibleEpochNanoseconds;
|
||||
}
|
||||
|
||||
// It determines the exact time that corresponds to the first valid wall-clock time in the calendar date isoDate in timeZone.
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-getstartofday */
|
||||
export function GetStartOfDay(
|
||||
timeZone: TimeZoneIdentifier,
|
||||
isoDate: ISODateRecord,
|
||||
): PlainCompletion<bigint> {
|
||||
const isoDateTime = CombineISODateAndTimeRecord(isoDate, MidnightTimeRecord());
|
||||
const possibleEpochNs = Q(GetPossibleEpochNanoseconds(timeZone, isoDateTime));
|
||||
if (possibleEpochNs.length) {
|
||||
return possibleEpochNs[0];
|
||||
}
|
||||
Assert(IsOffsetTimeZoneIdentifier(timeZone) === false);
|
||||
// TODO(temporal)
|
||||
const isoDateTimeAfter: ISODateTimeRecord = null!;
|
||||
Assert(!!isoDateTimeAfter, 'TODO: isoDateTimeAfter is the ISO Date-Time Record for which DifferenceISODateTime(isoDateTime, isoDateTimeAfter, "iso8601", hour).[[Time]] is the smallest possible value > 0 for which possibleEpochNsAfter is not empty (i.e., isoDateTimeAfter represents the first local time after the transition).');
|
||||
// const possibleEpochNsAfter = GetNamedTimeZoneEpochNanoseconds(timeZone, isoDateTimeAfter!);
|
||||
// Assert(possibleEpochNsAfter.length === 1);
|
||||
// return possibleEpochNsAfter[0];
|
||||
return 0n;
|
||||
}
|
||||
|
||||
// https://tc39.es/proposal-temporal/#sec-temporal-timezoneequals
|
||||
export function TimeZoneEquals(one: TimeZoneIdentifier, two: TimeZoneIdentifier): boolean {
|
||||
if (one === two) {
|
||||
return true;
|
||||
}
|
||||
if (!IsOffsetTimeZoneIdentifier(one) && !IsOffsetTimeZoneIdentifier(two)) {
|
||||
const recordOne = GetAvailableNamedTimeZoneIdentifier(one);
|
||||
const recordTwo = GetAvailableNamedTimeZoneIdentifier(two);
|
||||
Assert(recordOne !== undefined);
|
||||
Assert(recordTwo !== undefined);
|
||||
if (recordOne.PrimaryIdentifier === recordTwo.PrimaryIdentifier) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// TODO(temporal)
|
||||
// 3. Assert: If one and two are both offset time zone identifiers, they do not represent the same number of offset minutes.
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
import type { TemporalDurationObject } from '../../intrinsics/Temporal/Duration.mts';
|
||||
import type { ISODateRecord } from '../../intrinsics/Temporal/PlainDate.mts';
|
||||
import { type TemporalZonedDateTimeObject, isTemporalZonedDateTimeObject } from '../../intrinsics/Temporal/ZonedDateTime.mts';
|
||||
import { ParseISODateTime, ParseDateTimeUTCOffset } from '../../parser/TemporalParser.mts';
|
||||
import {
|
||||
GetOptionsObject,
|
||||
type TimeZoneIdentifier, GetUTCEpochNanoseconds, RoundingMode,
|
||||
} from './addition.mts';
|
||||
import {
|
||||
type PlainCompletion, Assert, Q, GetStartOfDay, GetEpochNanosecondsFor, CheckISODaysRange, IsValidEpochNanoseconds, Throw, GetPossibleEpochNanoseconds, RoundNumberToIncrement, DisambiguatePossibleEpochNanoseconds, Value, type ValueEvaluator, type CalendarType, ObjectValue, GetTemporalDisambiguationOption, GetTemporalOffsetOption, GetTemporalOverflowOption, X, GetTemporalCalendarIdentifierWithISODefault, PrepareCalendarFields, JSStringValue, ToTemporalTimeZoneIdentifier, CanonicalizeCalendar, CreateISODateRecord, type FunctionObject, surroundingAgent, OrdinaryCreateFromConstructor, type Mutable, RoundTemporalInstant, TemporalUnit, GetOffsetNanosecondsFor, GetISODateTimeFor, FormatDateTimeUTCOffsetRounded, FormatCalendarAnnotation, type InternalDurationRecord, DateDurationSign, AddInstant, CalendarDateAdd, CombineDateAndTimeDuration, ZeroDateDuration, type TimeDuration, CompareISODate, TimeDurationFromEpochNanosecondsDifference, TimeDurationSign, AddDaysToISODate, LargerOfTwoTemporalUnits, CalendarDateUntil, type DateUnit, TemporalUnitCategory, DifferenceInstant, type TimeUnit, RoundRelativeDuration, TotalTimeDuration, TotalRelativeDuration, CalendarEquals, GetDifferenceSettings, TemporalDurationFromInternal, CreateNegatedTemporalDuration, TimeZoneEquals, CreateTemporalDuration, ToTemporalDuration, ToInternalDurationRecord,
|
||||
BalanceISODateTime,
|
||||
CombineISODateAndTimeRecord,
|
||||
DifferenceTime,
|
||||
InterpretTemporalDateTimeFields,
|
||||
ISODateTimeToString,
|
||||
ISODateTimeWithinLimits,
|
||||
type TimeRecord,
|
||||
} from '#self';
|
||||
|
||||
export type ISODateTimeOffsetBehaviour = 'option' | 'exact' | 'wall';
|
||||
export type ISODateTimeMatchBehaviour = 'match-exactly' | 'match-minutes';
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-interpretisodatetimeoffset */
|
||||
export function InterpretISODateTimeOffset(
|
||||
isoDate: ISODateRecord,
|
||||
time: TimeRecord | 'start-of-day',
|
||||
offsetBehaviour: ISODateTimeOffsetBehaviour,
|
||||
offsetNanoseconds: number,
|
||||
timeZone: TimeZoneIdentifier,
|
||||
disambiguation: 'earlier' | 'later' | 'compatible' | 'reject',
|
||||
offsetOption: 'ignore' | 'use' | 'prefer' | 'reject',
|
||||
matchBehaviour: ISODateTimeMatchBehaviour,
|
||||
): PlainCompletion<bigint> {
|
||||
if (time === 'start-of-day') {
|
||||
Assert(offsetBehaviour === 'wall');
|
||||
Assert(offsetNanoseconds === 0);
|
||||
return Q(GetStartOfDay(timeZone, isoDate));
|
||||
}
|
||||
const isoDateTime = CombineISODateAndTimeRecord(isoDate, time);
|
||||
if (offsetBehaviour === 'wall' || (offsetBehaviour === 'option' && offsetOption === 'ignore')) {
|
||||
return Q(GetEpochNanosecondsFor(timeZone, isoDateTime, disambiguation));
|
||||
}
|
||||
if (offsetBehaviour === 'exact' || (offsetBehaviour === 'option' && offsetOption === 'use')) {
|
||||
const balanced = BalanceISODateTime(isoDate.Year, isoDate.Month, isoDate.Day, time.Hour, time.Minute, time.Second, time.Millisecond, time.Microsecond, time.Nanosecond - offsetNanoseconds);
|
||||
Q(CheckISODaysRange(balanced.ISODate));
|
||||
const epochNanoseconds = GetUTCEpochNanoseconds(balanced);
|
||||
if (!IsValidEpochNanoseconds(epochNanoseconds)) {
|
||||
return Throw.RangeError('Invalid date');
|
||||
}
|
||||
return epochNanoseconds;
|
||||
}
|
||||
Assert(offsetBehaviour === 'option');
|
||||
Assert(offsetOption === 'prefer' || offsetOption === 'reject');
|
||||
Q(CheckISODaysRange(isoDate));
|
||||
const utcEpochNanoseconds = GetUTCEpochNanoseconds(isoDateTime);
|
||||
const possibleEpochNs = Q(GetPossibleEpochNanoseconds(timeZone, isoDateTime));
|
||||
for (const candidate of possibleEpochNs) {
|
||||
const candidateOffset = utcEpochNanoseconds - candidate;
|
||||
if (candidateOffset === BigInt(offsetNanoseconds)) {
|
||||
return candidate;
|
||||
}
|
||||
if (matchBehaviour === 'match-minutes') {
|
||||
const roundedCandidateNanoseconds = RoundNumberToIncrement(Number(candidateOffset), 60 * 1e9, RoundingMode.HalfExpand);
|
||||
if (roundedCandidateNanoseconds === offsetNanoseconds) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (offsetOption === 'reject') {
|
||||
return Throw.RangeError('No matching offset found for the given date and time');
|
||||
}
|
||||
return Q(DisambiguatePossibleEpochNanoseconds(possibleEpochNs, timeZone, isoDateTime, disambiguation));
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-totemporalzoneddatetime */
|
||||
export function* ToTemporalZonedDateTime(
|
||||
item: Value,
|
||||
options: Value = Value.undefined,
|
||||
): ValueEvaluator<TemporalZonedDateTimeObject> {
|
||||
let hasUTCDesignator = false;
|
||||
let matchBehaviour: ISODateTimeMatchBehaviour = 'match-exactly';
|
||||
let calendar: CalendarType;
|
||||
let isoDate: ISODateRecord;
|
||||
let time: TimeRecord | 'start-of-day';
|
||||
let timeZone: TimeZoneIdentifier;
|
||||
let offsetString: string | undefined;
|
||||
let disambiguation: 'earlier' | 'later' | 'compatible' | 'reject';
|
||||
let offsetOption: 'ignore' | 'use' | 'prefer' | 'reject';
|
||||
if (item instanceof ObjectValue) {
|
||||
if (isTemporalZonedDateTimeObject(item)) {
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
Q(yield* GetTemporalDisambiguationOption(resolvedOptions));
|
||||
Q(yield* GetTemporalOffsetOption(resolvedOptions, 'reject'));
|
||||
Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
return X(CreateTemporalZonedDateTime(item.EpochNanoseconds, item.TimeZone, item.Calendar));
|
||||
}
|
||||
calendar = Q(yield* GetTemporalCalendarIdentifierWithISODefault(item));
|
||||
const fields = Q(yield* PrepareCalendarFields(calendar, item, ['year', 'month', 'month-code', 'day'], ['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond', 'offset', 'time-zone'], ['time-zone']));
|
||||
timeZone = fields.TimeZone! as TimeZoneIdentifier;
|
||||
offsetString = fields.OffsetString;
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
disambiguation = Q(yield* GetTemporalDisambiguationOption(resolvedOptions));
|
||||
offsetOption = Q(yield* GetTemporalOffsetOption(resolvedOptions, 'reject'));
|
||||
const overflow = Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
const result = Q(yield* InterpretTemporalDateTimeFields(calendar, fields, overflow));
|
||||
isoDate = result.ISODate;
|
||||
time = result.Time;
|
||||
} else {
|
||||
if (!(item instanceof JSStringValue)) {
|
||||
return Throw.TypeError('$1 is not a string', item);
|
||||
}
|
||||
const result = Q(ParseISODateTime(item.stringValue(), ['TemporalDateTimeString[+Zoned]']));
|
||||
const annotation = result.TimeZone.TimeZoneAnnotation;
|
||||
Assert(annotation !== undefined);
|
||||
timeZone = Q(ToTemporalTimeZoneIdentifier(annotation));
|
||||
offsetString = result.TimeZone.OffsetString;
|
||||
if (result.TimeZone.Z) {
|
||||
hasUTCDesignator = true;
|
||||
}
|
||||
let calendar = result.Calendar;
|
||||
if (calendar === undefined) {
|
||||
calendar = 'iso8601';
|
||||
}
|
||||
calendar = Q(CanonicalizeCalendar(calendar));
|
||||
matchBehaviour = 'match-minutes';
|
||||
if (offsetString) {
|
||||
// TODO(temporal):
|
||||
// i. Let offsetParseResult be ParseText(StringToCodePoints(offsetString), UTCOffset[+SubMinutePrecision]).
|
||||
// ii. Assert: offsetParseResult is a Parse Node.
|
||||
// iii. If offsetParseResult contains more than one MinuteSecond Parse Node, set matchBehaviour to match-exactly.
|
||||
}
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
disambiguation = Q(yield* GetTemporalDisambiguationOption(resolvedOptions));
|
||||
offsetOption = Q(yield* GetTemporalOffsetOption(resolvedOptions, 'reject'));
|
||||
Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
isoDate = CreateISODateRecord(result.Year!, result.Month, result.Day);
|
||||
time = result.Time;
|
||||
}
|
||||
let offsetBehaviour: ISODateTimeOffsetBehaviour;
|
||||
if (hasUTCDesignator) {
|
||||
offsetBehaviour = 'exact';
|
||||
} else if (offsetString === undefined) {
|
||||
offsetBehaviour = 'wall';
|
||||
} else {
|
||||
offsetBehaviour = 'option';
|
||||
}
|
||||
let offsetNanoseconds = 0;
|
||||
if (offsetBehaviour === 'option') {
|
||||
offsetNanoseconds = X(ParseDateTimeUTCOffset(offsetString!));
|
||||
}
|
||||
const epochNanoseconds = Q(InterpretISODateTimeOffset(isoDate, time, offsetBehaviour, offsetNanoseconds, timeZone, disambiguation, offsetOption, matchBehaviour));
|
||||
return X(CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar!));
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-createtemporalzoneddatetime */
|
||||
export function* CreateTemporalZonedDateTime(
|
||||
epochNanoseconds: bigint,
|
||||
timeZone: TimeZoneIdentifier,
|
||||
calendar: CalendarType,
|
||||
newTarget?: FunctionObject,
|
||||
): ValueEvaluator<TemporalZonedDateTimeObject> {
|
||||
Assert(IsValidEpochNanoseconds(epochNanoseconds));
|
||||
if (newTarget === undefined) {
|
||||
newTarget = surroundingAgent.intrinsic('%Temporal.ZonedDateTime%');
|
||||
}
|
||||
const object = Q(yield* OrdinaryCreateFromConstructor(newTarget, '%Temporal.ZonedDateTime.prototype%', [
|
||||
'InitializedTemporalZonedDateTime',
|
||||
'EpochNanoseconds',
|
||||
'TimeZone',
|
||||
'Calendar',
|
||||
])) as Mutable<TemporalZonedDateTimeObject>;
|
||||
object.EpochNanoseconds = epochNanoseconds;
|
||||
object.TimeZone = timeZone;
|
||||
object.Calendar = calendar;
|
||||
return object;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-temporalzoneddatetimetostring */
|
||||
export function TemporalZonedDateTimeToString(
|
||||
zonedDateTime: TemporalZonedDateTimeObject,
|
||||
precision: number | 'minute' | 'auto',
|
||||
showCalendar: 'auto' | 'always' | 'never' | 'critical',
|
||||
showTimeZone: 'auto' | 'never' | 'critical',
|
||||
showOffset: 'auto' | 'never',
|
||||
increment = 1,
|
||||
unit: TemporalUnit.Minute | TemporalUnit.Second | TemporalUnit.Millisecond | TemporalUnit.Microsecond | TemporalUnit.Nanosecond = TemporalUnit.Nanosecond,
|
||||
roundingMode = RoundingMode.Trunc,
|
||||
): string {
|
||||
let epochNs = zonedDateTime.EpochNanoseconds;
|
||||
epochNs = RoundTemporalInstant(epochNs, increment, unit, roundingMode);
|
||||
const timeZone = zonedDateTime.TimeZone;
|
||||
const offsetNanoseconds = GetOffsetNanosecondsFor(timeZone, epochNs);
|
||||
const isoDateTime = GetISODateTimeFor(timeZone, epochNs);
|
||||
const dateTimeString = ISODateTimeToString(isoDateTime, 'iso8601', precision, 'never');
|
||||
const offsetString = showOffset === 'never' ? '' : FormatDateTimeUTCOffsetRounded(offsetNanoseconds);
|
||||
let timeZoneString;
|
||||
if (showTimeZone === 'never') {
|
||||
timeZoneString = '';
|
||||
} else {
|
||||
const flag = showTimeZone === 'critical' ? '!' : '';
|
||||
timeZoneString = `[${flag}${timeZone}]`;
|
||||
}
|
||||
const calendarString = FormatCalendarAnnotation(zonedDateTime.Calendar, showCalendar);
|
||||
return dateTimeString + offsetString + timeZoneString + calendarString;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-addzoneddatetime */
|
||||
export function AddZonedDateTime(
|
||||
epochNanoseconds: bigint,
|
||||
timeZone: TimeZoneIdentifier,
|
||||
calendar: CalendarType,
|
||||
duration: InternalDurationRecord,
|
||||
overflow: 'constrain' | 'reject',
|
||||
): PlainCompletion<bigint> {
|
||||
if (DateDurationSign(duration.Date) === 0) {
|
||||
return AddInstant(epochNanoseconds, duration.Time);
|
||||
}
|
||||
const isoDateTime = GetISODateTimeFor(timeZone, epochNanoseconds);
|
||||
const addedDate = Q(CalendarDateAdd(calendar, isoDateTime.ISODate, duration.Date, overflow));
|
||||
const intermediateDateTime = CombineISODateAndTimeRecord(addedDate, isoDateTime.Time);
|
||||
if (!ISODateTimeWithinLimits(intermediateDateTime)) {
|
||||
return Throw.RangeError('Resulting date-time is out of range');
|
||||
}
|
||||
const intermediateNs = X(GetEpochNanosecondsFor(timeZone, intermediateDateTime, 'compatible'));
|
||||
return AddInstant(intermediateNs, duration.Time);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-differencezoneddatetime */
|
||||
export function DifferenceZonedDateTime(
|
||||
ns1: bigint,
|
||||
ns2: bigint,
|
||||
timeZone: TimeZoneIdentifier,
|
||||
calendar: CalendarType,
|
||||
largestUnit: TemporalUnit,
|
||||
): PlainCompletion<InternalDurationRecord> {
|
||||
if (ns1 === ns2) {
|
||||
return CombineDateAndTimeDuration(ZeroDateDuration(), 0 as TimeDuration);
|
||||
}
|
||||
const startDateTime = GetISODateTimeFor(timeZone, ns1);
|
||||
const endDateTime = GetISODateTimeFor(timeZone, ns2);
|
||||
if (CompareISODate(startDateTime.ISODate, endDateTime.ISODate) === 0) {
|
||||
const timeDuration = TimeDurationFromEpochNanosecondsDifference(ns2, ns1);
|
||||
return CombineDateAndTimeDuration(ZeroDateDuration(), timeDuration);
|
||||
}
|
||||
const sign = ns2 - ns1 > 0 ? 1 : -1;
|
||||
const maxDayCorrection = sign === -1 ? 2 : 1;
|
||||
let dayCorrection = 0;
|
||||
let timeDuration = DifferenceTime(startDateTime.Time, endDateTime.Time);
|
||||
if (TimeDurationSign(timeDuration) === sign) dayCorrection += 1;
|
||||
let success = false;
|
||||
let intermediateDateTime;
|
||||
while (dayCorrection <= maxDayCorrection && !success) {
|
||||
const intermediateDate = AddDaysToISODate(endDateTime.ISODate, dayCorrection * sign);
|
||||
intermediateDateTime = CombineISODateAndTimeRecord(intermediateDate, startDateTime.Time);
|
||||
const intermediateNs = Q(GetEpochNanosecondsFor(timeZone, intermediateDateTime, 'compatible'));
|
||||
timeDuration = TimeDurationFromEpochNanosecondsDifference(ns2, intermediateNs);
|
||||
const timeSign = TimeDurationSign(timeDuration);
|
||||
if (sign !== timeSign) {
|
||||
success = true;
|
||||
}
|
||||
dayCorrection += 1;
|
||||
}
|
||||
Assert(success);
|
||||
const dateLargestUnit = LargerOfTwoTemporalUnits(largestUnit, TemporalUnit.Day);
|
||||
const dateDifference = CalendarDateUntil(calendar, startDateTime.ISODate, intermediateDateTime!.ISODate, dateLargestUnit as DateUnit);
|
||||
return CombineDateAndTimeDuration(dateDifference, timeDuration);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-differencezoneddatetimewithrounding */
|
||||
export function DifferenceZonedDateTimeWithRounding(
|
||||
ns1: bigint,
|
||||
ns2: bigint,
|
||||
timeZone: TimeZoneIdentifier,
|
||||
calendar: CalendarType,
|
||||
largestUnit: TemporalUnit,
|
||||
roundingIncrement: number,
|
||||
smallestUnit: TemporalUnit,
|
||||
roundingMode: RoundingMode,
|
||||
): PlainCompletion<InternalDurationRecord> {
|
||||
if (TemporalUnitCategory(largestUnit) === 'time') {
|
||||
return DifferenceInstant(ns1, ns2, roundingIncrement, smallestUnit as TimeUnit, roundingMode);
|
||||
}
|
||||
const difference = Q(DifferenceZonedDateTime(ns1, ns2, timeZone, calendar, largestUnit));
|
||||
if (smallestUnit === TemporalUnit.Nanosecond && roundingIncrement === 1) {
|
||||
return difference;
|
||||
}
|
||||
const dateTime = GetISODateTimeFor(timeZone, ns1);
|
||||
return RoundRelativeDuration(difference, ns1, ns2, dateTime, timeZone, calendar, largestUnit, roundingIncrement, smallestUnit, roundingMode);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-differencezoneddatetimewithtotal */
|
||||
export function DifferenceZonedDateTimeWithTotal(
|
||||
ns1: bigint,
|
||||
ns2: bigint,
|
||||
timeZone: TimeZoneIdentifier,
|
||||
calendar: CalendarType,
|
||||
unit: TemporalUnit,
|
||||
): PlainCompletion<number> {
|
||||
if (TemporalUnitCategory(unit) === 'time') {
|
||||
const difference = TimeDurationFromEpochNanosecondsDifference(ns2, ns1);
|
||||
return TotalTimeDuration(difference, unit as TimeUnit);
|
||||
}
|
||||
const difference = Q(DifferenceZonedDateTime(ns1, ns2, timeZone, calendar, unit));
|
||||
const dateTime = GetISODateTimeFor(timeZone, ns1);
|
||||
return TotalRelativeDuration(difference, ns1, ns2, dateTime, timeZone, calendar, unit);
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-differencetemporalzoneddatetime */
|
||||
export function* DifferenceTemporalZonedDateTime(
|
||||
operation: 'until' | 'since',
|
||||
zonedDateTime: TemporalZonedDateTimeObject,
|
||||
_other: Value,
|
||||
options: Value,
|
||||
): ValueEvaluator<TemporalDurationObject> {
|
||||
const other = Q(yield* ToTemporalZonedDateTime(_other));
|
||||
if (!CalendarEquals(zonedDateTime.Calendar, other.Calendar)) {
|
||||
return Throw.RangeError('Calendars are not equal');
|
||||
}
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
const settings = Q(yield* GetDifferenceSettings(operation, resolvedOptions, 'datetime', [], TemporalUnit.Nanosecond, TemporalUnit.Hour));
|
||||
if (TemporalUnitCategory(settings.LargestUnit) === 'time') {
|
||||
const internalDuration = DifferenceInstant(zonedDateTime.EpochNanoseconds, other.EpochNanoseconds, settings.RoundingIncrement, settings.SmallestUnit as TimeUnit, settings.RoundingMode);
|
||||
let result = X(TemporalDurationFromInternal(internalDuration, settings.LargestUnit));
|
||||
if (operation === 'since') {
|
||||
result = CreateNegatedTemporalDuration(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (!TimeZoneEquals(zonedDateTime.TimeZone, other.TimeZone)) {
|
||||
return Throw.RangeError('Time zones are not equal');
|
||||
}
|
||||
if (zonedDateTime.EpochNanoseconds === other.EpochNanoseconds) {
|
||||
return X(CreateTemporalDuration(0, 0, 0, 0, 0, 0, 0, 0, 0, 0));
|
||||
}
|
||||
const internalDuration = Q(DifferenceZonedDateTimeWithRounding(
|
||||
zonedDateTime.EpochNanoseconds,
|
||||
other.EpochNanoseconds,
|
||||
zonedDateTime.TimeZone,
|
||||
zonedDateTime.Calendar,
|
||||
settings.LargestUnit,
|
||||
settings.RoundingIncrement,
|
||||
settings.SmallestUnit,
|
||||
settings.RoundingMode,
|
||||
));
|
||||
let result = X(TemporalDurationFromInternal(internalDuration, TemporalUnit.Hour));
|
||||
if (operation === 'since') {
|
||||
result = CreateNegatedTemporalDuration(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** https://tc39.es/proposal-temporal/#sec-temporal-adddurationtozoneddatetime */
|
||||
export function* AddDurationToZonedDateTime(
|
||||
operation: 'add' | 'subtract',
|
||||
zonedDateTime: TemporalZonedDateTimeObject,
|
||||
temporalDurationLike: Value,
|
||||
options: Value,
|
||||
): ValueEvaluator<TemporalZonedDateTimeObject> {
|
||||
let duration = Q(yield* ToTemporalDuration(temporalDurationLike));
|
||||
if (operation === 'subtract') {
|
||||
duration = CreateNegatedTemporalDuration(duration);
|
||||
}
|
||||
const resolvedOptions = Q(GetOptionsObject(options));
|
||||
const overflow = Q(yield* GetTemporalOverflowOption(resolvedOptions));
|
||||
const calendar = zonedDateTime.Calendar;
|
||||
const timeZone = zonedDateTime.TimeZone;
|
||||
const internalDuration = ToInternalDurationRecord(duration);
|
||||
const epochNanoseconds = Q(AddZonedDateTime(zonedDateTime.EpochNanoseconds, timeZone, calendar, internalDuration, overflow));
|
||||
return X(CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar));
|
||||
}
|
||||
Reference in New Issue
Block a user