Files
ask262/src/abstract-ops/temporal/temporal.mts
T
bendtherules 6e92d2345e Squashed 'engine262/' content from commit ae71998
git-subtree-dir: engine262
git-subtree-split: ae71998cc5a8315700555135b1ac202a0d6d0b31
2026-03-27 10:07:29 +05:30

878 lines
34 KiB
TypeScript

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,
};
}