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 { 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 { // 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 { 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 { 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 { 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 { 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 | '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 { // 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((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): PlainCompletion { 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 { 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 { 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 { 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 { 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 { 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, }; }