const ArrayIncludes = Array.prototype.includes; const ArrayPrototypePush = Array.prototype.push; const IntlDateTimeFormat = globalThis.Intl.DateTimeFormat; const MathMin = Math.min; const MathMax = Math.max; const MathAbs = Math.abs; const MathFloor = Math.floor; const MathSign = Math.sign; const MathTrunc = Math.trunc; const NumberIsNaN = Number.isNaN; const NumberIsFinite = Number.isFinite; const NumberCtor = Number; const StringCtor = String; const NumberMaxSafeInteger = Number.MAX_SAFE_INTEGER; const ObjectCreate = Object.create; const ObjectDefineProperty = Object.defineProperty; const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; const ReflectApply = Reflect.apply; const ReflectOwnKeys = Reflect.ownKeys; import { DEBUG } from './debug'; import JSBI from 'jsbi'; import type { Temporal } from '..'; import type { AnyTemporalLikeType, UnitSmallerThanOrEqualTo, CalendarProtocolParams, TimeZoneProtocolParams, InstantParams, PlainMonthDayParams, ZonedDateTimeParams, CalendarParams, TimeZoneParams, PlainDateParams, PlainTimeParams, DurationParams, PlainDateTimeParams, PlainYearMonthParams, PrimitiveFieldsOf, BuiltinCalendarId, Keys, AnyTemporalKey, CalendarSlot, TimeZoneSlot } from './internaltypes'; import { GetIntrinsic } from './intrinsicclass'; import { CreateSlots, GetSlot, HasSlot, SetSlot, EPOCHNANOSECONDS, TIMEZONE_ID, CALENDAR_ID, INSTANT, ISO_YEAR, ISO_MONTH, ISO_DAY, ISO_HOUR, ISO_MINUTE, ISO_SECOND, ISO_MILLISECOND, ISO_MICROSECOND, ISO_NANOSECOND, DATE_BRAND, YEAR_MONTH_BRAND, MONTH_DAY_BRAND, TIME_ZONE, CALENDAR, YEARS, MONTHS, WEEKS, DAYS, HOURS, MINUTES, SECONDS, MILLISECONDS, MICROSECONDS, NANOSECONDS } from './slots'; export const ZERO = JSBI.BigInt(0); const ONE = JSBI.BigInt(1); const SIXTY = JSBI.BigInt(60); const TWENTY_FOUR = JSBI.BigInt(24); export const THOUSAND = JSBI.BigInt(1e3); export const MILLION = JSBI.BigInt(1e6); export const BILLION = JSBI.BigInt(1e9); const NEGATIVE_ONE = JSBI.BigInt(-1); const HOUR_SECONDS = 3600; export const HOUR_NANOS = JSBI.multiply(JSBI.BigInt(HOUR_SECONDS), BILLION); const MINUTE_NANOS = JSBI.multiply(SIXTY, BILLION); const DAY_NANOS = JSBI.multiply(HOUR_NANOS, TWENTY_FOUR); const NS_MIN = JSBI.multiply(JSBI.BigInt(-86400), JSBI.BigInt(1e17)); const NS_MAX = JSBI.multiply(JSBI.BigInt(86400), JSBI.BigInt(1e17)); const YEAR_MIN = -271821; const YEAR_MAX = 275760; const BEFORE_FIRST_OFFSET_TRANSITION = JSBI.multiply(JSBI.BigInt(-388152), JSBI.BigInt(1e13)); // 1847-01-01T00:00:00Z const ABOUT_TEN_YEARS_NANOS = JSBI.multiply(DAY_NANOS, JSBI.BigInt(366 * 10)); const ABOUT_ONE_YEAR_NANOS = JSBI.multiply(DAY_NANOS, JSBI.BigInt(366 * 1)); const TWO_WEEKS_NANOS = JSBI.multiply(DAY_NANOS, JSBI.BigInt(2 * 7)); const BUILTIN_CALENDAR_IDS = [ 'iso8601', 'hebrew', 'islamic', 'islamic-umalqura', 'islamic-tbla', 'islamic-civil', 'islamic-rgsa', 'islamicc', 'persian', 'ethiopic', 'ethioaa', 'coptic', 'chinese', 'dangi', 'roc', 'indian', 'buddhist', 'japanese', 'gregory' ]; /* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function */ /** * uncheckedAssertNarrowedType forces TypeScript to change the type of the argument to the one given in * the type parameter. This should only be used to help TS understand when variables change types, * but TS can't or won't infer this automatically. They should be used sparingly, because * if used incorrectly can lead to difficult-to-diagnose problems. * */ export function uncheckedAssertNarrowedType( arg: unknown, justification: string ): asserts arg is T extends typeof arg ? T : never {} /* eslint-enable */ type ArrayElement = ArrayType extends readonly (infer ElementType)[] ? ElementType : never; type ArrayWithNewKeys = Array | Keys>; /** * In debug builds, this function verifies that the given argument "exists" (is not * null or undefined). This function becomes a no-op in the final bundles distributed via NPM. * @param arg */ export function assertExists(arg: A): asserts arg is NonNullable { if (DEBUG) { if (arg != null) { throw new Error('Expected arg to be set.'); } } } function isZero(value: JSBI): boolean { return JSBI.equal(value, ZERO); } type Stringless = Exclude; function GetMethod unknown }, M extends string & keyof T>( obj: T, methodName: M ): T[M]; function GetMethod< T extends string | { [s in M]?: (...args: any[]) => unknown }, M extends string & keyof Stringless >(obj: T, methodName: M): Stringless[M] | undefined; function GetMethod< T extends string | { [s in M]?: undefined | ((...args: any[]) => unknown) }, M extends string & keyof T >(obj: T, methodName: M): T[M] | undefined { const result = obj[methodName]; if (result === undefined) return undefined; if (DEBUG) { if (typeof result !== 'function') throw new TypeError(`'${methodName}' must be a function`); } return result; } export function Call( target: (this: T, ...args: A) => R, thisArgument: T, argumentsList: Readonly ): R { const args = arguments.length > 2 ? argumentsList : []; if (DEBUG) { if (!Array.isArray(argumentsList)) { throw new TypeError('Assertion failed: optional `argumentsList`, if provided, must be an array'); } } return ReflectApply(target, thisArgument, args); } // For unknown values, this narrows the result to a Record. But for union types // like `Temporal.DurationLike | string`, it'll strip the primitive types while // leaving the object type(s) unchanged. export function IsObject( value: T ): value is Exclude; export function IsObject(value: unknown): value is Record { return (typeof value === 'object' && value !== null) || typeof value === 'function'; } export function ToNumber(value: unknown): number { // ES 2022's es-abstract made minor changes to ToNumber, but polyfilling these // changes adds zero benefit to Temporal and brings in a lot of extra code. So // we'll leave ToNumber as-is. // See https://github.com/ljharb/es-abstract/blob/main/2022/ToNumber.js if (typeof value === 'bigint') throw new TypeError('Cannot convert BigInt to number'); return NumberCtor(value); } function ToIntegerOrInfinity(value: unknown) { const number = ToNumber(value); if (NumberIsNaN(number) || number === 0) { return 0; } if (!NumberIsFinite(number)) { return number; } const integer = MathFloor(MathAbs(number)); if (integer === 0) { return 0; } return MathSign(number) * integer; } function IsIntegralNumber(argument: unknown) { if (typeof argument !== 'number' || NumberIsNaN(argument) || !NumberIsFinite(argument)) { return false; } const absValue = MathAbs(argument); return MathFloor(absValue) === absValue; } export function ToString(value: unknown): string { if (typeof value === 'symbol') { throw new TypeError('Cannot convert a Symbol value to a String'); } return StringCtor(value); } export function ToIntegerWithTruncation(value: unknown): number { const number = ToNumber(value); if (number === 0) return 0; if (NumberIsNaN(number) || !NumberIsFinite(number)) { throw new RangeError('invalid number value'); } const integer = MathTrunc(number); if (integer === 0) return 0; // ℝ(value) in spec text; converts -0 to 0 return integer; } function ToPositiveIntegerWithTruncation(valueParam: unknown, property?: string): number { const integer = ToIntegerWithTruncation(valueParam); if (integer <= 0) { if (property !== undefined) { throw new RangeError(`property '${property}' cannot be a a number less than one`); } throw new RangeError('Cannot convert a number less than one to a positive integer'); } return integer; } export function ToIntegerIfIntegral(valueParam: unknown): number { const number = ToNumber(valueParam); if (!NumberIsFinite(number)) throw new RangeError('infinity is out of range'); if (!IsIntegralNumber(number)) throw new RangeError(`unsupported fractional value ${valueParam}`); if (number === 0) return 0; // ℝ(value) in spec text; converts -0 to 0 return number; } function divmod(x: JSBI, y: JSBI): { quotient: JSBI; remainder: JSBI } { const quotient = JSBI.divide(x, y); const remainder = JSBI.remainder(x, y); return { quotient, remainder }; } function isNegativeJSBI(value: JSBI): boolean { return JSBI.lessThan(value, ZERO); } function signJSBI(value: JSBI): 1 | 0 | -1 { if (isZero(value)) return 0; if (isNegativeJSBI(value)) return -1; return 1; } function abs(x: JSBI): JSBI { if (JSBI.lessThan(x, ZERO)) return JSBI.multiply(x, NEGATIVE_ONE); return x; } type BuiltinCastFunction = (v: unknown) => string | number; const BUILTIN_CASTS = new Map([ ['year', ToIntegerWithTruncation], ['month', ToPositiveIntegerWithTruncation], ['monthCode', ToString], ['day', ToPositiveIntegerWithTruncation], ['hour', ToIntegerWithTruncation], ['minute', ToIntegerWithTruncation], ['second', ToIntegerWithTruncation], ['millisecond', ToIntegerWithTruncation], ['microsecond', ToIntegerWithTruncation], ['nanosecond', ToIntegerWithTruncation], ['years', ToIntegerIfIntegral], ['months', ToIntegerIfIntegral], ['weeks', ToIntegerIfIntegral], ['days', ToIntegerIfIntegral], ['hours', ToIntegerIfIntegral], ['minutes', ToIntegerIfIntegral], ['seconds', ToIntegerIfIntegral], ['milliseconds', ToIntegerIfIntegral], ['microseconds', ToIntegerIfIntegral], ['nanoseconds', ToIntegerIfIntegral], ['era', ToString], ['eraYear', ToIntegerOrInfinity], ['offset', ToString] ]); const BUILTIN_DEFAULTS = new Map([ ['hour', 0], ['minute', 0], ['second', 0], ['millisecond', 0], ['microsecond', 0], ['nanosecond', 0] ]); // each item is [plural, singular, category] const SINGULAR_PLURAL_UNITS = [ ['years', 'year', 'date'], ['months', 'month', 'date'], ['weeks', 'week', 'date'], ['days', 'day', 'date'], ['hours', 'hour', 'time'], ['minutes', 'minute', 'time'], ['seconds', 'second', 'time'], ['milliseconds', 'millisecond', 'time'], ['microseconds', 'microsecond', 'time'], ['nanoseconds', 'nanosecond', 'time'] ] as const; const SINGULAR_FOR = new Map(SINGULAR_PLURAL_UNITS.map((e) => [e[0], e[1]] as const)); const PLURAL_FOR = new Map(SINGULAR_PLURAL_UNITS.map(([p, s]) => [s, p])); const UNITS_DESCENDING = SINGULAR_PLURAL_UNITS.map(([, s]) => s); const DURATION_FIELDS = Array.from(SINGULAR_FOR.keys()).sort(); import * as PARSE from './regex'; const IntlDateTimeFormatEnUsCache = new Map(); function getIntlDateTimeFormatEnUsForTimeZone(timeZoneIdentifier: string) { let instance = IntlDateTimeFormatEnUsCache.get(timeZoneIdentifier); if (instance === undefined) { instance = new IntlDateTimeFormat('en-us', { timeZone: StringCtor(timeZoneIdentifier), hour12: false, era: 'short', year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' }); IntlDateTimeFormatEnUsCache.set(timeZoneIdentifier, instance); } return instance; } export function ToObject(value: T): T extends Record ? T : object { if (typeof value === 'undefined' || value === null) { throw new TypeError(`Expected object not ${value}`); } return Object(value); } // Adapted from https://github.com/ljharb/es-abstract/blob/main/2022/CopyDataProperties.js // but simplified (e.g. removed assertions) for this polyfill to reduce bundle size. export function CopyDataProperties>( target: T, source: T | undefined, excludedKeys: K[], excludedValues?: unknown[] ) { if (typeof source === 'undefined' || source === null) return; const keys = ReflectOwnKeys(source) as (keyof T)[]; for (const nextKey of keys) { if (excludedKeys.some((e) => Object.is(e, nextKey))) continue; if (Object.prototype.propertyIsEnumerable.call(source, nextKey)) { const propValue = source[nextKey]; if (excludedValues && excludedValues.some((e) => Object.is(e, propValue))) continue; target[nextKey] = propValue; } } } export function IsTemporalInstant(item: unknown): item is Temporal.Instant { return HasSlot(item, EPOCHNANOSECONDS) && !HasSlot(item, TIME_ZONE, CALENDAR); } export function IsTemporalTimeZone(item: unknown): item is Temporal.TimeZone { return HasSlot(item, TIMEZONE_ID); } export function IsTemporalCalendar(item: unknown): item is Temporal.Calendar { return HasSlot(item, CALENDAR_ID); } export function IsTemporalDuration(item: unknown): item is Temporal.Duration { return HasSlot(item, YEARS, MONTHS, DAYS, HOURS, MINUTES, SECONDS, MILLISECONDS, MICROSECONDS, NANOSECONDS); } export function IsTemporalDate(item: unknown): item is Temporal.PlainDate { return HasSlot(item, DATE_BRAND); } export function IsTemporalTime(item: unknown): item is Temporal.PlainTime { return ( HasSlot(item, ISO_HOUR, ISO_MINUTE, ISO_SECOND, ISO_MILLISECOND, ISO_MICROSECOND, ISO_NANOSECOND) && !HasSlot(item, ISO_YEAR, ISO_MONTH, ISO_DAY) ); } export function IsTemporalDateTime(item: unknown): item is Temporal.PlainDateTime { return HasSlot( item, ISO_YEAR, ISO_MONTH, ISO_DAY, ISO_HOUR, ISO_MINUTE, ISO_SECOND, ISO_MILLISECOND, ISO_MICROSECOND, ISO_NANOSECOND ); } export function IsTemporalYearMonth(item: unknown): item is Temporal.PlainYearMonth { return HasSlot(item, YEAR_MONTH_BRAND); } export function IsTemporalMonthDay(item: unknown): item is Temporal.PlainMonthDay { return HasSlot(item, MONTH_DAY_BRAND); } export function IsTemporalZonedDateTime(item: unknown): item is Temporal.ZonedDateTime { return HasSlot(item, EPOCHNANOSECONDS, TIME_ZONE, CALENDAR); } export function RejectTemporalLikeObject(item: AnyTemporalLikeType) { if (HasSlot(item, CALENDAR) || HasSlot(item, TIME_ZONE)) { throw new TypeError('with() does not support a calendar or timeZone property'); } if (IsTemporalTime(item)) { throw new TypeError('with() does not accept Temporal.PlainTime, use withPlainTime() instead'); } if ((item as { calendar: unknown }).calendar !== undefined) { throw new TypeError('with() does not support a calendar property'); } if ((item as { timeZone: unknown }).timeZone !== undefined) { throw new TypeError('with() does not support a timeZone property'); } } function ParseTemporalTimeZone(stringIdent: string) { const { ianaName, offset, z } = ParseTemporalTimeZoneString(stringIdent); if (ianaName) return GetCanonicalTimeZoneIdentifier(ianaName); if (z) return 'UTC'; // if !ianaName && !z then offset must be present assertExists(offset); const offsetNs = ParseTimeZoneOffsetString(offset); return FormatTimeZoneOffsetString(offsetNs); } function MaybeFormatCalendarAnnotation( calendar: CalendarSlot, showCalendar: Temporal.ShowCalendarOption['calendarName'] ): string { if (showCalendar === 'never') return ''; return FormatCalendarAnnotation(ToTemporalCalendarIdentifier(calendar), showCalendar); } function FormatCalendarAnnotation(id: string, showCalendar: Temporal.ShowCalendarOption['calendarName']) { if (showCalendar === 'never') return ''; if (showCalendar === 'auto' && id === 'iso8601') return ''; const flag = showCalendar === 'critical' ? '!' : ''; return `[${flag}u-ca=${id}]`; } function ParseISODateTime(isoString: string) { // ZDT is the superset of fields for every other Temporal type const match = PARSE.zoneddatetime.exec(isoString); if (!match) throw new RangeError(`invalid ISO 8601 string: ${isoString}`); let yearString = match[1]; if (yearString[0] === '\u2212') yearString = `-${yearString.slice(1)}`; if (yearString === '-000000') throw new RangeError(`invalid ISO 8601 string: ${isoString}`); const year = ToIntegerOrInfinity(yearString); const month = ToIntegerOrInfinity(match[2] || match[4]); const day = ToIntegerOrInfinity(match[3] || match[5]); const hour = ToIntegerOrInfinity(match[6]); const hasTime = match[6] !== undefined; const minute = ToIntegerOrInfinity(match[7] || match[10]); let second = ToIntegerOrInfinity(match[8] || match[11]); if (second === 60) second = 59; const fraction = (match[9] || match[12]) + '000000000'; const millisecond = ToIntegerOrInfinity(fraction.slice(0, 3)); const microsecond = ToIntegerOrInfinity(fraction.slice(3, 6)); const nanosecond = ToIntegerOrInfinity(fraction.slice(6, 9)); let offset; let z = false; if (match[13]) { offset = undefined; z = true; } else if (match[14] && match[15]) { const offsetSign = match[14] === '-' || match[14] === '\u2212' ? '-' : '+'; const offsetHours = match[15] || '00'; const offsetMinutes = match[16] || '00'; const offsetSeconds = match[17] || '00'; let offsetFraction = match[18] || '0'; offset = `${offsetSign}${offsetHours}:${offsetMinutes}`; if (+offsetFraction) { while (offsetFraction.endsWith('0')) offsetFraction = offsetFraction.slice(0, -1); offset += `:${offsetSeconds}.${offsetFraction}`; } else if (+offsetSeconds) { offset += `:${offsetSeconds}`; } if (offset === '-00:00') offset = '+00:00'; } const ianaName = match[19]; const annotations = match[20]; let calendar; for (const [, critical, key, value] of annotations.matchAll(PARSE.annotation)) { if (key === 'u-ca') { if (calendar === undefined) calendar = value; } else if (critical === '!') { throw new RangeError(`Unrecognized annotation: !${key}=${value}`); } } RejectDateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond); return { year, month, day, hasTime, hour, minute, second, millisecond, microsecond, nanosecond, ianaName, offset, z, calendar }; } // ts-prune-ignore-next TODO: remove if test/validStrings is converted to TS. export function ParseTemporalInstantString(isoString: string) { const result = ParseISODateTime(isoString); if (!result.z && !result.offset) throw new RangeError('Temporal.Instant requires a time zone offset'); return result; } // ts-prune-ignore-next TODO: remove if test/validStrings is converted to TS. export function ParseTemporalZonedDateTimeString(isoString: string) { const result = ParseISODateTime(isoString); if (!result.ianaName) throw new RangeError('Temporal.ZonedDateTime requires a time zone ID in brackets'); return result; } // ts-prune-ignore-next TODO: remove if test/validStrings is converted to TS. export function ParseTemporalDateTimeString(isoString: string) { return ParseISODateTime(isoString); } // ts-prune-ignore-next TODO: remove if test/validStrings is converted to TS. export function ParseTemporalDateString(isoString: string) { return ParseISODateTime(isoString); } // ts-prune-ignore-next TODO: remove if test/validStrings is converted to TS. export function ParseTemporalTimeString(isoString: string) { const match = PARSE.time.exec(isoString); let hour, minute, second, millisecond, microsecond, nanosecond, annotations; if (match) { hour = ToIntegerOrInfinity(match[1]); minute = ToIntegerOrInfinity(match[2] || match[5]); second = ToIntegerOrInfinity(match[3] || match[6]); if (second === 60) second = 59; const fraction = (match[4] || match[7]) + '000000000'; millisecond = ToIntegerOrInfinity(fraction.slice(0, 3)); microsecond = ToIntegerOrInfinity(fraction.slice(3, 6)); nanosecond = ToIntegerOrInfinity(fraction.slice(6, 9)); annotations = match[14]; for (const [, critical, key, value] of annotations.matchAll(PARSE.annotation)) { if (key !== 'u-ca' && critical === '!') { throw new RangeError(`Unrecognized annotation: !${key}=${value}`); } } if (match[8]) throw new RangeError('Z designator not supported for PlainTime'); } else { let z, hasTime; ({ hasTime, hour, minute, second, millisecond, microsecond, nanosecond, z } = ParseISODateTime(isoString)); if (!hasTime) throw new RangeError(`time is missing in string: ${isoString}`); if (z) throw new RangeError('Z designator not supported for PlainTime'); } // if it's a date-time string, OK if (/[tT ][0-9][0-9]/.test(isoString)) { return { hour, minute, second, millisecond, microsecond, nanosecond }; } try { const { month, day } = ParseTemporalMonthDayString(isoString); RejectISODate(1972, month, day); } catch { try { const { year, month } = ParseTemporalYearMonthString(isoString); RejectISODate(year, month, 1); } catch { return { hour, minute, second, millisecond, microsecond, nanosecond }; } } throw new RangeError(`invalid ISO 8601 time-only string ${isoString}; may need a T prefix`); } // ts-prune-ignore-next TODO: remove if test/validStrings is converted to TS. export function ParseTemporalYearMonthString(isoString: string) { const match = PARSE.yearmonth.exec(isoString); let year, month, calendar, referenceISODay; if (match) { let yearString = match[1]; if (yearString[0] === '\u2212') yearString = `-${yearString.slice(1)}`; if (yearString === '-000000') throw new RangeError(`invalid ISO 8601 string: ${isoString}`); year = ToIntegerOrInfinity(yearString); month = ToIntegerOrInfinity(match[2]); const annotations = match[3]; for (const [, critical, key, value] of annotations.matchAll(PARSE.annotation)) { if (key === 'u-ca') { if (calendar === undefined) calendar = value; } else if (critical === '!') { throw new RangeError(`Unrecognized annotation: !${key}=${value}`); } } if (calendar !== undefined && calendar !== 'iso8601') { throw new RangeError('YYYY-MM format is only valid with iso8601 calendar'); } } else { let z; ({ year, month, calendar, day: referenceISODay, z } = ParseISODateTime(isoString)); if (z) throw new RangeError('Z designator not supported for PlainYearMonth'); } return { year, month, calendar, referenceISODay }; } // ts-prune-ignore-next TODO: remove if test/validStrings is converted to TS. export function ParseTemporalMonthDayString(isoString: string) { const match = PARSE.monthday.exec(isoString); let month, day, calendar, referenceISOYear; if (match) { month = ToIntegerOrInfinity(match[1]); day = ToIntegerOrInfinity(match[2]); const annotations = match[3]; for (const [, critical, key, value] of annotations.matchAll(PARSE.annotation)) { if (key === 'u-ca') { if (calendar === undefined) calendar = value; } else if (critical === '!') { throw new RangeError(`Unrecognized annotation: !${key}=${value}`); } } if (calendar !== undefined && calendar !== 'iso8601') { throw new RangeError('MM-DD format is only valid with iso8601 calendar'); } } else { let z; ({ month, day, calendar, year: referenceISOYear, z } = ParseISODateTime(isoString)); if (z) throw new RangeError('Z designator not supported for PlainMonthDay'); } return { month, day, calendar, referenceISOYear }; } // ts-prune-ignore-next TODO: remove if test/validStrings is converted to TS. export function ParseTemporalTimeZoneString(stringIdent: string): Partial<{ ianaName: string | undefined; offset: string | undefined; z: boolean | undefined; }> { const bareID = new RegExp(`^${PARSE.timeZoneID.source}$`, 'i'); if (bareID.test(stringIdent)) return { ianaName: stringIdent }; try { // Try parsing ISO string instead const result = ParseISODateTime(stringIdent); if (result.z || result.offset || result.ianaName) { return result; } } catch { // fall through } throw new RangeError(`Invalid time zone: ${stringIdent}`); } // ts-prune-ignore-next TODO: remove if test/validStrings is converted to TS. export function ParseTemporalDurationString(isoString: string) { const match = PARSE.duration.exec(isoString); if (!match) throw new RangeError(`invalid duration: ${isoString}`); if (match.slice(2).every((element) => element === undefined)) { throw new RangeError(`invalid duration: ${isoString}`); } const sign = match[1] === '-' || match[1] === '\u2212' ? -1 : 1; const years = match[2] === undefined ? 0 : ToIntegerWithTruncation(match[2]) * sign; const months = match[3] === undefined ? 0 : ToIntegerWithTruncation(match[3]) * sign; const weeks = match[4] === undefined ? 0 : ToIntegerWithTruncation(match[4]) * sign; const days = match[5] === undefined ? 0 : ToIntegerWithTruncation(match[5]) * sign; const hours = match[6] === undefined ? 0 : ToIntegerWithTruncation(match[6]) * sign; const fHours = match[7]; const minutesStr = match[8]; const fMinutes = match[9]; const secondsStr = match[10]; const fSeconds = match[11]; let minutes = 0; let seconds = 0; // fractional hours, minutes, or seconds, expressed in whole nanoseconds: let excessNanoseconds = 0; if (fHours !== undefined) { if (minutesStr ?? fMinutes ?? secondsStr ?? fSeconds ?? false) { throw new RangeError('only the smallest unit can be fractional'); } excessNanoseconds = ToIntegerOrInfinity((fHours + '000000000').slice(0, 9)) * 3600 * sign; } else { minutes = minutesStr === undefined ? 0 : ToIntegerWithTruncation(minutesStr) * sign; if (fMinutes !== undefined) { if (secondsStr ?? fSeconds ?? false) { throw new RangeError('only the smallest unit can be fractional'); } excessNanoseconds = ToIntegerOrInfinity((fMinutes + '000000000').slice(0, 9)) * 60 * sign; } else { seconds = secondsStr === undefined ? 0 : ToIntegerWithTruncation(secondsStr) * sign; if (fSeconds !== undefined) { excessNanoseconds = ToIntegerOrInfinity((fSeconds + '000000000').slice(0, 9)) * sign; } } } const nanoseconds = excessNanoseconds % 1000; const microseconds = MathTrunc(excessNanoseconds / 1000) % 1000; const milliseconds = MathTrunc(excessNanoseconds / 1e6) % 1000; seconds += MathTrunc(excessNanoseconds / 1e9) % 60; minutes += MathTrunc(excessNanoseconds / 6e10); RejectDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; } // ts-prune-ignore-next TODO: remove if test/validStrings is converted to TS. export function ParseTemporalInstant(isoString: string) { let { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, offset, z } = ParseTemporalInstantString(isoString); if (!z && !offset) throw new RangeError('Temporal.Instant requires a time zone offset'); // At least one of z or offset is defined, but TS doesn't seem to understand // that we only use offset if z is not defined (and thus offset must be defined). // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion const offsetNs = z ? 0 : ParseTimeZoneOffsetString(offset!); ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = BalanceISODateTime( year, month, day, hour, minute, second, millisecond, microsecond, nanosecond - offsetNs )); const epochNs = GetUTCEpochNanoseconds(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond); if (epochNs === null) throw new RangeError('DateTime outside of supported range'); return epochNs; } export function RegulateISODate( yearParam: number, monthParam: number, dayParam: number, overflow: Temporal.ArithmeticOptions['overflow'] ) { let year = yearParam; let month = monthParam; let day = dayParam; switch (overflow) { case 'reject': RejectISODate(year, month, day); break; case 'constrain': ({ year, month, day } = ConstrainISODate(year, month, day)); break; } return { year, month, day }; } export function RegulateTime( hourParam: number, minuteParam: number, secondParam: number, millisecondParam: number, microsecondParam: number, nanosecondParam: number, overflow: Temporal.ArithmeticOptions['overflow'] ) { let hour = hourParam; let minute = minuteParam; let second = secondParam; let millisecond = millisecondParam; let microsecond = microsecondParam; let nanosecond = nanosecondParam; switch (overflow) { case 'reject': RejectTime(hour, minute, second, millisecond, microsecond, nanosecond); break; case 'constrain': ({ hour, minute, second, millisecond, microsecond, nanosecond } = ConstrainTime( hour, minute, second, millisecond, microsecond, nanosecond )); break; } return { hour, minute, second, millisecond, microsecond, nanosecond }; } export function RegulateISOYearMonth( yearParam: number, monthParam: number, overflow: Temporal.ArithmeticOptions['overflow'] ) { let year = yearParam; let month = monthParam; const referenceISODay = 1; switch (overflow) { case 'reject': RejectISODate(year, month, referenceISODay); break; case 'constrain': ({ year, month } = ConstrainISODate(year, month)); break; } return { year, month }; } function ToTemporalDurationRecord(item: Temporal.DurationLike | string) { if (!IsObject(item)) { return ParseTemporalDurationString(ToString(item)); } if (IsTemporalDuration(item)) { return { years: GetSlot(item, YEARS), months: GetSlot(item, MONTHS), weeks: GetSlot(item, WEEKS), days: GetSlot(item, DAYS), hours: GetSlot(item, HOURS), minutes: GetSlot(item, MINUTES), seconds: GetSlot(item, SECONDS), milliseconds: GetSlot(item, MILLISECONDS), microseconds: GetSlot(item, MICROSECONDS), nanoseconds: GetSlot(item, NANOSECONDS) }; } const result = { years: 0, months: 0, weeks: 0, days: 0, hours: 0, minutes: 0, seconds: 0, milliseconds: 0, microseconds: 0, nanoseconds: 0 }; let partial = ToTemporalPartialDurationRecord(item); for (const property of DURATION_FIELDS) { const value = partial[property]; if (value !== undefined) { result[property] = value; } } let { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = result; RejectDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; } function ToTemporalPartialDurationRecord(temporalDurationLike: Temporal.DurationLike | string) { if (!IsObject(temporalDurationLike)) { throw new TypeError('invalid duration-like'); } const result: Record = { years: undefined, months: undefined, weeks: undefined, days: undefined, hours: undefined, minutes: undefined, seconds: undefined, milliseconds: undefined, microseconds: undefined, nanoseconds: undefined }; let any = false; for (const property of DURATION_FIELDS) { const value = temporalDurationLike[property]; if (value !== undefined) { any = true; result[property] = ToIntegerIfIntegral(value); } } if (!any) { throw new TypeError('invalid duration-like'); } return result; } function ToLimitedTemporalDuration( item: Temporal.DurationLike | string, disallowedProperties: (keyof Temporal.DurationLike)[] ) { let record = ToTemporalDurationRecord(item); for (const property of disallowedProperties) { if (record[property] !== 0) { throw new RangeError( `Duration field ${property} not supported by Temporal.Instant. Try Temporal.ZonedDateTime instead.` ); } } return record; } export function ToTemporalOverflow(options: Temporal.AssignmentOptions | undefined) { if (options === undefined) return 'constrain'; return GetOption(options, 'overflow', ['constrain', 'reject'], 'constrain'); } export function ToTemporalDisambiguation(options: Temporal.ToInstantOptions | undefined) { if (options === undefined) return 'compatible'; return GetOption(options, 'disambiguation', ['compatible', 'earlier', 'later', 'reject'], 'compatible'); } export function ToTemporalRoundingMode( options: { roundingMode?: Temporal.RoundingMode }, fallback: Temporal.RoundingMode ) { return GetOption( options, 'roundingMode', ['ceil', 'floor', 'expand', 'trunc', 'halfCeil', 'halfFloor', 'halfExpand', 'halfTrunc', 'halfEven'], fallback ); } function NegateTemporalRoundingMode(roundingMode: Temporal.RoundingMode) { switch (roundingMode) { case 'ceil': return 'floor'; case 'floor': return 'ceil'; case 'halfCeil': return 'halfFloor'; case 'halfFloor': return 'halfCeil'; default: return roundingMode; } } export function ToTemporalOffset( options: Temporal.OffsetDisambiguationOptions | undefined, fallback: Required['offset'] ) { if (options === undefined) return fallback; return GetOption(options, 'offset', ['prefer', 'use', 'ignore', 'reject'], fallback); } export function ToCalendarNameOption(options: Temporal.ShowCalendarOption) { return GetOption(options, 'calendarName', ['auto', 'always', 'never', 'critical'], 'auto'); } export function ToTimeZoneNameOption(options: Temporal.ZonedDateTimeToStringOptions) { return GetOption(options, 'timeZoneName', ['auto', 'never', 'critical'], 'auto'); } export function ToShowOffsetOption(options: Temporal.ZonedDateTimeToStringOptions) { return GetOption(options, 'offset', ['auto', 'never'], 'auto'); } export function ToTemporalRoundingIncrement(options: { roundingIncrement?: number }) { let increment = options.roundingIncrement; if (increment === undefined) return 1; increment = ToNumber(increment); if (!NumberIsFinite(increment)) { throw new RangeError('roundingIncrement must be finite'); } const integerIncrement = MathTrunc(increment); if (integerIncrement < 1 || integerIncrement > 1e9) { throw new RangeError(`roundingIncrement must be at least 1 and at most 1e9, not ${increment}`); } return integerIncrement; } export function ValidateTemporalRoundingIncrement(increment: number, dividend: number, inclusive: boolean) { const maximum = inclusive ? dividend : dividend - 1; if (increment > maximum) { throw new RangeError(`roundingIncrement must be at least 1 and less than ${maximum}, not ${increment}`); } if (dividend % increment !== 0) { throw new RangeError(`Rounding increment must divide evenly into ${dividend}`); } } export function ToFractionalSecondDigits( normalizedOptions: Temporal.ToStringPrecisionOptions ): Temporal.ToStringPrecisionOptions['fractionalSecondDigits'] { const digitsValue = normalizedOptions.fractionalSecondDigits; if (digitsValue === undefined) return 'auto'; if (typeof digitsValue !== 'number') { if (ToString(digitsValue) !== 'auto') { throw new RangeError(`fractionalSecondDigits must be 'auto' or 0 through 9, not ${digitsValue}`); } return 'auto'; } const digitCount = MathFloor(digitsValue); if (!NumberIsFinite(digitCount) || digitCount < 0 || digitCount > 9) { throw new RangeError(`fractionalSecondDigits must be 'auto' or 0 through 9, not ${digitsValue}`); } return digitCount as Exclude; } export function ToSecondsStringPrecisionRecord( smallestUnit: Temporal.ToStringPrecisionOptions['smallestUnit'], precision: Temporal.ToStringPrecisionOptions['fractionalSecondDigits'] ): { precision: Temporal.ToStringPrecisionOptions['fractionalSecondDigits'] | 'minute'; unit: UnitSmallerThanOrEqualTo<'minute'>; increment: number; } { switch (smallestUnit) { case 'minute': return { precision: 'minute', unit: 'minute', increment: 1 }; case 'second': return { precision: 0, unit: 'second', increment: 1 }; case 'millisecond': return { precision: 3, unit: 'millisecond', increment: 1 }; case 'microsecond': return { precision: 6, unit: 'microsecond', increment: 1 }; case 'nanosecond': return { precision: 9, unit: 'nanosecond', increment: 1 }; default: // fall through if option not given } switch (precision) { case 'auto': return { precision, unit: 'nanosecond', increment: 1 }; case 0: return { precision, unit: 'second', increment: 1 }; case 1: case 2: case 3: return { precision, unit: 'millisecond', increment: 10 ** (3 - precision) }; case 4: case 5: case 6: return { precision, unit: 'microsecond', increment: 10 ** (6 - precision) }; case 7: case 8: case 9: return { precision, unit: 'nanosecond', increment: 10 ** (9 - precision) }; default: throw new RangeError(`fractionalSecondDigits must be 'auto' or 0 through 9, not ${precision}`); } } export const REQUIRED = Symbol('~required~'); interface TemporalUnitOptionsBag { smallestUnit?: Temporal.PluralUnit | Temporal.DateTimeUnit; largestUnit?: Temporal.PluralUnit | Temporal.DateTimeUnit | 'auto'; unit?: Temporal.PluralUnit | Temporal.DateTimeUnit; } type UnitTypeMapping = { date: Temporal.DateUnit; time: Temporal.TimeUnit; datetime: Temporal.DateTimeUnit; }; // This type specifies the allowed defaults for each unit key type. type AllowedGetTemporalUnitDefaultValues = { smallestUnit: undefined; largestUnit: 'auto' | undefined; unit: undefined; }; export function GetTemporalUnit< U extends keyof TemporalUnitOptionsBag, T extends keyof UnitTypeMapping, D extends typeof REQUIRED | UnitTypeMapping[T] | AllowedGetTemporalUnitDefaultValues[U], R extends Exclude | UnitTypeMapping[T] >(options: TemporalUnitOptionsBag, key: U, unitGroup: T, requiredOrDefault: D): R; export function GetTemporalUnit< U extends keyof TemporalUnitOptionsBag, T extends keyof UnitTypeMapping, D extends typeof REQUIRED | UnitTypeMapping[T] | AllowedGetTemporalUnitDefaultValues[U], E extends 'auto' | Temporal.DateTimeUnit, R extends UnitTypeMapping[T] | Exclude | E >(options: TemporalUnitOptionsBag, key: U, unitGroup: T, requiredOrDefault: D, extraValues: ReadonlyArray): R; // This signature of the function is NOT used in type-checking, so restricting // the default value via generic binding like the other overloads isn't // necessary. export function GetTemporalUnit< T extends keyof UnitTypeMapping, D extends typeof REQUIRED | UnitTypeMapping[T] | 'auto' | undefined, E extends 'auto' | Temporal.DateTimeUnit, R extends UnitTypeMapping[T] | Exclude | E >( options: TemporalUnitOptionsBag, key: keyof typeof options, unitGroup: T, requiredOrDefault: D, extraValues: ReadonlyArray | never[] = [] ): R { const allowedSingular: Array = []; for (const [, singular, category] of SINGULAR_PLURAL_UNITS) { if (unitGroup === 'datetime' || unitGroup === category) { allowedSingular.push(singular); } } allowedSingular.push(...extraValues); let defaultVal: typeof REQUIRED | Temporal.DateTimeUnit | 'auto' | undefined = requiredOrDefault; if (defaultVal === REQUIRED) { defaultVal = undefined; } else if (defaultVal !== undefined) { allowedSingular.push(defaultVal); } const allowedValues: Array | 'auto'> = [ ...allowedSingular ]; for (const singular of allowedSingular) { const plural = PLURAL_FOR.get(singular as Parameters[0]); if (plural !== undefined) allowedValues.push(plural); } let retval = GetOption(options, key, allowedValues, defaultVal); if (retval === undefined && requiredOrDefault === REQUIRED) { throw new RangeError(`${key} is required`); } // Coerce any plural units into their singular form if (SINGULAR_FOR.has(retval as Temporal.PluralUnit)) { // We just has-checked this, but tsc doesn't understand that. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return SINGULAR_FOR.get(retval as Temporal.PluralUnit)! as R; } return retval as R; } export function ToRelativeTemporalObject(options: { relativeTo?: | Temporal.ZonedDateTime | Temporal.PlainDateTime | Temporal.ZonedDateTimeLike | Temporal.PlainDateTimeLike | string | undefined; }): Temporal.ZonedDateTime | Temporal.PlainDate | undefined { const relativeTo = options.relativeTo; if (relativeTo === undefined) return relativeTo; let offsetBehaviour: OffsetBehaviour = 'option'; let matchMinutes = false; let year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, timeZone, offset; if (IsObject(relativeTo)) { if (IsTemporalZonedDateTime(relativeTo) || IsTemporalDate(relativeTo)) return relativeTo; if (IsTemporalDateTime(relativeTo)) return TemporalDateTimeToDate(relativeTo); calendar = GetTemporalCalendarSlotValueWithISODefault(relativeTo); const fieldNames = CalendarFields(calendar, [ 'day', 'hour', 'microsecond', 'millisecond', 'minute', 'month', 'monthCode', 'nanosecond', 'second', 'year' ] as const); type FieldNamesWithTimeZoneAndOffset = ArrayWithNewKeys; (fieldNames as FieldNamesWithTimeZoneAndOffset).push('timeZone', 'offset'); const fields = PrepareTemporalFields(relativeTo, fieldNames, []); const dateOptions = ObjectCreate(null) as Temporal.AssignmentOptions; dateOptions.overflow = 'constrain'; ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = InterpretTemporalDateTimeFields( calendar, fields, dateOptions )); offset = fields.offset; if (offset === undefined) offsetBehaviour = 'wall'; timeZone = fields.timeZone; if (timeZone !== undefined) timeZone = ToTemporalTimeZoneSlotValue(timeZone); } else { let ianaName, z; ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, ianaName, offset, z } = ParseISODateTime(ToString(relativeTo))); if (ianaName) { timeZone = ToTemporalTimeZoneSlotValue(ianaName); if (z) { offsetBehaviour = 'exact'; } else if (!offset) { offsetBehaviour = 'wall'; } matchMinutes = true; } else if (z) { throw new RangeError( 'Z designator not supported for PlainDate relativeTo; either remove the Z or add a bracketed time zone' ); } if (!calendar) calendar = 'iso8601'; if (!IsBuiltinCalendar(calendar)) throw new RangeError(`invalid calendar identifier ${calendar}`); calendar = ASCIILowercase(calendar); } if (timeZone === undefined) return CreateTemporalDate(year, month, day, calendar); // If offset is missing here, then offsetBehavior will never be be 'option'. assertExists(offset); const offsetNs = offsetBehaviour === 'option' ? ParseTimeZoneOffsetString(offset) : 0; const epochNanoseconds = InterpretISODateTimeOffset( year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, offsetBehaviour, offsetNs, timeZone, 'compatible', 'reject', matchMinutes ); return CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar); } export function DefaultTemporalLargestUnit( years: number, months: number, weeks: number, days: number, hours: number, minutes: number, seconds: number, milliseconds: number, microseconds: number, nanoseconds: number ): Temporal.DateTimeUnit { for (const [prop, v] of [ ['years', years], ['months', months], ['weeks', weeks], ['days', days], ['hours', hours], ['minutes', minutes], ['seconds', seconds], ['milliseconds', milliseconds], ['microseconds', microseconds], ['nanoseconds', nanoseconds] ] as const) { if (v !== 0) { // All the above keys are definitely in SINGULAR_FOR // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return SINGULAR_FOR.get(prop)!; } } return 'nanosecond'; } export function LargerOfTwoTemporalUnits( unit1: T1, unit2: T2 ) { if (UNITS_DESCENDING.indexOf(unit1) > UNITS_DESCENDING.indexOf(unit2)) return unit2; return unit1; } type FieldCompleteness = 'complete' | 'partial'; interface FieldPrepareOptions { emptySourceErrorMessage: string; } // Returns all potential owners from all Temporal Like-types for a given union // of keys in K. // e.g. // Owner<'nanosecond'> => PlainDateTimeLike | ZonedDateTimeLike | PlainDateTimeLike | ZonedDateTimeLike // Owner<'nanoseconds'> => Duration (the only type with plural keys) type Owner = // Conditional typing maps over all of the types given in AnyTemporalLikeType // union K extends unknown ? OwnerOf : 'ThisShouldNeverHappen'; // Returns T iff T has K as all of the key(s) (even if those keys are optional // in T), never otherwise. This is a private type for use only in the Owner type // above. type OwnerOf = // Distribute the union before passing to Required // Without distributing, this is // Required extends Record // vs (with distribution) // Required extends Record<....> | Required extends Record<....> T extends unknown ? // All the keys in the Like-types are optional, so in order for them to // 'extend Record', where K indicates the required fields, we pass T // through Required to make all the keys non-optional. // Note this doesn't work the other way around: using Partial> // will always be extended by any object (as all the keys are optional). Required extends Record ? T : // never is the 'identity' type for unions - nothing will be added or // removed from the union. never : 'ThisShouldNeverHappen'; type Prop = T extends unknown ? (K extends keyof T ? T[K] : undefined) : 'ThisShouldNeverHappen'; // Resolve copies the keys and values of a given object type so that TS will // stop using type names in error messages / autocomplete. Generally, those // names can be more useful, but sometimes having the primitive object shape is // significantly easier to reason about (e.g. deeply-nested types). // Resolve is an identity function for function types. type Resolve = // Re-mapping doesn't work very well for functions, so exclude them T extends (...args: never[]) => unknown ? T : // Re-map all the keys in T to the same value. This forces TS into no longer // using type aliases, etc. { [K in keyof T]: T[K] }; type FieldObjectFromOwners = Resolve< // The resulting object type contains: // - All keys in FieldKeys, which are required properties and their values // don't include undefined. // - All the other keys in OwnerT that aren't in FieldKeys, which are optional // properties and their value types explicitly include undefined. { -readonly [k in FieldKeys]: Exclude, undefined>; } & { -readonly [k in Exclude, FieldKeys>]?: Prop | undefined; } >; type PrepareTemporalFieldsReturn< FieldKeys extends AnyTemporalKey, RequiredFieldsOpt extends ReadonlyArray | FieldCompleteness, OwnerT extends Owner > = RequiredFieldsOpt extends 'partial' ? Partial : FieldObjectFromOwners; export function PrepareTemporalFields< FieldKeys extends AnyTemporalKey, // Constrains the Required keys to be a subset of the given field keys // This could have been written directly into the parameter type, but that // causes an unintended effect where the required fields are added to the list // of field keys, even if that key isn't present in 'fields'. // RequiredFieldKeys extends FieldKeys, RequiredFields extends ReadonlyArray | FieldCompleteness >( bag: Partial>, fields: Array, requiredFields: RequiredFields, { emptySourceErrorMessage }: FieldPrepareOptions = { emptySourceErrorMessage: 'no supported properties found' } ): PrepareTemporalFieldsReturn> { const result: Partial> = ObjectCreate(null); let any = false; fields.sort(); for (const property of fields) { let value = bag[property]; if (value !== undefined) { any = true; if (BUILTIN_CASTS.has(property)) { // We just has-checked this map access, so there will definitely be a // value. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion value = BUILTIN_CASTS.get(property)!(value); } result[property] = value; } else if (requiredFields !== 'partial') { // TODO: using .call in this way is not correctly type-checked by tsc. // We might need a type-safe Call wrapper? if (ArrayIncludes.call(requiredFields, property)) { throw new TypeError(`required property '${property}' missing or undefined`); } value = BUILTIN_DEFAULTS.get(property); result[property] = value; } } if (requiredFields === 'partial' && !any) { throw new TypeError(emptySourceErrorMessage); } return result as unknown as PrepareTemporalFieldsReturn>; } interface TimeRecord { hour?: number; minute?: number; second?: number; microsecond?: number; millisecond?: number; nanosecond?: number; } export function ToTemporalTimeRecord(bag: Partial>): Required; export function ToTemporalTimeRecord( bag: Partial>, completeness: 'partial' ): Partial; export function ToTemporalTimeRecord( bag: Partial>, completeness: 'complete' ): Required; export function ToTemporalTimeRecord( bag: Partial>, completeness: FieldCompleteness = 'complete' ): Partial { // NOTE: Field order is sorted to make the sort in PrepareTemporalFields more efficient. const fields: (keyof TimeRecord)[] = ['hour', 'microsecond', 'millisecond', 'minute', 'nanosecond', 'second']; const partial = PrepareTemporalFields(bag, fields, 'partial', { emptySourceErrorMessage: 'invalid time-like' }); const result: Partial = {}; for (const field of fields) { const valueDesc = ObjectGetOwnPropertyDescriptor(partial, field); if (valueDesc !== undefined) { result[field] = valueDesc.value; } else if (completeness === 'complete') { result[field] = 0; } } return result; } export function ToTemporalDate( itemParam: PlainDateParams['from'][0], options?: PlainDateParams['from'][1] ): Temporal.PlainDate { let item = itemParam; if (IsObject(item)) { if (IsTemporalDate(item)) return item; if (IsTemporalZonedDateTime(item)) { ToTemporalOverflow(options); // validate and ignore item = GetPlainDateTimeFor(GetSlot(item, TIME_ZONE), GetSlot(item, INSTANT), GetSlot(item, CALENDAR)); } if (IsTemporalDateTime(item)) { ToTemporalOverflow(options); // validate and ignore return CreateTemporalDate( GetSlot(item, ISO_YEAR), GetSlot(item, ISO_MONTH), GetSlot(item, ISO_DAY), GetSlot(item, CALENDAR) ); } const calendar = GetTemporalCalendarSlotValueWithISODefault(item); const fieldNames = CalendarFields(calendar, ['day', 'month', 'monthCode', 'year'] as const); const fields = PrepareTemporalFields(item, fieldNames, []); return CalendarDateFromFields(calendar, fields, options); } ToTemporalOverflow(options); // validate and ignore let { year, month, day, calendar, z } = ParseTemporalDateString(ToString(item)); if (z) throw new RangeError('Z designator not supported for PlainDate'); if (!calendar) calendar = 'iso8601'; if (!IsBuiltinCalendar(calendar)) throw new RangeError(`invalid calendar identifier ${calendar}`); calendar = ASCIILowercase(calendar); return CreateTemporalDate(year, month, day, calendar); } export function InterpretTemporalDateTimeFields( calendar: CalendarSlot, fields: PrimitiveFieldsOf & Parameters[1], options?: Temporal.AssignmentOptions ) { let { hour, minute, second, millisecond, microsecond, nanosecond } = ToTemporalTimeRecord(fields); const overflow = ToTemporalOverflow(options); const date = CalendarDateFromFields(calendar, fields, options); const year = GetSlot(date, ISO_YEAR); const month = GetSlot(date, ISO_MONTH); const day = GetSlot(date, ISO_DAY); ({ hour, minute, second, millisecond, microsecond, nanosecond } = RegulateTime( hour, minute, second, millisecond, microsecond, nanosecond, overflow )); return { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond }; } export function ToTemporalDateTime(item: PlainDateTimeParams['from'][0], options?: PlainDateTimeParams['from'][1]) { let year: number, month: number, day: number, hour: number, minute: number, second: number, millisecond: number, microsecond: number, nanosecond: number, calendar; if (IsObject(item)) { if (IsTemporalDateTime(item)) return item; if (IsTemporalZonedDateTime(item)) { ToTemporalOverflow(options); // validate and ignore return GetPlainDateTimeFor(GetSlot(item, TIME_ZONE), GetSlot(item, INSTANT), GetSlot(item, CALENDAR)); } if (IsTemporalDate(item)) { ToTemporalOverflow(options); // validate and ignore return CreateTemporalDateTime( GetSlot(item, ISO_YEAR), GetSlot(item, ISO_MONTH), GetSlot(item, ISO_DAY), 0, 0, 0, 0, 0, 0, GetSlot(item, CALENDAR) ); } calendar = GetTemporalCalendarSlotValueWithISODefault(item); const fieldNames = CalendarFields(calendar, [ 'day', 'hour', 'microsecond', 'millisecond', 'minute', 'month', 'monthCode', 'nanosecond', 'second', 'year' ] as const); const fields = PrepareTemporalFields(item, fieldNames, []); ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = InterpretTemporalDateTimeFields( calendar, fields, options )); } else { ToTemporalOverflow(options); // validate and ignore let z; ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, z } = ParseTemporalDateTimeString(ToString(item))); if (z) throw new RangeError('Z designator not supported for PlainDateTime'); RejectDateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond); if (!calendar) calendar = 'iso8601'; if (!IsBuiltinCalendar(calendar)) throw new RangeError(`invalid calendar identifier ${calendar}`); calendar = ASCIILowercase(calendar); } return CreateTemporalDateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar); } export function ToTemporalDuration(item: DurationParams['from'][0]) { if (IsTemporalDuration(item)) return item; let { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ToTemporalDurationRecord(item); const TemporalDuration = GetIntrinsic('%Temporal.Duration%'); return new TemporalDuration( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds ); } export function ToTemporalInstant(item: InstantParams['from'][0]) { if (IsTemporalInstant(item)) return item; if (IsTemporalZonedDateTime(item)) { const TemporalInstant = GetIntrinsic('%Temporal.Instant%'); return new TemporalInstant(GetSlot(item, EPOCHNANOSECONDS)); } const ns = ParseTemporalInstant(ToString(item)); const TemporalInstant = GetIntrinsic('%Temporal.Instant%'); return new TemporalInstant(ns); } export function ToTemporalMonthDay( itemParam: PlainMonthDayParams['from'][0], options?: PlainMonthDayParams['from'][1] ) { let item = itemParam; if (IsObject(item)) { if (IsTemporalMonthDay(item)) return item; let calendar: CalendarSlot, calendarAbsent: boolean; if (HasSlot(item, CALENDAR)) { calendar = GetSlot(item, CALENDAR); calendarAbsent = false; } else { let calendarFromItem = item.calendar; calendarAbsent = calendarFromItem === undefined; if (calendarFromItem === undefined) calendarFromItem = 'iso8601'; calendar = ToTemporalCalendarSlotValue(calendarFromItem); } // HasSlot above adjusts the type of 'item' to include // TypesWithCalendarUnits, which causes type-inference failures below. // This is probably indicative of problems with HasSlot's typing. const fieldNames = CalendarFields(calendar, ['day', 'month', 'monthCode', 'year'] as const); const fields = PrepareTemporalFields(item, fieldNames, []); // Callers who omit the calendar are not writing calendar-independent // code. In that case, `monthCode`/`year` can be omitted; `month` and // `day` are sufficient. Add a `year` to satisfy calendar validation. if (calendarAbsent && fields.month !== undefined && fields.monthCode === undefined && fields.year === undefined) { fields.year = 1972; } return CalendarMonthDayFromFields(calendar, fields, options); } ToTemporalOverflow(options); // validate and ignore let { month, day, referenceISOYear, calendar } = ParseTemporalMonthDayString(ToString(item)); if (calendar === undefined) calendar = 'iso8601'; if (!IsBuiltinCalendar(calendar)) throw new RangeError(`invalid calendar identifier ${calendar}`); calendar = ASCIILowercase(calendar); if (referenceISOYear === undefined) { RejectISODate(1972, month, day); return CreateTemporalMonthDay(month, day, calendar); } const result = CreateTemporalMonthDay(month, day, calendar, referenceISOYear); return CalendarMonthDayFromFields(calendar, result); } export function ToTemporalTime( itemParam: PlainTimeParams['from'][0], overflow: NonNullable['overflow'] = 'constrain' ) { let item = itemParam; let hour, minute, second, millisecond, microsecond, nanosecond; if (IsObject(item)) { if (IsTemporalTime(item)) return item; if (IsTemporalZonedDateTime(item)) { item = GetPlainDateTimeFor(GetSlot(item, TIME_ZONE), GetSlot(item, INSTANT), GetSlot(item, CALENDAR)); } if (IsTemporalDateTime(item)) { const TemporalPlainTime = GetIntrinsic('%Temporal.PlainTime%'); return new TemporalPlainTime( GetSlot(item, ISO_HOUR), GetSlot(item, ISO_MINUTE), GetSlot(item, ISO_SECOND), GetSlot(item, ISO_MILLISECOND), GetSlot(item, ISO_MICROSECOND), GetSlot(item, ISO_NANOSECOND) ); } ({ hour, minute, second, millisecond, microsecond, nanosecond } = ToTemporalTimeRecord(item)); ({ hour, minute, second, millisecond, microsecond, nanosecond } = RegulateTime( hour, minute, second, millisecond, microsecond, nanosecond, overflow )); } else { ({ hour, minute, second, millisecond, microsecond, nanosecond } = ParseTemporalTimeString(ToString(item))); RejectTime(hour, minute, second, millisecond, microsecond, nanosecond); } const TemporalPlainTime = GetIntrinsic('%Temporal.PlainTime%'); return new TemporalPlainTime(hour, minute, second, millisecond, microsecond, nanosecond); } export function ToTemporalYearMonth( item: PlainYearMonthParams['from'][0], options?: PlainYearMonthParams['from'][1] ): Temporal.PlainYearMonth { if (IsObject(item)) { if (IsTemporalYearMonth(item)) return item; const calendar = GetTemporalCalendarSlotValueWithISODefault(item); const fieldNames = CalendarFields(calendar, ['month', 'monthCode', 'year'] as const); const fields = PrepareTemporalFields(item, fieldNames, []); return CalendarYearMonthFromFields(calendar, fields, options); } ToTemporalOverflow(options); // validate and ignore let { year, month, referenceISODay, calendar } = ParseTemporalYearMonthString(ToString(item)); if (calendar === undefined) calendar = 'iso8601'; if (!IsBuiltinCalendar(calendar)) throw new RangeError(`invalid calendar identifier ${calendar}`); calendar = ASCIILowercase(calendar); if (referenceISODay === undefined) { RejectISODate(year, month, 1); return CreateTemporalYearMonth(year, month, calendar); } const result = CreateTemporalYearMonth(year, month, calendar, referenceISODay); return CalendarYearMonthFromFields(calendar, result); } type OffsetBehaviour = 'wall' | 'exact' | 'option'; export function InterpretISODateTimeOffset( year: number, month: number, day: number, hour: number, minute: number, second: number, millisecond: number, microsecond: number, nanosecond: number, offsetBehaviour: OffsetBehaviour, offsetNs: number, timeZone: string | Temporal.TimeZoneProtocol, disambiguation: NonNullable, offsetOpt: Temporal.OffsetDisambiguationOptions['offset'], matchMinute: boolean ) { const DateTime = GetIntrinsic('%Temporal.PlainDateTime%'); const dt = new DateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond); if (offsetBehaviour === 'wall' || offsetOpt === 'ignore') { // Simple case: ISO string without a TZ offset (or caller wants to ignore // the offset), so just convert DateTime to Instant in the given time zone const instant = GetInstantFor(timeZone, dt, disambiguation); return GetSlot(instant, EPOCHNANOSECONDS); } // The caller wants the offset to always win ('use') OR the caller is OK // with the offset winning ('prefer' or 'reject') as long as it's valid // for this timezone and date/time. if (offsetBehaviour === 'exact' || offsetOpt === 'use') { // Calculate the instant for the input's date/time and offset const epochNs = GetUTCEpochNanoseconds( year, month, day, hour, minute, second, millisecond, microsecond, nanosecond ); if (epochNs === null) throw new RangeError('ZonedDateTime outside of supported range'); return JSBI.subtract(epochNs, JSBI.BigInt(offsetNs)); } // "prefer" or "reject" const possibleInstants = GetPossibleInstantsFor(timeZone, dt); for (const candidate of possibleInstants) { const candidateOffset = GetOffsetNanosecondsFor(timeZone, candidate); const roundedCandidateOffset = JSBI.toNumber( RoundNumberToIncrement(JSBI.BigInt(candidateOffset), MINUTE_NANOS, 'halfExpand') ); if (candidateOffset === offsetNs || (matchMinute && roundedCandidateOffset === offsetNs)) { return GetSlot(candidate, EPOCHNANOSECONDS); } } // the user-provided offset doesn't match any instants for this time // zone and date/time. if (offsetOpt === 'reject') { const offsetStr = FormatTimeZoneOffsetString(offsetNs); const timeZoneString = IsTemporalTimeZone(timeZone) ? GetSlot(timeZone, TIMEZONE_ID) : 'time zone'; // The tsc emit for this line rewrites to invoke the PlainDateTime's valueOf method, NOT // toString (which is invoked by Node when using template literals directly). // See https://github.com/microsoft/TypeScript/issues/39744 for the proposed fix in tsc emit throw new RangeError(`Offset ${offsetStr} is invalid for ${dt.toString()} in ${timeZoneString}`); } // fall through: offsetOpt === 'prefer', but the offset doesn't match // so fall back to use the time zone instead. const instant = DisambiguatePossibleInstants(possibleInstants, timeZone, dt, disambiguation); return GetSlot(instant, EPOCHNANOSECONDS); } export function ToTemporalZonedDateTime( item: ZonedDateTimeParams['from'][0], options?: ZonedDateTimeParams['from'][1] ) { let year: number, month: number, day: number, hour: number, minute: number, second: number, millisecond: number, microsecond: number, nanosecond: number, timeZone, offset: string | undefined, calendar: string | Temporal.CalendarProtocol | undefined; let disambiguation: NonNullable; let offsetOpt: NonNullable; let matchMinute = false; let offsetBehaviour: OffsetBehaviour = 'option'; if (IsObject(item)) { if (IsTemporalZonedDateTime(item)) return item; calendar = GetTemporalCalendarSlotValueWithISODefault(item); const fieldNames: (keyof Temporal.ZonedDateTimeLike)[] = CalendarFields(calendar, [ 'day', 'hour', 'microsecond', 'millisecond', 'minute', 'month', 'monthCode', 'nanosecond', 'second', 'year' ] as const); fieldNames.push('timeZone', 'offset'); const fields = PrepareTemporalFields(item, fieldNames, ['timeZone']); timeZone = ToTemporalTimeZoneSlotValue(fields.timeZone); offset = fields.offset; if (offset === undefined) { offsetBehaviour = 'wall'; } disambiguation = ToTemporalDisambiguation(options); offsetOpt = ToTemporalOffset(options, 'reject'); ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = InterpretTemporalDateTimeFields( calendar, fields, options )); } else { let ianaName, z; ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, ianaName, offset, z, calendar } = ParseTemporalZonedDateTimeString(ToString(item))); timeZone = ToTemporalTimeZoneSlotValue(ianaName); if (z) { offsetBehaviour = 'exact'; } else if (!offset) { offsetBehaviour = 'wall'; } if (!calendar) calendar = 'iso8601'; if (!IsBuiltinCalendar(calendar)) throw new RangeError(`invalid calendar identifier ${calendar}`); calendar = ASCIILowercase(calendar); matchMinute = true; // ISO strings may specify offset with less precision disambiguation = ToTemporalDisambiguation(options); offsetOpt = ToTemporalOffset(options, 'reject'); ToTemporalOverflow(options); // validate and ignore } let offsetNs = 0; // The code above guarantees that if offsetBehaviour === 'option', then // `offset` is not undefined. if (offsetBehaviour === 'option') offsetNs = ParseTimeZoneOffsetString(offset as string); const epochNanoseconds = InterpretISODateTimeOffset( year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, offsetBehaviour, offsetNs, timeZone, disambiguation, offsetOpt, matchMinute ); return CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar); } export function CreateTemporalDateSlots( result: Temporal.PlainDate, isoYear: number, isoMonth: number, isoDay: number, calendar: CalendarSlot ) { RejectISODate(isoYear, isoMonth, isoDay); RejectDateRange(isoYear, isoMonth, isoDay); CreateSlots(result); SetSlot(result, ISO_YEAR, isoYear); SetSlot(result, ISO_MONTH, isoMonth); SetSlot(result, ISO_DAY, isoDay); SetSlot(result, CALENDAR, calendar); SetSlot(result, DATE_BRAND, true); if (DEBUG) { ObjectDefineProperty(result, '_repr_', { value: `${result[Symbol.toStringTag]} <${TemporalDateToString(result)}>`, writable: false, enumerable: false, configurable: false }); } } export function CreateTemporalDate( isoYear: number, isoMonth: number, isoDay: number, calendar: CalendarSlot = 'iso8601' ) { const TemporalPlainDate = GetIntrinsic('%Temporal.PlainDate%'); const result = ObjectCreate(TemporalPlainDate.prototype); CreateTemporalDateSlots(result, isoYear, isoMonth, isoDay, calendar); return result; } export function CreateTemporalDateTimeSlots( result: Temporal.PlainDateTime, isoYear: number, isoMonth: number, isoDay: number, h: number, min: number, s: number, ms: number, µs: number, ns: number, calendar: CalendarSlot ) { RejectDateTime(isoYear, isoMonth, isoDay, h, min, s, ms, µs, ns); RejectDateTimeRange(isoYear, isoMonth, isoDay, h, min, s, ms, µs, ns); CreateSlots(result); SetSlot(result, ISO_YEAR, isoYear); SetSlot(result, ISO_MONTH, isoMonth); SetSlot(result, ISO_DAY, isoDay); SetSlot(result, ISO_HOUR, h); SetSlot(result, ISO_MINUTE, min); SetSlot(result, ISO_SECOND, s); SetSlot(result, ISO_MILLISECOND, ms); SetSlot(result, ISO_MICROSECOND, µs); SetSlot(result, ISO_NANOSECOND, ns); SetSlot(result, CALENDAR, calendar); if (DEBUG) { Object.defineProperty(result, '_repr_', { value: `${result[Symbol.toStringTag]} <${TemporalDateTimeToString(result, 'auto')}>`, writable: false, enumerable: false, configurable: false }); } } export function CreateTemporalDateTime( isoYear: number, isoMonth: number, isoDay: number, h: number, min: number, s: number, ms: number, µs: number, ns: number, calendar: CalendarSlot = 'iso8601' ) { const TemporalPlainDateTime = GetIntrinsic('%Temporal.PlainDateTime%'); const result = ObjectCreate(TemporalPlainDateTime.prototype); CreateTemporalDateTimeSlots(result, isoYear, isoMonth, isoDay, h, min, s, ms, µs, ns, calendar); return result as Temporal.PlainDateTime; } export function CreateTemporalMonthDaySlots( result: Temporal.PlainMonthDay, isoMonth: number, isoDay: number, calendar: CalendarSlot, referenceISOYear: number ) { RejectISODate(referenceISOYear, isoMonth, isoDay); RejectDateRange(referenceISOYear, isoMonth, isoDay); CreateSlots(result); SetSlot(result, ISO_MONTH, isoMonth); SetSlot(result, ISO_DAY, isoDay); SetSlot(result, ISO_YEAR, referenceISOYear); SetSlot(result, CALENDAR, calendar); SetSlot(result, MONTH_DAY_BRAND, true); if (DEBUG) { Object.defineProperty(result, '_repr_', { value: `${result[Symbol.toStringTag]} <${TemporalMonthDayToString(result)}>`, writable: false, enumerable: false, configurable: false }); } } export function CreateTemporalMonthDay( isoMonth: number, isoDay: number, calendar: CalendarSlot = 'iso8601', referenceISOYear = 1972 ) { const TemporalPlainMonthDay = GetIntrinsic('%Temporal.PlainMonthDay%'); const result = ObjectCreate(TemporalPlainMonthDay.prototype); CreateTemporalMonthDaySlots(result, isoMonth, isoDay, calendar, referenceISOYear); return result; } export function CreateTemporalYearMonthSlots( result: Temporal.PlainYearMonth, isoYear: number, isoMonth: number, calendar: CalendarSlot, referenceISODay: number ) { RejectISODate(isoYear, isoMonth, referenceISODay); RejectYearMonthRange(isoYear, isoMonth); CreateSlots(result); SetSlot(result, ISO_YEAR, isoYear); SetSlot(result, ISO_MONTH, isoMonth); SetSlot(result, ISO_DAY, referenceISODay); SetSlot(result, CALENDAR, calendar); SetSlot(result, YEAR_MONTH_BRAND, true); if (DEBUG) { Object.defineProperty(result, '_repr_', { value: `${result[Symbol.toStringTag]} <${TemporalYearMonthToString(result)}>`, writable: false, enumerable: false, configurable: false }); } } export function CreateTemporalYearMonth( isoYear: number, isoMonth: number, calendar: CalendarSlot = 'iso8601', referenceISODay = 1 ) { const TemporalPlainYearMonth = GetIntrinsic('%Temporal.PlainYearMonth%'); const result = ObjectCreate(TemporalPlainYearMonth.prototype); CreateTemporalYearMonthSlots(result, isoYear, isoMonth, calendar, referenceISODay); return result; } export function CreateTemporalZonedDateTimeSlots( result: Temporal.ZonedDateTime, epochNanoseconds: JSBI, timeZone: string | Temporal.TimeZoneProtocol, calendar: CalendarSlot ) { ValidateEpochNanoseconds(epochNanoseconds); CreateSlots(result); SetSlot(result, EPOCHNANOSECONDS, epochNanoseconds); SetSlot(result, TIME_ZONE, timeZone); SetSlot(result, CALENDAR, calendar); const TemporalInstant = GetIntrinsic('%Temporal.Instant%'); const instant = new TemporalInstant(GetSlot(result, EPOCHNANOSECONDS)); SetSlot(result, INSTANT, instant); if (DEBUG) { Object.defineProperty(result, '_repr_', { value: `${result[Symbol.toStringTag]} <${TemporalZonedDateTimeToString(result, 'auto')}>`, writable: false, enumerable: false, configurable: false }); } } export function CreateTemporalZonedDateTime( epochNanoseconds: JSBI, timeZone: string | Temporal.TimeZoneProtocol, calendar: CalendarSlot = 'iso8601' ) { const TemporalZonedDateTime = GetIntrinsic('%Temporal.ZonedDateTime%'); const result = ObjectCreate(TemporalZonedDateTime.prototype); CreateTemporalZonedDateTimeSlots(result, epochNanoseconds, timeZone, calendar); return result; } // TODO: should (can?) we make this generic so the field names are checked // against the type that the calendar is a property of? export function CalendarFields(calendar: CalendarSlot, fieldNamesParam: ReadonlyArray) { if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.fields%'), calendarObj, [fieldNamesParam]) as K[]; } const fields = GetMethod(calendar, 'fields'); const fieldNames = Call(fields, calendar, [fieldNamesParam]); const result: K[] = []; for (const name of fieldNames) { if (typeof name !== 'string') throw new TypeError('bad return from calendar.fields()'); ArrayPrototypePush.call(result, name); } return result; } export function CalendarMergeFields, ToAdd extends Record>( calendar: CalendarSlot, fields: Base, additionalFields: ToAdd ) { if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.mergeFields%'), calendarObj, [ fields, additionalFields ]) as Base & ToAdd; } const mergeFields = GetMethod(calendar, 'mergeFields'); const result = Call(mergeFields, calendar, [fields, additionalFields]); if (!IsObject(result)) throw new TypeError('bad return from calendar.mergeFields()'); return result as Base & ToAdd; } export function CalendarDateAdd( calendar: CalendarSlot, date: CalendarProtocolParams['dateAdd'][0], duration: CalendarProtocolParams['dateAdd'][1], options: CalendarProtocolParams['dateAdd'][2], dateAddParam?: Temporal.CalendarProtocol['dateAdd'] | undefined ) { let dateAdd = dateAddParam; if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.dateAdd%'), calendarObj, [date, duration, options]); } if (dateAdd === undefined) { dateAdd = GetMethod(calendar, 'dateAdd'); } const result = ReflectApply(dateAdd, calendar, [date, duration, options]); if (!IsTemporalDate(result)) throw new TypeError('invalid result'); return result; } function CalendarDateUntil( calendar: CalendarSlot, date: CalendarProtocolParams['dateUntil'][0], otherDate: CalendarProtocolParams['dateUntil'][1], options: CalendarProtocolParams['dateUntil'][2], dateUntilParam?: Temporal.CalendarProtocol['dateUntil'] | undefined ) { let dateUntil = dateUntilParam; if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.dateUntil%'), calendarObj, [date, otherDate, options]); } if (dateUntil === undefined) { dateUntil = GetMethod(calendar, 'dateUntil'); } const result = ReflectApply(dateUntil, calendar, [date, otherDate, options]); if (!IsTemporalDuration(result)) throw new TypeError('invalid result'); return result; } export function CalendarYear(calendar: CalendarSlot, dateLike: CalendarProtocolParams['year'][0]) { if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.year%'), calendarObj, [dateLike]); } const year = GetMethod(calendar, 'year'); let result = Call(year, calendar, [dateLike]); if (typeof result !== 'number') { throw new TypeError('calendar year result must be an integer'); } if (!IsIntegralNumber(result)) { throw new RangeError('calendar year result must be an integer'); } return result; } export function CalendarMonth(calendar: CalendarSlot, dateLike: CalendarProtocolParams['month'][0]) { if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.month%'), calendarObj, [dateLike]); } const month = GetMethod(calendar, 'month'); let result = Call(month, calendar, [dateLike]); if (typeof result !== 'number') { throw new TypeError('calendar month result must be a positive integer'); } if (!IsIntegralNumber(result) || result < 1) { throw new RangeError('calendar month result must be a positive integer'); } return result; } export function CalendarMonthCode(calendar: CalendarSlot, dateLike: CalendarProtocolParams['monthCode'][0]) { if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.monthCode%'), calendarObj, [dateLike]); } const monthCode = GetMethod(calendar, 'monthCode'); let result = Call(monthCode, calendar, [dateLike]); if (typeof result !== 'string') { throw new TypeError('calendar monthCode result must be a string'); } return result; } export function CalendarDay(calendar: CalendarSlot, dateLike: CalendarProtocolParams['era'][0]) { if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.day%'), calendarObj, [dateLike]); } const day = GetMethod(calendar, 'day'); const result = Call(day, calendar, [dateLike]); if (typeof result !== 'number') { throw new TypeError('calendar day result must be a positive integer'); } if (!IsIntegralNumber(result) || result < 1) { throw new RangeError('calendar day result must be a positive integer'); } return result; } export function CalendarEra(calendar: CalendarSlot, dateLike: CalendarProtocolParams['era'][0]) { if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.era%'), calendarObj, [dateLike]); } const era = GetMethod(calendar, 'era'); let result = Call(era, calendar, [dateLike]); if (result === undefined) { return result; } if (typeof result !== 'string') { throw new TypeError('calendar era result must be a string or undefined'); } return result; } export function CalendarEraYear(calendar: CalendarSlot, dateLike: CalendarProtocolParams['era'][0]) { if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.eraYear%'), calendarObj, [dateLike]); } const eraYear = GetMethod(calendar, 'eraYear'); let result = Call(eraYear, calendar, [dateLike]); if (result === undefined) { return result; } if (typeof result !== 'number') { throw new TypeError('calendar eraYear result must be an integer or undefined'); } if (!IsIntegralNumber(result)) { throw new RangeError('calendar eraYear result must be an integer or undefined'); } return result; } export function CalendarDayOfWeek(calendar: CalendarSlot, dateLike: CalendarProtocolParams['era'][0]) { if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.dayOfWeek%'), calendarObj, [dateLike]); } const dayOfWeek = GetMethod(calendar, 'dayOfWeek'); const result = Call(dayOfWeek, calendar, [dateLike]); if (typeof result !== 'number') { throw new TypeError('calendar dayOfWeek result must be a positive integer'); } if (!IsIntegralNumber(result) || result < 1) { throw new RangeError('calendar dayOfWeek result must be a positive integer'); } return result; } export function CalendarDayOfYear(calendar: CalendarSlot, dateLike: CalendarProtocolParams['era'][0]) { if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.dayOfYear%'), calendarObj, [dateLike]); } const dayOfYear = GetMethod(calendar, 'dayOfYear'); const result = Call(dayOfYear, calendar, [dateLike]); if (typeof result !== 'number') { throw new TypeError('calendar dayOfYear result must be a positive integer'); } if (!IsIntegralNumber(result) || result < 1) { throw new RangeError('calendar dayOfYear result must be a positive integer'); } return result; } export function CalendarWeekOfYear(calendar: CalendarSlot, dateLike: CalendarProtocolParams['era'][0]) { if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.weekOfYear%'), calendarObj, [dateLike]); } const weekOfYear = GetMethod(calendar, 'weekOfYear'); const result = Call(weekOfYear, calendar, [dateLike]); if (typeof result !== 'number') { throw new TypeError('calendar weekOfYear result must be a positive integer'); } if (!IsIntegralNumber(result) || result < 1) { throw new RangeError('calendar weekOfYear result must be a positive integer'); } return result; } export function CalendarYearOfWeek(calendar: CalendarSlot, dateLike: CalendarProtocolParams['era'][0]) { if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.yearOfWeek%'), calendarObj, [dateLike]); } const yearOfWeek = GetMethod(calendar, 'yearOfWeek'); const result = Call(yearOfWeek, calendar, [dateLike]); if (typeof result !== 'number') { throw new TypeError('calendar yearOfWeek result must be an integer'); } if (!IsIntegralNumber(result)) { throw new RangeError('calendar yearOfWeek result must be an integer'); } return result; } export function CalendarDaysInWeek(calendar: CalendarSlot, dateLike: CalendarProtocolParams['era'][0]) { if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.daysInWeek%'), calendarObj, [dateLike]); } const daysInWeek = GetMethod(calendar, 'daysInWeek'); const result = Call(daysInWeek, calendar, [dateLike]); if (typeof result !== 'number') { throw new TypeError('calendar daysInWeek result must be a positive integer'); } if (!IsIntegralNumber(result) || result < 1) { throw new RangeError('calendar daysInWeek result must be a positive integer'); } return result; } export function CalendarDaysInMonth(calendar: CalendarSlot, dateLike: CalendarProtocolParams['era'][0]) { if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.daysInMonth%'), calendarObj, [dateLike]); } const daysInMonth = GetMethod(calendar, 'daysInMonth'); const result = Call(daysInMonth, calendar, [dateLike]); if (typeof result !== 'number') { throw new TypeError('calendar daysInMonth result must be a positive integer'); } if (!IsIntegralNumber(result) || result < 1) { throw new RangeError('calendar daysInMonth result must be a positive integer'); } return result; } export function CalendarDaysInYear(calendar: CalendarSlot, dateLike: CalendarProtocolParams['era'][0]) { if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.daysInYear%'), calendarObj, [dateLike]); } const daysInYear = GetMethod(calendar, 'daysInYear'); const result = Call(daysInYear, calendar, [dateLike]); if (typeof result !== 'number') { throw new TypeError('calendar daysInYear result must be a positive integer'); } if (!IsIntegralNumber(result) || result < 1) { throw new RangeError('calendar daysInYear result must be a positive integer'); } return result; } export function CalendarMonthsInYear(calendar: CalendarSlot, dateLike: CalendarProtocolParams['era'][0]) { if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.monthsInYear%'), calendarObj, [dateLike]); } const monthsInYear = GetMethod(calendar, 'monthsInYear'); const result = Call(monthsInYear, calendar, [dateLike]); if (typeof result !== 'number') { throw new TypeError('calendar monthsInYear result must be a positive integer'); } if (!IsIntegralNumber(result) || result < 1) { throw new RangeError('calendar monthsInYear result must be a positive integer'); } return result; } export function CalendarInLeapYear(calendar: CalendarSlot, dateLike: CalendarProtocolParams['era'][0]) { if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.inLeapYear%'), calendarObj, [dateLike]); } const inLeapYear = GetMethod(calendar, 'inLeapYear'); const result = Call(inLeapYear, calendar, [dateLike]); if (typeof result !== 'boolean') { throw new TypeError('calendar inLeapYear result must be a boolean'); } return result; } type MaybeCalendarProtocol = Partial>; function ObjectImplementsTemporalCalendarProtocol(object: MaybeCalendarProtocol) { if (IsTemporalCalendar(object)) return true; return ( 'dateAdd' in object && 'dateFromFields' in object && 'dateUntil' in object && 'day' in object && 'dayOfWeek' in object && 'dayOfYear' in object && 'daysInMonth' in object && 'daysInWeek' in object && 'daysInYear' in object && 'fields' in object && 'id' in object && 'inLeapYear' in object && 'mergeFields' in object && 'month' in object && 'monthCode' in object && 'monthDayFromFields' in object && 'monthsInYear' in object && 'weekOfYear' in object && 'year' in object && 'yearMonthFromFields' in object && 'yearOfWeek' in object ); } export function ToTemporalCalendarSlotValue(calendarLike: string): string; export function ToTemporalCalendarSlotValue(calendarLike: Temporal.CalendarProtocol): Temporal.CalendarProtocol; export function ToTemporalCalendarSlotValue(calendarLike: Temporal.CalendarLike): string | Temporal.CalendarProtocol; export function ToTemporalCalendarSlotValue(calendarLike: CalendarParams['from'][0]) { if (IsObject(calendarLike)) { if (HasSlot(calendarLike, CALENDAR)) return GetSlot(calendarLike, CALENDAR); if (!ObjectImplementsTemporalCalendarProtocol(calendarLike)) { throw new TypeError('expected a Temporal.Calendar or object implementing the Temporal.Calendar protocol'); } return calendarLike; } const identifier = ToString(calendarLike); if (IsBuiltinCalendar(identifier)) return ASCIILowercase(identifier); let calendar; try { ({ calendar } = ParseISODateTime(identifier)); } catch { try { ({ calendar } = ParseTemporalYearMonthString(identifier)); } catch { ({ calendar } = ParseTemporalMonthDayString(identifier)); } } if (!calendar) calendar = 'iso8601'; if (!IsBuiltinCalendar(calendar)) throw new RangeError(`invalid calendar identifier ${calendar}`); return ASCIILowercase(calendar); } function GetTemporalCalendarSlotValueWithISODefault(item: { calendar?: Temporal.CalendarLike }): CalendarSlot { if (HasSlot(item, CALENDAR)) return GetSlot(item, CALENDAR); const { calendar } = item; if (calendar === undefined) return 'iso8601'; return ToTemporalCalendarSlotValue(calendar); } export function ToTemporalCalendarIdentifier(slotValue: CalendarSlot) { if (typeof slotValue === 'string') return slotValue; const result = slotValue.id; if (typeof result !== 'string') throw new TypeError('calendar.id should be a string'); return result; } export function ToTemporalCalendarObject(slotValue: CalendarSlot) { if (IsObject(slotValue)) return slotValue; const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); return new TemporalCalendar(slotValue); } export function CalendarEquals(one: CalendarSlot, two: CalendarSlot) { if (one === two) return true; const cal1 = ToTemporalCalendarIdentifier(one); const cal2 = ToTemporalCalendarIdentifier(two); return cal1 === cal2; } // This operation is not in the spec, it implements the following: // "If ? CalendarEquals(one, two) is false, throw a RangeError exception." // This is so that we can build an informative error message without // re-getting the .id properties. function ThrowIfCalendarsNotEqual(one: CalendarSlot, two: CalendarSlot, errorMessageAction: string) { if (one === two) return; const cal1 = ToTemporalCalendarIdentifier(one); const cal2 = ToTemporalCalendarIdentifier(two); if (cal1 !== cal2) { throw new RangeError(`cannot ${errorMessageAction} of ${cal1} and ${cal2} calendars`); } } export function ConsolidateCalendars(one: CalendarSlot, two: CalendarSlot) { if (one === two) return two; const sOne = ToTemporalCalendarIdentifier(one); const sTwo = ToTemporalCalendarIdentifier(two); if (sOne === sTwo || sOne === 'iso8601') { return two; } else if (sTwo === 'iso8601') { return one; } else { throw new RangeError('irreconcilable calendars'); } } export function CalendarDateFromFields( calendar: CalendarSlot, fields: CalendarProtocolParams['dateFromFields'][0], options?: Partial, dateFromFieldsParam?: Temporal.CalendarProtocol['dateFromFields'] ) { if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.dateFromFields%'), calendarObj, [fields, options]); } const dateFromFields = dateFromFieldsParam ?? GetMethod(calendar, 'dateFromFields'); const result = Call(dateFromFields, calendar, [fields, options]); if (!IsTemporalDate(result)) throw new TypeError('invalid result'); return result; } export function CalendarYearMonthFromFields( calendar: CalendarSlot, fields: CalendarProtocolParams['yearMonthFromFields'][0], options?: CalendarProtocolParams['yearMonthFromFields'][1] ) { if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.yearMonthFromFields%'), calendarObj, [fields, options]); } const yearMonthFromFields = GetMethod(calendar, 'yearMonthFromFields'); let result = Call(yearMonthFromFields, calendar, [fields, options]); if (!IsTemporalYearMonth(result)) throw new TypeError('invalid result'); return result; } export function CalendarMonthDayFromFields( calendar: CalendarSlot, fields: CalendarProtocolParams['monthDayFromFields'][0], options?: CalendarProtocolParams['monthDayFromFields'][1] ) { if (typeof calendar === 'string') { const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%'); const calendarObj = new TemporalCalendar(calendar); return Call(GetIntrinsic('%Temporal.Calendar.prototype.monthDayFromFields%'), calendarObj, [fields, options]); } const monthDayFromFields = GetMethod(calendar, 'monthDayFromFields'); let result = Call(monthDayFromFields, calendar, [fields, options]); if (!IsTemporalMonthDay(result)) throw new TypeError('invalid result'); return result; } type MaybeTimeZoneProtocol = Partial< Pick >; function ObjectImplementsTemporalTimeZoneProtocol(object: MaybeTimeZoneProtocol) { if (IsTemporalTimeZone(object)) return true; return 'getOffsetNanosecondsFor' in object && 'getPossibleInstantsFor' in object && 'id' in object; } export function ToTemporalTimeZoneSlotValue(temporalTimeZoneLike: string): string; export function ToTemporalTimeZoneSlotValue(temporalTimeZoneLike: Temporal.TimeZoneProtocol): Temporal.TimeZoneProtocol; export function ToTemporalTimeZoneSlotValue( temporalTimeZoneLike: Temporal.TimeZoneLike ): string | Temporal.TimeZoneProtocol; export function ToTemporalTimeZoneSlotValue(temporalTimeZoneLike: TimeZoneParams['from'][0]) { if (IsObject(temporalTimeZoneLike)) { if (IsTemporalZonedDateTime(temporalTimeZoneLike)) return GetSlot(temporalTimeZoneLike, TIME_ZONE); if (!ObjectImplementsTemporalTimeZoneProtocol(temporalTimeZoneLike)) { throw new TypeError('expected a Temporal.TimeZone or object implementing the Temporal.TimeZone protocol'); } return temporalTimeZoneLike; } const identifier = ToString(temporalTimeZoneLike); return ParseTemporalTimeZone(identifier); } export function ToTemporalTimeZoneIdentifier(slotValue: TimeZoneSlot) { if (typeof slotValue === 'string') return slotValue; const result = slotValue.id; if (typeof result !== 'string') throw new TypeError('timeZone.id should be a string'); return result; } export function ToTemporalTimeZoneObject(slotValue: TimeZoneSlot) { if (IsObject(slotValue)) return slotValue; const TemporalTimeZone = GetIntrinsic('%Temporal.TimeZone%'); return new TemporalTimeZone(slotValue); } export function TimeZoneEquals(one: string | Temporal.TimeZoneProtocol, two: string | Temporal.TimeZoneProtocol) { if (one === two) return true; const tz1 = ToTemporalTimeZoneIdentifier(one); const tz2 = ToTemporalTimeZoneIdentifier(two); return tz1 === tz2; } export function TemporalDateTimeToDate(dateTime: Temporal.PlainDateTime) { return CreateTemporalDate( GetSlot(dateTime, ISO_YEAR), GetSlot(dateTime, ISO_MONTH), GetSlot(dateTime, ISO_DAY), GetSlot(dateTime, CALENDAR) ); } export function TemporalDateTimeToTime(dateTime: Temporal.PlainDateTime) { const Time = GetIntrinsic('%Temporal.PlainTime%'); return new Time( GetSlot(dateTime, ISO_HOUR), GetSlot(dateTime, ISO_MINUTE), GetSlot(dateTime, ISO_SECOND), GetSlot(dateTime, ISO_MILLISECOND), GetSlot(dateTime, ISO_MICROSECOND), GetSlot(dateTime, ISO_NANOSECOND) ); } export function GetOffsetNanosecondsFor( timeZone: string | Temporal.TimeZoneProtocol, instant: TimeZoneProtocolParams['getOffsetNanosecondsFor'][0], getOffsetNanosecondsForParam?: Temporal.TimeZoneProtocol['getOffsetNanosecondsFor'] ) { if (typeof timeZone === 'string') { const TemporalTimeZone = GetIntrinsic('%Temporal.TimeZone%'); const timeZoneObject = new TemporalTimeZone(timeZone); return Call(GetIntrinsic('%Temporal.TimeZone.prototype.getOffsetNanosecondsFor%'), timeZoneObject, [instant]); } const getOffsetNanosecondsFor = getOffsetNanosecondsForParam ?? GetMethod(timeZone, 'getOffsetNanosecondsFor'); const offsetNs = Call(getOffsetNanosecondsFor, timeZone, [instant]); if (typeof offsetNs !== 'number') { throw new TypeError('bad return from getOffsetNanosecondsFor'); } if (!IsIntegralNumber(offsetNs) || MathAbs(offsetNs) >= 86400e9) { throw new RangeError('out-of-range return from getOffsetNanosecondsFor'); } return offsetNs; } export function GetOffsetStringFor(timeZone: string | Temporal.TimeZoneProtocol, instant: Temporal.Instant) { const offsetNs = GetOffsetNanosecondsFor(timeZone, instant); return FormatTimeZoneOffsetString(offsetNs); } export function GetPlainDateTimeFor( timeZone: string | Temporal.TimeZoneProtocol, instant: Temporal.Instant, calendar: CalendarSlot ) { const ns = GetSlot(instant, EPOCHNANOSECONDS); const offsetNs = GetOffsetNanosecondsFor(timeZone, instant); let { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = GetISOPartsFromEpoch(ns); ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = BalanceISODateTime( year, month, day, hour, minute, second, millisecond, microsecond, nanosecond + offsetNs )); return CreateTemporalDateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar); } export function GetInstantFor( timeZone: string | Temporal.TimeZoneProtocol, dateTime: Temporal.PlainDateTime, disambiguation: NonNullable ) { const possibleInstants = GetPossibleInstantsFor(timeZone, dateTime); return DisambiguatePossibleInstants(possibleInstants, timeZone, dateTime, disambiguation); } function DisambiguatePossibleInstants( possibleInstants: Temporal.Instant[], timeZone: string | Temporal.TimeZoneProtocol, dateTime: Temporal.PlainDateTime, disambiguation: NonNullable ) { const Instant = GetIntrinsic('%Temporal.Instant%'); const numInstants = possibleInstants.length; if (numInstants === 1) return possibleInstants[0]; if (numInstants) { switch (disambiguation) { case 'compatible': // fall through because 'compatible' means 'earlier' for "fall back" transitions case 'earlier': return possibleInstants[0]; case 'later': return possibleInstants[numInstants - 1]; case 'reject': { throw new RangeError('multiple instants found'); } } } const year = GetSlot(dateTime, ISO_YEAR); const month = GetSlot(dateTime, ISO_MONTH); const day = GetSlot(dateTime, ISO_DAY); const hour = GetSlot(dateTime, ISO_HOUR); const minute = GetSlot(dateTime, ISO_MINUTE); const second = GetSlot(dateTime, ISO_SECOND); const millisecond = GetSlot(dateTime, ISO_MILLISECOND); const microsecond = GetSlot(dateTime, ISO_MICROSECOND); const nanosecond = GetSlot(dateTime, ISO_NANOSECOND); const utcns = GetUTCEpochNanoseconds(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond); if (utcns === null) throw new RangeError('DateTime outside of supported range'); const dayBefore = new Instant(JSBI.subtract(utcns, DAY_NANOS)); const dayAfter = new Instant(JSBI.add(utcns, DAY_NANOS)); const offsetBefore = GetOffsetNanosecondsFor(timeZone, dayBefore); const offsetAfter = GetOffsetNanosecondsFor(timeZone, dayAfter); const nanoseconds = offsetAfter - offsetBefore; switch (disambiguation) { case 'earlier': { const calendar = GetSlot(dateTime, CALENDAR); const PlainDateTime = GetIntrinsic('%Temporal.PlainDateTime%'); const earlier = AddDateTime( year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, 0, 0, 0, 0, 0, 0, 0, 0, 0, -nanoseconds, undefined ); const earlierPlainDateTime = new PlainDateTime( earlier.year, earlier.month, earlier.day, earlier.hour, earlier.minute, earlier.second, earlier.millisecond, earlier.microsecond, earlier.nanosecond, calendar ); return GetPossibleInstantsFor(timeZone, earlierPlainDateTime)[0]; } case 'compatible': // fall through because 'compatible' means 'later' for "spring forward" transitions case 'later': { const calendar = GetSlot(dateTime, CALENDAR); const PlainDateTime = GetIntrinsic('%Temporal.PlainDateTime%'); const later = AddDateTime( year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, 0, 0, 0, 0, 0, 0, 0, 0, 0, nanoseconds, undefined ); const laterPlainDateTime = new PlainDateTime( later.year, later.month, later.day, later.hour, later.minute, later.second, later.millisecond, later.microsecond, later.nanosecond, calendar ); const possible = GetPossibleInstantsFor(timeZone, laterPlainDateTime); return possible[possible.length - 1]; } case 'reject': { throw new RangeError('no such instant found'); } } } function GetPossibleInstantsFor( timeZone: string | Temporal.TimeZoneProtocol, dateTime: TimeZoneProtocolParams['getPossibleInstantsFor'][0], getPossibleInstantsForParam?: Temporal.TimeZoneProtocol['getPossibleInstantsFor'] ) { if (typeof timeZone === 'string') { const TemporalTimeZone = GetIntrinsic('%Temporal.TimeZone%'); const timeZoneObject = new TemporalTimeZone(timeZone); return Call(GetIntrinsic('%Temporal.TimeZone.prototype.getPossibleInstantsFor%'), timeZoneObject, [dateTime]); } const getPossibleInstantsFor = getPossibleInstantsForParam ?? GetMethod(timeZone, 'getPossibleInstantsFor'); const possibleInstants = Call(getPossibleInstantsFor, timeZone, [dateTime]); const result: Temporal.Instant[] = []; for (const instant of possibleInstants) { if (!IsTemporalInstant(instant)) { throw new TypeError('bad return from getPossibleInstantsFor'); } ArrayPrototypePush.call(result, instant); } return result; } export function ISOYearString(year: number) { let yearString; if (year < 0 || year > 9999) { const sign = year < 0 ? '-' : '+'; const yearNumber = MathAbs(year); yearString = sign + `000000${yearNumber}`.slice(-6); } else { yearString = `0000${year}`.slice(-4); } return yearString; } export function ISODateTimePartString(part: number) { return `00${part}`.slice(-2); } export function FormatSecondsStringPart( second: number, millisecond: number, microsecond: number, nanosecond: number, precision: ReturnType['precision'] ) { if (precision === 'minute') return ''; const secs = `:${ISODateTimePartString(second)}`; let fractionNumber = millisecond * 1e6 + microsecond * 1e3 + nanosecond; let fraction: string; if (precision === 'auto') { if (fractionNumber === 0) return secs; fraction = `${fractionNumber}`.padStart(9, '0'); while (fraction[fraction.length - 1] === '0') fraction = fraction.slice(0, -1); } else { if (precision === 0) return secs; fraction = `${fractionNumber}`.padStart(9, '0').slice(0, precision); } return `${secs}.${fraction}`; } export function TemporalInstantToString( instant: Temporal.Instant, timeZone: string | Temporal.TimeZoneProtocol | undefined, precision: ReturnType['precision'] ) { let outputTimeZone = timeZone; if (outputTimeZone === undefined) outputTimeZone = 'UTC'; const dateTime = GetPlainDateTimeFor(outputTimeZone, instant, 'iso8601'); const year = ISOYearString(GetSlot(dateTime, ISO_YEAR)); const month = ISODateTimePartString(GetSlot(dateTime, ISO_MONTH)); const day = ISODateTimePartString(GetSlot(dateTime, ISO_DAY)); const hour = ISODateTimePartString(GetSlot(dateTime, ISO_HOUR)); const minute = ISODateTimePartString(GetSlot(dateTime, ISO_MINUTE)); const seconds = FormatSecondsStringPart( GetSlot(dateTime, ISO_SECOND), GetSlot(dateTime, ISO_MILLISECOND), GetSlot(dateTime, ISO_MICROSECOND), GetSlot(dateTime, ISO_NANOSECOND), precision ); let timeZoneString = 'Z'; if (timeZone !== undefined) { const offsetNs = GetOffsetNanosecondsFor(outputTimeZone, instant); timeZoneString = FormatISOTimeZoneOffsetString(offsetNs); } return `${year}-${month}-${day}T${hour}:${minute}${seconds}${timeZoneString}`; } interface ToStringOptions { unit: ReturnType['unit']; increment: number; roundingMode: ReturnType; } export function TemporalDurationToString( duration: Temporal.Duration, precision: Temporal.ToStringPrecisionOptions['fractionalSecondDigits'] = 'auto', options: ToStringOptions | undefined = undefined ) { function formatNumber(num: number) { if (num <= NumberMaxSafeInteger) return num.toString(10); return JSBI.BigInt(num).toString(10); } const years = GetSlot(duration, YEARS); const months = GetSlot(duration, MONTHS); const weeks = GetSlot(duration, WEEKS); const days = GetSlot(duration, DAYS); const hours = GetSlot(duration, HOURS); const minutes = GetSlot(duration, MINUTES); let seconds = GetSlot(duration, SECONDS); let ms = GetSlot(duration, MILLISECONDS); let µs = GetSlot(duration, MICROSECONDS); let ns = GetSlot(duration, NANOSECONDS); const sign = DurationSign(years, months, weeks, days, hours, minutes, seconds, ms, µs, ns); if (options) { const { unit, increment, roundingMode } = options; ({ seconds, milliseconds: ms, microseconds: µs, nanoseconds: ns } = RoundDuration(0, 0, 0, 0, 0, 0, seconds, ms, µs, ns, increment, unit, roundingMode)); } const dateParts: string[] = []; if (years) dateParts.push(`${formatNumber(MathAbs(years))}Y`); if (months) dateParts.push(`${formatNumber(MathAbs(months))}M`); if (weeks) dateParts.push(`${formatNumber(MathAbs(weeks))}W`); if (days) dateParts.push(`${formatNumber(MathAbs(days))}D`); const timeParts: string[] = []; if (hours) timeParts.push(`${formatNumber(MathAbs(hours))}H`); if (minutes) timeParts.push(`${formatNumber(MathAbs(minutes))}M`); const secondParts: string[] = []; let total = TotalDurationNanoseconds(0, 0, 0, seconds, ms, µs, ns, 0); let nsBigInt: JSBI, µsBigInt: JSBI, msBigInt: JSBI, secondsBigInt: JSBI; ({ quotient: total, remainder: nsBigInt } = divmod(total, THOUSAND)); ({ quotient: total, remainder: µsBigInt } = divmod(total, THOUSAND)); ({ quotient: secondsBigInt, remainder: msBigInt } = divmod(total, THOUSAND)); const fraction = MathAbs(JSBI.toNumber(msBigInt)) * 1e6 + MathAbs(JSBI.toNumber(µsBigInt)) * 1e3 + MathAbs(JSBI.toNumber(nsBigInt)); let decimalPart; if (precision === 'auto') { if (fraction !== 0) { decimalPart = `${fraction}`.padStart(9, '0'); while (decimalPart[decimalPart.length - 1] === '0') { decimalPart = decimalPart.slice(0, -1); } } } else if (precision !== 0) { decimalPart = `${fraction}`.padStart(9, '0').slice(0, precision); } if (decimalPart) secondParts.unshift('.', decimalPart); if (!JSBI.equal(secondsBigInt, ZERO) || secondParts.length || precision !== 'auto') { secondParts.unshift(abs(secondsBigInt).toString()); } if (secondParts.length) timeParts.push(`${secondParts.join('')}S`); if (timeParts.length) timeParts.unshift('T'); if (!dateParts.length && !timeParts.length) return 'PT0S'; return `${sign < 0 ? '-' : ''}P${dateParts.join('')}${timeParts.join('')}`; } export function TemporalDateToString( date: Temporal.PlainDate, showCalendar: Temporal.ShowCalendarOption['calendarName'] = 'auto' ) { const year = ISOYearString(GetSlot(date, ISO_YEAR)); const month = ISODateTimePartString(GetSlot(date, ISO_MONTH)); const day = ISODateTimePartString(GetSlot(date, ISO_DAY)); const calendar = MaybeFormatCalendarAnnotation(GetSlot(date, CALENDAR), showCalendar); return `${year}-${month}-${day}${calendar}`; } export function TemporalDateTimeToString( dateTime: Temporal.PlainDateTime, precision: ReturnType['precision'], showCalendar: ReturnType = 'auto', options: ToStringOptions | undefined = undefined ) { let year = GetSlot(dateTime, ISO_YEAR); let month = GetSlot(dateTime, ISO_MONTH); let day = GetSlot(dateTime, ISO_DAY); let hour = GetSlot(dateTime, ISO_HOUR); let minute = GetSlot(dateTime, ISO_MINUTE); let second = GetSlot(dateTime, ISO_SECOND); let millisecond = GetSlot(dateTime, ISO_MILLISECOND); let microsecond = GetSlot(dateTime, ISO_MICROSECOND); let nanosecond = GetSlot(dateTime, ISO_NANOSECOND); if (options) { const { unit, increment, roundingMode } = options; ({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = RoundISODateTime( year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, increment, unit, roundingMode )); } const yearString = ISOYearString(year); const monthString = ISODateTimePartString(month); const dayString = ISODateTimePartString(day); const hourString = ISODateTimePartString(hour); const minuteString = ISODateTimePartString(minute); const secondsString = FormatSecondsStringPart(second, millisecond, microsecond, nanosecond, precision); const calendar = MaybeFormatCalendarAnnotation(GetSlot(dateTime, CALENDAR), showCalendar); return `${yearString}-${monthString}-${dayString}T${hourString}:${minuteString}${secondsString}${calendar}`; } export function TemporalMonthDayToString( monthDay: Temporal.PlainMonthDay, showCalendar: Temporal.ShowCalendarOption['calendarName'] = 'auto' ) { const month = ISODateTimePartString(GetSlot(monthDay, ISO_MONTH)); const day = ISODateTimePartString(GetSlot(monthDay, ISO_DAY)); let resultString = `${month}-${day}`; const calendar = GetSlot(monthDay, CALENDAR); const calendarID = ToTemporalCalendarIdentifier(calendar); if (showCalendar === 'always' || showCalendar === 'critical' || calendarID !== 'iso8601') { const year = ISOYearString(GetSlot(monthDay, ISO_YEAR)); resultString = `${year}-${resultString}`; } const calendarString = FormatCalendarAnnotation(calendarID, showCalendar); if (calendarString) resultString += calendarString; return resultString; } export function TemporalYearMonthToString( yearMonth: Temporal.PlainYearMonth, showCalendar: Temporal.ShowCalendarOption['calendarName'] = 'auto' ) { const year = ISOYearString(GetSlot(yearMonth, ISO_YEAR)); const month = ISODateTimePartString(GetSlot(yearMonth, ISO_MONTH)); let resultString = `${year}-${month}`; const calendar = GetSlot(yearMonth, CALENDAR); const calendarID = ToTemporalCalendarIdentifier(calendar); if (showCalendar === 'always' || showCalendar === 'critical' || calendarID !== 'iso8601') { const day = ISODateTimePartString(GetSlot(yearMonth, ISO_DAY)); resultString += `-${day}`; } const calendarString = FormatCalendarAnnotation(calendarID, showCalendar); if (calendarString) resultString += calendarString; return resultString; } export function TemporalZonedDateTimeToString( zdt: Temporal.ZonedDateTime, precision: ReturnType['precision'], showCalendar: ReturnType = 'auto', showTimeZone: ReturnType = 'auto', showOffset: ReturnType = 'auto', options: ToStringOptions | undefined = undefined ) { let instant = GetSlot(zdt, INSTANT); if (options) { const { unit, increment, roundingMode } = options; const ns = RoundInstant(GetSlot(zdt, EPOCHNANOSECONDS), increment, unit, roundingMode); const TemporalInstant = GetIntrinsic('%Temporal.Instant%'); instant = new TemporalInstant(ns); } const tz = GetSlot(zdt, TIME_ZONE); const dateTime = GetPlainDateTimeFor(tz, instant, 'iso8601'); const year = ISOYearString(GetSlot(dateTime, ISO_YEAR)); const month = ISODateTimePartString(GetSlot(dateTime, ISO_MONTH)); const day = ISODateTimePartString(GetSlot(dateTime, ISO_DAY)); const hour = ISODateTimePartString(GetSlot(dateTime, ISO_HOUR)); const minute = ISODateTimePartString(GetSlot(dateTime, ISO_MINUTE)); const seconds = FormatSecondsStringPart( GetSlot(dateTime, ISO_SECOND), GetSlot(dateTime, ISO_MILLISECOND), GetSlot(dateTime, ISO_MICROSECOND), GetSlot(dateTime, ISO_NANOSECOND), precision ); let result = `${year}-${month}-${day}T${hour}:${minute}${seconds}`; if (showOffset !== 'never') { const offsetNs = GetOffsetNanosecondsFor(tz, instant); result += FormatISOTimeZoneOffsetString(offsetNs); } if (showTimeZone !== 'never') { const identifier = ToTemporalTimeZoneIdentifier(tz); const flag = showTimeZone === 'critical' ? '!' : ''; result += `[${flag}${identifier}]`; } result += MaybeFormatCalendarAnnotation(GetSlot(zdt, CALENDAR), showCalendar); return result; } export function IsTimeZoneOffsetString(string: string) { return OFFSET.test(StringCtor(string)); } export function ParseTimeZoneOffsetString(string: string): number { const match = OFFSET.exec(StringCtor(string)); if (!match) { throw new RangeError(`invalid time zone offset: ${string}`); } const sign = match[1] === '-' || match[1] === '\u2212' ? -1 : +1; const hours = +match[2]; const minutes = +(match[3] || 0); const seconds = +(match[4] || 0); const nanoseconds = +((match[5] || 0) + '000000000').slice(0, 9); return sign * (((hours * 60 + minutes) * 60 + seconds) * 1e9 + nanoseconds); } export function GetCanonicalTimeZoneIdentifier(timeZoneIdentifier: string): string { if (IsTimeZoneOffsetString(timeZoneIdentifier)) { const offsetNs = ParseTimeZoneOffsetString(timeZoneIdentifier); return FormatTimeZoneOffsetString(offsetNs); } const formatter = getIntlDateTimeFormatEnUsForTimeZone(StringCtor(timeZoneIdentifier)); return formatter.resolvedOptions().timeZone; } export function GetNamedTimeZoneOffsetNanoseconds(id: string, epochNanoseconds: JSBI) { const { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = GetNamedTimeZoneDateTimeParts(id, epochNanoseconds); // The pattern of leap years in the ISO 8601 calendar repeats every 400 // years. To avoid overflowing at the edges of the range, we reduce the year // to the remainder after dividing by 400, and then add back all the // nanoseconds from the multiples of 400 years at the end. const reducedYear = year % 400; const yearCycles = (year - reducedYear) / 400; const nsIn400YearCycle = JSBI.multiply(JSBI.BigInt(400 * 365 + 97), DAY_NANOS); const reducedUTC = GetUTCEpochNanoseconds( reducedYear, month, day, hour, minute, second, millisecond, microsecond, nanosecond ); assertExists(reducedUTC); const utc = JSBI.add(reducedUTC, JSBI.multiply(nsIn400YearCycle, JSBI.BigInt(yearCycles))); return JSBI.toNumber(JSBI.subtract(utc, epochNanoseconds)); } function FormatTimeZoneOffsetString(offsetNanosecondsParam: number): string { const sign = offsetNanosecondsParam < 0 ? '-' : '+'; const offsetNanoseconds = MathAbs(offsetNanosecondsParam); const nanoseconds = offsetNanoseconds % 1e9; const seconds = MathFloor(offsetNanoseconds / 1e9) % 60; const minutes = MathFloor(offsetNanoseconds / 60e9) % 60; const hours = MathFloor(offsetNanoseconds / 3600e9); const hourString = ISODateTimePartString(hours); const minuteString = ISODateTimePartString(minutes); const secondString = ISODateTimePartString(seconds); let post = ''; if (nanoseconds) { let fraction = `${nanoseconds}`.padStart(9, '0'); while (fraction[fraction.length - 1] === '0') fraction = fraction.slice(0, -1); post = `:${secondString}.${fraction}`; } else if (seconds) { post = `:${secondString}`; } return `${sign}${hourString}:${minuteString}${post}`; } function FormatISOTimeZoneOffsetString(offsetNanosecondsParam: number): string { let offsetNanoseconds = JSBI.toNumber( RoundNumberToIncrement(JSBI.BigInt(offsetNanosecondsParam), MINUTE_NANOS, 'halfExpand') ); const sign = offsetNanoseconds < 0 ? '-' : '+'; offsetNanoseconds = MathAbs(offsetNanoseconds); const minutes = (offsetNanoseconds / 60e9) % 60; const hours = MathFloor(offsetNanoseconds / 3600e9); const hourString = ISODateTimePartString(hours); const minuteString = ISODateTimePartString(minutes); return `${sign}${hourString}:${minuteString}`; } export function GetUTCEpochNanoseconds( year: number, month: number, day: number, hour: number, minute: number, second: number, millisecond: number, microsecond: number, nanosecond: number ) { // Note: Date.UTC() interprets one and two-digit years as being in the // 20th century, so don't use it const legacyDate = new Date(); legacyDate.setUTCHours(hour, minute, second, millisecond); legacyDate.setUTCFullYear(year, month - 1, day); const ms = legacyDate.getTime(); if (NumberIsNaN(ms)) return null; let ns = JSBI.multiply(JSBI.BigInt(ms), MILLION); ns = JSBI.add(ns, JSBI.multiply(JSBI.BigInt(microsecond), THOUSAND)); ns = JSBI.add(ns, JSBI.BigInt(nanosecond)); if (JSBI.lessThan(ns, NS_MIN) || JSBI.greaterThan(ns, NS_MAX)) return null; return ns; } function GetISOPartsFromEpoch(epochNanoseconds: JSBI) { const { quotient, remainder } = divmod(epochNanoseconds, MILLION); let epochMilliseconds = JSBI.toNumber(quotient); let nanos = JSBI.toNumber(remainder); if (nanos < 0) { nanos += 1e6; epochMilliseconds -= 1; } const microsecond = MathFloor(nanos / 1e3) % 1e3; const nanosecond = nanos % 1e3; const item = new Date(epochMilliseconds); const year = item.getUTCFullYear(); const month = item.getUTCMonth() + 1; const day = item.getUTCDate(); const hour = item.getUTCHours(); const minute = item.getUTCMinutes(); const second = item.getUTCSeconds(); const millisecond = item.getUTCMilliseconds(); return { epochMilliseconds, year, month, day, hour, minute, second, millisecond, microsecond, nanosecond }; } // ts-prune-ignore-next TODO: remove this after tests are converted to TS export function GetNamedTimeZoneDateTimeParts(id: string, epochNanoseconds: JSBI) { const { epochMilliseconds, millisecond, microsecond, nanosecond } = GetISOPartsFromEpoch(epochNanoseconds); const { year, month, day, hour, minute, second } = GetFormatterParts(id, epochMilliseconds); return BalanceISODateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond); } function maxJSBI(one: JSBI, two: JSBI) { return JSBI.lessThan(one, two) ? two : one; } /** * Our best guess at how far in advance new rules will be put into the TZDB for * future offset transitions. We'll pick 10 years but can always revise it if * we find that countries are being unusually proactive in their announcing * of offset changes. */ function afterLatestPossibleTzdbRuleChange() { return JSBI.add(SystemUTCEpochNanoSeconds(), ABOUT_TEN_YEARS_NANOS); } export function GetNamedTimeZoneNextTransition(id: string, epochNanoseconds: JSBI): JSBI | null { if (JSBI.lessThan(epochNanoseconds, BEFORE_FIRST_OFFSET_TRANSITION)) { return GetNamedTimeZoneNextTransition(id, BEFORE_FIRST_OFFSET_TRANSITION); } // Decide how far in the future after `epochNanoseconds` we'll look for an // offset change. There are two cases: // 1. If it's a past date (or a date in the near future) then it's possible // that the time zone may have newly added DST in the next few years. So // we'll have to look from the provided time until a few years after the // current system time. (Changes to DST policy are usually announced a few // years in the future.) Note that the first DST anywhere started in 1847, // so we'll start checks in 1847 instead of wasting cycles on years where // there will never be transitions. // 2. If it's a future date beyond the next few years, then we'll just assume // that the latest DST policy in TZDB will still be in effect. In this // case, we only need to look one year in the future to see if there are // any DST transitions. We actually only need to look 9-10 months because // DST has two transitions per year, but we'll use a year just to be safe. const oneYearLater = JSBI.add(epochNanoseconds, ABOUT_ONE_YEAR_NANOS); const uppercap = maxJSBI(afterLatestPossibleTzdbRuleChange(), oneYearLater); // The first transition (in any timezone) recorded in the TZDB was in 1847, so // start there if an earlier date is supplied. let leftNanos = maxJSBI(BEFORE_FIRST_OFFSET_TRANSITION, epochNanoseconds); const leftOffsetNs = GetNamedTimeZoneOffsetNanoseconds(id, leftNanos); let rightNanos = leftNanos; let rightOffsetNs = leftOffsetNs; while (leftOffsetNs === rightOffsetNs && JSBI.lessThan(JSBI.BigInt(leftNanos), uppercap)) { rightNanos = JSBI.add(leftNanos, TWO_WEEKS_NANOS); if (JSBI.greaterThan(rightNanos, NS_MAX)) return null; rightOffsetNs = GetNamedTimeZoneOffsetNanoseconds(id, rightNanos); if (leftOffsetNs === rightOffsetNs) { leftNanos = rightNanos; } } if (leftOffsetNs === rightOffsetNs) return null; const result = bisect( (epochNs: JSBI) => GetNamedTimeZoneOffsetNanoseconds(id, epochNs), leftNanos, rightNanos, leftOffsetNs, rightOffsetNs ); return result; } export function GetNamedTimeZonePreviousTransition(id: string, epochNanoseconds: JSBI): JSBI | null { // If a time zone uses DST (at the time of `epochNanoseconds`), then we only // have to look back one year to find a transition. But if it doesn't use DST, // then we need to look all the way back to 1847 (the earliest rule in the // TZDB) to see if it had other offset transitions in the past. Looping back // from a far-future date to 1847 is very slow (minutes of 100% CPU!), and is // also unnecessary because DST rules aren't put into the TZDB more than a few // years in the future because the political changes in time zones happen with // only a few years' warning. Therefore, if a far-future date is provided, // then we'll run the check in two parts: // 1. First, we'll look back for up to one year to see if the latest TZDB // rules have DST. // 2. If not, then we'll "fast-reverse" back to a few years later than the // current system time, and then look back to 1847. This reduces the // worst-case loop from 273K years to 175 years, for a ~1500x improvement // in worst-case perf. const afterLatestRule = afterLatestPossibleTzdbRuleChange(); const isFarFuture = JSBI.greaterThan(epochNanoseconds, afterLatestRule); const lowercap = isFarFuture ? JSBI.subtract(epochNanoseconds, ABOUT_ONE_YEAR_NANOS) : BEFORE_FIRST_OFFSET_TRANSITION; // TODO: proposal-temporal polyfill has different code for very similar // optimizations as above, as well as in GetNamedTimeZonePreviousTransition. // We should figure out if we should change one polyfill to match the other. // We assume most time zones either have regular DST rules that extend // indefinitely into the future, or they have no DST transitions between now // and next year. Africa/Casablanca and Africa/El_Aaiun are unique cases // that fit neither of these. Their irregular DST transitions are // precomputed until 2087 in the current time zone database, so requesting // the previous transition for an instant far in the future may take an // extremely long time as it loops backward 2 weeks at a time. if (id === 'Africa/Casablanca' || id === 'Africa/El_Aaiun') { const lastPrecomputed = GetSlot(ToTemporalInstant('2088-01-01T00Z'), EPOCHNANOSECONDS); if (JSBI.lessThan(lastPrecomputed, epochNanoseconds)) { return GetNamedTimeZonePreviousTransition(id, lastPrecomputed); } } let rightNanos = JSBI.subtract(epochNanoseconds, ONE); if (JSBI.lessThan(rightNanos, BEFORE_FIRST_OFFSET_TRANSITION)) return null; const rightOffsetNs = GetNamedTimeZoneOffsetNanoseconds(id, rightNanos); let leftNanos = rightNanos; let leftOffsetNs = rightOffsetNs; while (rightOffsetNs === leftOffsetNs && JSBI.greaterThan(rightNanos, lowercap)) { leftNanos = JSBI.subtract(rightNanos, TWO_WEEKS_NANOS); if (JSBI.lessThan(leftNanos, BEFORE_FIRST_OFFSET_TRANSITION)) return null; leftOffsetNs = GetNamedTimeZoneOffsetNanoseconds(id, leftNanos); if (rightOffsetNs === leftOffsetNs) { rightNanos = leftNanos; } } if (rightOffsetNs === leftOffsetNs) { if (isFarFuture) { // There was no DST after looking back one year, which means that the most // recent TZDB rules don't have any recurring transitions. To check for // transitions in older rules, back up to a few years after the current // date and then look all the way back to 1847. Note that we move back one // day from the latest possible rule so that when the recursion runs it // won't consider the new time to be "far future" because the system clock // has advanced in the meantime. const newTimeToCheck = JSBI.subtract(afterLatestRule, DAY_NANOS); return GetNamedTimeZonePreviousTransition(id, newTimeToCheck); } return null; } const result = bisect( (epochNs: JSBI) => GetNamedTimeZoneOffsetNanoseconds(id, epochNs), leftNanos, rightNanos, leftOffsetNs, rightOffsetNs ); return result; } // ts-prune-ignore-next TODO: remove this after tests are converted to TS export function parseFromEnUsFormat(datetime: string) { const parts = datetime.split(/[^\w]+/); if (parts.length !== 7) { throw new RangeError(`expected 7 parts in "${datetime}`); } const month = +parts[0]; const day = +parts[1]; let year = +parts[2]; const era = parts[3].toUpperCase(); if (era === 'B' || era === 'BC') { year = -year + 1; } else if (era !== 'A' && era !== 'AD') { throw new RangeError(`Unknown era ${era} in "${datetime}`); } let hour = +parts[4]; if (hour === 24) { // bugs.chromium.org/p/chromium/issues/detail?id=1045791 hour = 0; } const minute = +parts[5]; const second = +parts[6]; if ( !NumberIsFinite(year) || !NumberIsFinite(month) || !NumberIsFinite(day) || !NumberIsFinite(hour) || !NumberIsFinite(minute) || !NumberIsFinite(second) ) { throw new RangeError(`Invalid number in "${datetime}`); } return { year, month, day, hour, minute, second }; } // ts-prune-ignore-next TODO: remove this after tests are converted to TS export function GetFormatterParts(timeZone: string, epochMilliseconds: number) { const formatter = getIntlDateTimeFormatEnUsForTimeZone(timeZone); // Using `format` instead of `formatToParts` for compatibility with older clients const datetime = formatter.format(new Date(epochMilliseconds)); return parseFromEnUsFormat(datetime); } export function GetNamedTimeZoneEpochNanoseconds( id: string, year: number, month: number, day: number, hour: number, minute: number, second: number, millisecond: number, microsecond: number, nanosecond: number ) { const ns = GetUTCEpochNanoseconds(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond); if (ns === null) throw new RangeError('DateTime outside of supported range'); let nsEarlier = JSBI.subtract(ns, DAY_NANOS); if (JSBI.lessThan(nsEarlier, NS_MIN)) nsEarlier = ns; let nsLater = JSBI.add(ns, DAY_NANOS); if (JSBI.greaterThan(nsLater, NS_MAX)) nsLater = ns; const earliest = GetNamedTimeZoneOffsetNanoseconds(id, nsEarlier); const latest = GetNamedTimeZoneOffsetNanoseconds(id, nsLater); const found = earliest === latest ? [earliest] : [earliest, latest]; return found .map((offsetNanoseconds) => { const epochNanoseconds = JSBI.subtract(ns, JSBI.BigInt(offsetNanoseconds)); const parts = GetNamedTimeZoneDateTimeParts(id, epochNanoseconds); if ( year !== parts.year || month !== parts.month || day !== parts.day || hour !== parts.hour || minute !== parts.minute || second !== parts.second || millisecond !== parts.millisecond || microsecond !== parts.microsecond || nanosecond !== parts.nanosecond ) { return undefined; } return epochNanoseconds; }) .filter((x) => x !== undefined) as JSBI[]; } export function LeapYear(year: number) { if (undefined === year) return false; const isDiv4 = year % 4 === 0; const isDiv100 = year % 100 === 0; const isDiv400 = year % 400 === 0; return isDiv4 && (!isDiv100 || isDiv400); } export function ISODaysInMonth(year: number, month: number) { const DoM = { standard: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], leapyear: [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] }; return DoM[LeapYear(year) ? 'leapyear' : 'standard'][month - 1]; } export function DayOfWeek(year: number, month: number, day: number) { const m = month + (month < 3 ? 10 : -2); const Y = year - (month < 3 ? 1 : 0); const c = MathFloor(Y / 100); const y = Y - c * 100; const d = day; const pD = d; const pM = MathFloor(2.6 * m - 0.2); const pY = y + MathFloor(y / 4); const pC = MathFloor(c / 4) - 2 * c; const dow = (pD + pM + pY + pC) % 7; return dow + (dow <= 0 ? 7 : 0); } export function DayOfYear(year: number, month: number, day: number) { let days = day; for (let m = month - 1; m > 0; m--) { days += ISODaysInMonth(year, m); } return days; } export function WeekOfYear(year: number, month: number, day: number) { const doy = DayOfYear(year, month, day); const dow = DayOfWeek(year, month, day) || 7; const doj = DayOfWeek(year, 1, 1); const week = MathFloor((doy - dow + 10) / 7); if (week < 1) { if (doj === 5 || (doj === 6 && LeapYear(year - 1))) { return { week: 53, year: year - 1 }; } else { return { week: 52, year: year - 1 }; } } if (week === 53) { if ((LeapYear(year) ? 366 : 365) - doy < 4 - dow) { return { week: 1, year: year + 1 }; } } return { week, year }; } export function DurationSign( y: number, mon: number, w: number, d: number, h: number, min: number, s: number, ms: number, µs: number, ns: number ) { for (const prop of [y, mon, w, d, h, min, s, ms, µs, ns]) { if (prop !== 0) return prop < 0 ? -1 : 1; } return 0; } function BalanceISOYearMonth(yearParam: number, monthParam: number) { let year = yearParam; let month = monthParam; if (!NumberIsFinite(year) || !NumberIsFinite(month)) throw new RangeError('infinity is out of range'); month -= 1; year += MathFloor(month / 12); month %= 12; if (month < 0) month += 12; month += 1; return { year, month }; } function BalanceISODate(yearParam: number, monthParam: number, dayParam: number) { let year = yearParam; let month = monthParam; let day = dayParam; if (!NumberIsFinite(day)) throw new RangeError('infinity is out of range'); ({ year, month } = BalanceISOYearMonth(year, month)); // The pattern of leap years in the ISO 8601 calendar repeats every 400 // years. So if we have more than 400 years in days, there's no need to // convert days to a year 400 times. We can convert a multiple of 400 all at // once. const daysIn400YearCycle = 400 * 365 + 97; if (MathAbs(day) > daysIn400YearCycle) { const nCycles = MathTrunc(day / daysIn400YearCycle); year += 400 * nCycles; day -= nCycles * daysIn400YearCycle; } let daysInYear = 0; let testYear = month > 2 ? year : year - 1; while (((daysInYear = LeapYear(testYear) ? 366 : 365), day < -daysInYear)) { year -= 1; testYear -= 1; day += daysInYear; } testYear += 1; while (((daysInYear = LeapYear(testYear) ? 366 : 365), day > daysInYear)) { year += 1; testYear += 1; day -= daysInYear; } while (day < 1) { ({ year, month } = BalanceISOYearMonth(year, month - 1)); day += ISODaysInMonth(year, month); } while (day > ISODaysInMonth(year, month)) { day -= ISODaysInMonth(year, month); ({ year, month } = BalanceISOYearMonth(year, month + 1)); } return { year, month, day }; } function BalanceISODateTime( yearParam: number, monthParam: number, dayParam: number, hourParam: number, minuteParam: number, secondParam: number, millisecondParam: number, microsecondParam: number, nanosecondParam: number ) { const { deltaDays, hour, minute, second, millisecond, microsecond, nanosecond } = BalanceTime( hourParam, minuteParam, secondParam, millisecondParam, microsecondParam, nanosecondParam ); const { year, month, day } = BalanceISODate(yearParam, monthParam, dayParam + deltaDays); return { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond }; } function BalanceTime( hourParam: number, minuteParam: number, secondParam: number, millisecondParam: number, microsecondParam: number, nanosecondParam: number ) { let hour = JSBI.BigInt(hourParam); let minute = JSBI.BigInt(minuteParam); let second = JSBI.BigInt(secondParam); let millisecond = JSBI.BigInt(millisecondParam); let microsecond = JSBI.BigInt(microsecondParam); let nanosecond = JSBI.BigInt(nanosecondParam); let quotient; ({ quotient, remainder: nanosecond } = NonNegativeBigIntDivmod(nanosecond, THOUSAND)); microsecond = JSBI.add(microsecond, quotient); ({ quotient, remainder: microsecond } = NonNegativeBigIntDivmod(microsecond, THOUSAND)); millisecond = JSBI.add(millisecond, quotient); ({ quotient, remainder: millisecond } = NonNegativeBigIntDivmod(millisecond, THOUSAND)); second = JSBI.add(second, quotient); ({ quotient, remainder: second } = NonNegativeBigIntDivmod(second, SIXTY)); minute = JSBI.add(minute, quotient); ({ quotient, remainder: minute } = NonNegativeBigIntDivmod(minute, SIXTY)); hour = JSBI.add(hour, quotient); ({ quotient, remainder: hour } = NonNegativeBigIntDivmod(hour, TWENTY_FOUR)); return { deltaDays: JSBI.toNumber(quotient), hour: JSBI.toNumber(hour), minute: JSBI.toNumber(minute), second: JSBI.toNumber(second), millisecond: JSBI.toNumber(millisecond), microsecond: JSBI.toNumber(microsecond), nanosecond: JSBI.toNumber(nanosecond) }; } export function TotalDurationNanoseconds( daysParam: number, hoursParam: number | JSBI, minutesParam: number | JSBI, secondsParam: number | JSBI, millisecondsParam: number | JSBI, microsecondsParam: number | JSBI, nanosecondsParam: number | JSBI, offsetShift: number ) { const days: JSBI = JSBI.BigInt(daysParam); let nanoseconds: JSBI = JSBI.BigInt(nanosecondsParam); if (daysParam !== 0) nanoseconds = JSBI.subtract(JSBI.BigInt(nanosecondsParam), JSBI.BigInt(offsetShift)); const hours = JSBI.add(JSBI.BigInt(hoursParam), JSBI.multiply(days, JSBI.BigInt(24))); const minutes = JSBI.add(JSBI.BigInt(minutesParam), JSBI.multiply(hours, SIXTY)); const seconds = JSBI.add(JSBI.BigInt(secondsParam), JSBI.multiply(minutes, SIXTY)); const milliseconds = JSBI.add(JSBI.BigInt(millisecondsParam), JSBI.multiply(seconds, THOUSAND)); const microseconds = JSBI.add(JSBI.BigInt(microsecondsParam), JSBI.multiply(milliseconds, THOUSAND)); return JSBI.add(JSBI.BigInt(nanoseconds), JSBI.multiply(microseconds, THOUSAND)); } function NanosecondsToDays(nanosecondsParam: JSBI, relativeTo: ReturnType) { const TemporalInstant = GetIntrinsic('%Temporal.Instant%'); const sign = MathSign(JSBI.toNumber(nanosecondsParam)); let nanoseconds = JSBI.BigInt(nanosecondsParam); let dayLengthNs = 86400e9; if (sign === 0) return { days: 0, nanoseconds: ZERO, dayLengthNs }; if (!IsTemporalZonedDateTime(relativeTo)) { let days: JSBI; ({ quotient: days, remainder: nanoseconds } = divmod(nanoseconds, JSBI.BigInt(dayLengthNs))); return { days: JSBI.toNumber(days), nanoseconds, dayLengthNs }; } const startNs = GetSlot(relativeTo, EPOCHNANOSECONDS); const start = GetSlot(relativeTo, INSTANT); const endNs = JSBI.add(startNs, nanoseconds); const end = new TemporalInstant(endNs); const timeZone = GetSlot(relativeTo, TIME_ZONE); const calendar = GetSlot(relativeTo, CALENDAR); // Find the difference in days only. const dtStart = GetPlainDateTimeFor(timeZone, start, calendar); const dtEnd = GetPlainDateTimeFor(timeZone, end, calendar); let { days: daysNumber } = DifferenceISODateTime( GetSlot(dtStart, ISO_YEAR), GetSlot(dtStart, ISO_MONTH), GetSlot(dtStart, ISO_DAY), GetSlot(dtStart, ISO_HOUR), GetSlot(dtStart, ISO_MINUTE), GetSlot(dtStart, ISO_SECOND), GetSlot(dtStart, ISO_MILLISECOND), GetSlot(dtStart, ISO_MICROSECOND), GetSlot(dtStart, ISO_NANOSECOND), GetSlot(dtEnd, ISO_YEAR), GetSlot(dtEnd, ISO_MONTH), GetSlot(dtEnd, ISO_DAY), GetSlot(dtEnd, ISO_HOUR), GetSlot(dtEnd, ISO_MINUTE), GetSlot(dtEnd, ISO_SECOND), GetSlot(dtEnd, ISO_MILLISECOND), GetSlot(dtEnd, ISO_MICROSECOND), GetSlot(dtEnd, ISO_NANOSECOND), calendar, 'day', ObjectCreate(null) as Temporal.DifferenceOptions ); let intermediateNs = AddZonedDateTime(start, timeZone, calendar, 0, 0, 0, daysNumber, 0, 0, 0, 0, 0, 0); // may disambiguate // If clock time after addition was in the middle of a skipped period, the // endpoint was disambiguated to a later clock time. So it's possible that // the resulting disambiguated result is later than endNs. If so, then back // up one day and try again. Repeat if necessary (some transitions are // > 24 hours) until either there's zero days left or the date duration is // back inside the period where it belongs. Note that this case only can // happen for positive durations because the only direction that // `disambiguation: 'compatible'` can change clock time is forwards. let daysBigInt = JSBI.BigInt(daysNumber); if (sign === 1) { while (JSBI.greaterThan(daysBigInt, ZERO) && JSBI.greaterThan(intermediateNs, endNs)) { daysBigInt = JSBI.subtract(daysBigInt, ONE); intermediateNs = AddZonedDateTime( start, timeZone, calendar, 0, 0, 0, JSBI.toNumber(daysBigInt), 0, 0, 0, 0, 0, 0 ); // may do disambiguation } } nanoseconds = JSBI.subtract(endNs, intermediateNs); let isOverflow = false; let relativeInstant = new TemporalInstant(intermediateNs); do { // calculate length of the next day (day that contains the time remainder) const oneDayFartherNs = AddZonedDateTime(relativeInstant, timeZone, calendar, 0, 0, 0, sign, 0, 0, 0, 0, 0, 0); const relativeNs = GetSlot(relativeInstant, EPOCHNANOSECONDS); dayLengthNs = JSBI.toNumber(JSBI.subtract(oneDayFartherNs, relativeNs)); isOverflow = JSBI.greaterThanOrEqual( JSBI.multiply(JSBI.subtract(nanoseconds, JSBI.BigInt(dayLengthNs)), JSBI.BigInt(sign)), ZERO ); if (isOverflow) { nanoseconds = JSBI.subtract(nanoseconds, JSBI.BigInt(dayLengthNs)); relativeInstant = new TemporalInstant(oneDayFartherNs); daysBigInt = JSBI.add(daysBigInt, JSBI.BigInt(sign)); } } while (isOverflow); if (!isZero(daysBigInt) && signJSBI(daysBigInt) !== sign) { throw new RangeError('Time zone or calendar converted nanoseconds into a number of days with the opposite sign'); } if (!isZero(nanoseconds) && signJSBI(nanoseconds) !== sign) { if (isNegativeJSBI(nanoseconds) && sign === 1) { throw new Error('assert not reached'); } throw new RangeError('Time zone or calendar ended up with a remainder of nanoseconds with the opposite sign'); } if (JSBI.greaterThanOrEqual(abs(nanoseconds), abs(JSBI.BigInt(dayLengthNs)))) { throw new Error('assert not reached'); } return { days: JSBI.toNumber(daysBigInt), nanoseconds, dayLengthNs: MathAbs(dayLengthNs) }; } export function BalanceDuration( daysParam: number, hoursParam: number | JSBI, minutesParam: number | JSBI, secondsParam: number | JSBI, millisecondsParam: number | JSBI, microsecondsParam: number | JSBI, nanosecondsParam: number | JSBI, largestUnit: Temporal.DateTimeUnit, relativeTo: ReturnType = undefined ) { let result = BalancePossiblyInfiniteDuration( daysParam, hoursParam, minutesParam, secondsParam, millisecondsParam, microsecondsParam, nanosecondsParam, largestUnit, relativeTo ); if (result === 'positive overflow' || result === 'negative overflow') { throw new RangeError('Duration out of range'); } else { return result; } } export function BalancePossiblyInfiniteDuration( daysParam: number, hoursParam: number | JSBI, minutesParam: number | JSBI, secondsParam: number | JSBI, millisecondsParam: number | JSBI, microsecondsParam: number | JSBI, nanosecondsParam: number | JSBI, largestUnit: Temporal.DateTimeUnit, relativeTo: ReturnType = undefined ) { let days = daysParam; let nanosecondsBigInt: JSBI, microsecondsBigInt: JSBI, millisecondsBigInt: JSBI, secondsBigInt: JSBI, minutesBigInt: JSBI, hoursBigInt: JSBI; if (IsTemporalZonedDateTime(relativeTo)) { const endNs = AddZonedDateTime( GetSlot(relativeTo, INSTANT), GetSlot(relativeTo, TIME_ZONE), GetSlot(relativeTo, CALENDAR), 0, 0, 0, days, hoursParam, minutesParam, secondsParam, millisecondsParam, microsecondsParam, nanosecondsParam ); const startNs = GetSlot(relativeTo, EPOCHNANOSECONDS); nanosecondsBigInt = JSBI.subtract(endNs, startNs); } else { nanosecondsBigInt = TotalDurationNanoseconds( days, hoursParam, minutesParam, secondsParam, millisecondsParam, microsecondsParam, nanosecondsParam, 0 ); } if (largestUnit === 'year' || largestUnit === 'month' || largestUnit === 'week' || largestUnit === 'day') { ({ days, nanoseconds: nanosecondsBigInt } = NanosecondsToDays(nanosecondsBigInt, relativeTo)); } else { days = 0; } const sign = JSBI.lessThan(nanosecondsBigInt, ZERO) ? -1 : 1; nanosecondsBigInt = abs(nanosecondsBigInt); microsecondsBigInt = millisecondsBigInt = secondsBigInt = minutesBigInt = hoursBigInt = ZERO; switch (largestUnit) { case 'year': case 'month': case 'week': case 'day': case 'hour': ({ quotient: microsecondsBigInt, remainder: nanosecondsBigInt } = divmod(nanosecondsBigInt, THOUSAND)); ({ quotient: millisecondsBigInt, remainder: microsecondsBigInt } = divmod(microsecondsBigInt, THOUSAND)); ({ quotient: secondsBigInt, remainder: millisecondsBigInt } = divmod(millisecondsBigInt, THOUSAND)); ({ quotient: minutesBigInt, remainder: secondsBigInt } = divmod(secondsBigInt, SIXTY)); ({ quotient: hoursBigInt, remainder: minutesBigInt } = divmod(minutesBigInt, SIXTY)); break; case 'minute': ({ quotient: microsecondsBigInt, remainder: nanosecondsBigInt } = divmod(nanosecondsBigInt, THOUSAND)); ({ quotient: millisecondsBigInt, remainder: microsecondsBigInt } = divmod(microsecondsBigInt, THOUSAND)); ({ quotient: secondsBigInt, remainder: millisecondsBigInt } = divmod(millisecondsBigInt, THOUSAND)); ({ quotient: minutesBigInt, remainder: secondsBigInt } = divmod(secondsBigInt, SIXTY)); break; case 'second': ({ quotient: microsecondsBigInt, remainder: nanosecondsBigInt } = divmod(nanosecondsBigInt, THOUSAND)); ({ quotient: millisecondsBigInt, remainder: microsecondsBigInt } = divmod(microsecondsBigInt, THOUSAND)); ({ quotient: secondsBigInt, remainder: millisecondsBigInt } = divmod(millisecondsBigInt, THOUSAND)); break; case 'millisecond': ({ quotient: microsecondsBigInt, remainder: nanosecondsBigInt } = divmod(nanosecondsBigInt, THOUSAND)); ({ quotient: millisecondsBigInt, remainder: microsecondsBigInt } = divmod(microsecondsBigInt, THOUSAND)); break; case 'microsecond': ({ quotient: microsecondsBigInt, remainder: nanosecondsBigInt } = divmod(nanosecondsBigInt, THOUSAND)); break; case 'nanosecond': break; default: throw new Error('assert not reached'); } const hours = JSBI.toNumber(hoursBigInt) * sign; const minutes = JSBI.toNumber(minutesBigInt) * sign; const seconds = JSBI.toNumber(secondsBigInt) * sign; const milliseconds = JSBI.toNumber(millisecondsBigInt) * sign; const microseconds = JSBI.toNumber(microsecondsBigInt) * sign; const nanoseconds = JSBI.toNumber(nanosecondsBigInt) * sign; for (const prop of [days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds]) { if (!NumberIsFinite(prop)) { if (sign === 1) { return 'positive overflow' as const; } else { return 'negative overflow' as const; } } } return { days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; } export function UnbalanceDurationRelative( yearsParam: number, monthsParam: number, weeksParam: number, daysParam: number, largestUnit: Temporal.DateTimeUnit, relativeToParam: ReturnType ): { years: number; months: number; weeks: number; days: number; } { const TemporalDuration = GetIntrinsic('%Temporal.Duration%'); const sign = DurationSign(yearsParam, monthsParam, weeksParam, daysParam, 0, 0, 0, 0, 0, 0); if (sign === 0) return { years: yearsParam, months: monthsParam, weeks: weeksParam, days: daysParam }; const signBI = JSBI.BigInt(sign); let years = JSBI.BigInt(yearsParam); let months = JSBI.BigInt(monthsParam); let weeks = JSBI.BigInt(weeksParam); let days = JSBI.BigInt(daysParam); let calendar; let relativeTo: Temporal.PlainDate | undefined; if (relativeToParam) { relativeTo = ToTemporalDate(relativeToParam); calendar = GetSlot(relativeTo, CALENDAR); } const oneYear = new TemporalDuration(sign); const oneMonth = new TemporalDuration(0, sign); const oneWeek = new TemporalDuration(0, 0, sign); switch (largestUnit) { case 'year': // no-op break; case 'month': { if (!calendar) throw new RangeError('a starting point is required for months balancing'); assertExists(relativeTo); // balance years down to months let dateAdd, dateUntil; if (typeof calendar !== 'string') { dateAdd = GetMethod(calendar, 'dateAdd'); dateUntil = GetMethod(calendar, 'dateUntil'); } while (!isZero(years)) { const newRelativeTo = CalendarDateAdd(calendar, relativeTo, oneYear, undefined, dateAdd); const untilOptions = ObjectCreate(null) as Temporal.DifferenceOptions; untilOptions.largestUnit = 'month'; const untilResult = CalendarDateUntil(calendar, relativeTo, newRelativeTo, untilOptions, dateUntil); const oneYearMonths = JSBI.BigInt(GetSlot(untilResult, MONTHS)); relativeTo = newRelativeTo; months = JSBI.add(months, oneYearMonths); years = JSBI.subtract(years, signBI); } } break; case 'week': { if (!calendar) throw new RangeError('a starting point is required for weeks balancing'); assertExists(relativeTo); const dateAdd = typeof calendar !== 'string' ? GetMethod(calendar, 'dateAdd') : undefined; // balance years down to days while (!isZero(years)) { let oneYearDays; ({ relativeTo, days: oneYearDays } = MoveRelativeDate(calendar, relativeTo, oneYear, dateAdd)); days = JSBI.add(days, JSBI.BigInt(oneYearDays)); years = JSBI.subtract(years, signBI); } // balance months down to days while (!isZero(months)) { let oneMonthDays; ({ relativeTo, days: oneMonthDays } = MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd)); days = JSBI.add(days, JSBI.BigInt(oneMonthDays)); months = JSBI.subtract(months, signBI); } break; } default: { // balance years down to days if (isZero(years) && isZero(months) && isZero(weeks)) break; if (!calendar) throw new RangeError('a starting point is required for balancing calendar units'); const dateAdd = typeof calendar !== 'string' ? GetMethod(calendar, 'dateAdd') : undefined; while (!isZero(years)) { assertExists(relativeTo); let oneYearDays; ({ relativeTo, days: oneYearDays } = MoveRelativeDate(calendar, relativeTo, oneYear, dateAdd)); days = JSBI.add(days, JSBI.BigInt(oneYearDays)); years = JSBI.subtract(years, signBI); } // balance months down to days while (!isZero(months)) { assertExists(relativeTo); let oneMonthDays; ({ relativeTo, days: oneMonthDays } = MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd)); days = JSBI.add(days, JSBI.BigInt(oneMonthDays)); months = JSBI.subtract(months, signBI); } // balance weeks down to days while (!isZero(weeks)) { assertExists(relativeTo); let oneWeekDays; ({ relativeTo, days: oneWeekDays } = MoveRelativeDate(calendar, relativeTo, oneWeek, dateAdd)); days = JSBI.add(days, JSBI.BigInt(oneWeekDays)); weeks = JSBI.subtract(weeks, signBI); } break; } } return { years: JSBI.toNumber(years), months: JSBI.toNumber(months), weeks: JSBI.toNumber(weeks), days: JSBI.toNumber(days) }; } export function BalanceDurationRelative( yearsParam: number, monthsParam: number, weeksParam: number, daysParam: number, largestUnit: Temporal.DateTimeUnit, relativeToParam: ReturnType ): { years: number; months: number; weeks: number; days: number; } { const TemporalDuration = GetIntrinsic('%Temporal.Duration%'); const sign = DurationSign(yearsParam, monthsParam, weeksParam, daysParam, 0, 0, 0, 0, 0, 0); if (sign === 0) return { years: yearsParam, months: monthsParam, weeks: weeksParam, days: daysParam }; const signBI = JSBI.BigInt(sign); let years = JSBI.BigInt(yearsParam); let months = JSBI.BigInt(monthsParam); let weeks = JSBI.BigInt(weeksParam); let days = JSBI.BigInt(daysParam); let calendar; let relativeTo: Temporal.PlainDate | undefined; if (relativeToParam) { relativeTo = ToTemporalDate(relativeToParam); calendar = GetSlot(relativeTo, CALENDAR); } const oneYear = new TemporalDuration(sign); const oneMonth = new TemporalDuration(0, sign); const oneWeek = new TemporalDuration(0, 0, sign); switch (largestUnit) { case 'year': { if (!calendar) throw new RangeError('a starting point is required for years balancing'); assertExists(relativeTo); const dateAdd = typeof calendar !== 'string' ? GetMethod(calendar, 'dateAdd') : undefined; // balance days up to years let newRelativeTo, oneYearDays; ({ relativeTo: newRelativeTo, days: oneYearDays } = MoveRelativeDate(calendar, relativeTo, oneYear, dateAdd)); while (JSBI.greaterThanOrEqual(abs(days), JSBI.BigInt(MathAbs(oneYearDays)))) { days = JSBI.subtract(days, JSBI.BigInt(oneYearDays)); years = JSBI.add(years, signBI); relativeTo = newRelativeTo; ({ relativeTo: newRelativeTo, days: oneYearDays } = MoveRelativeDate(calendar, relativeTo, oneYear, dateAdd)); } // balance days up to months let oneMonthDays; ({ relativeTo: newRelativeTo, days: oneMonthDays } = MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd)); while (JSBI.greaterThanOrEqual(abs(days), JSBI.BigInt(MathAbs(oneMonthDays)))) { days = JSBI.subtract(days, JSBI.BigInt(oneMonthDays)); months = JSBI.add(months, signBI); relativeTo = newRelativeTo; ({ relativeTo: newRelativeTo, days: oneMonthDays } = MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd)); } // balance months up to years newRelativeTo = CalendarDateAdd(calendar, relativeTo, oneYear, undefined, dateAdd); const dateUntil = typeof calendar !== 'string' ? GetMethod(calendar, 'dateUntil') : undefined; const untilOptions = ObjectCreate(null) as Temporal.DifferenceOptions<'month'>; untilOptions.largestUnit = 'month'; let untilResult = CalendarDateUntil(calendar, relativeTo, newRelativeTo, untilOptions, dateUntil); let oneYearMonths = GetSlot(untilResult, MONTHS); while (JSBI.greaterThanOrEqual(abs(months), JSBI.BigInt(MathAbs(oneYearMonths)))) { months = JSBI.subtract(months, JSBI.BigInt(oneYearMonths)); years = JSBI.add(years, signBI); relativeTo = newRelativeTo; newRelativeTo = CalendarDateAdd(calendar, relativeTo, oneYear, undefined, dateAdd); const untilOptions = ObjectCreate(null) as Temporal.DifferenceOptions<'month'>; untilOptions.largestUnit = 'month'; untilResult = CalendarDateUntil(calendar, relativeTo, newRelativeTo, untilOptions, dateUntil); oneYearMonths = GetSlot(untilResult, MONTHS); } break; } case 'month': { if (!calendar) throw new RangeError('a starting point is required for months balancing'); assertExists(relativeTo); const dateAdd = typeof calendar !== 'string' ? GetMethod(calendar, 'dateAdd') : undefined; // balance days up to months let newRelativeTo, oneMonthDays; ({ relativeTo: newRelativeTo, days: oneMonthDays } = MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd)); while (JSBI.greaterThanOrEqual(abs(days), JSBI.BigInt(MathAbs(oneMonthDays)))) { days = JSBI.subtract(days, JSBI.BigInt(oneMonthDays)); months = JSBI.add(months, signBI); relativeTo = newRelativeTo; ({ relativeTo: newRelativeTo, days: oneMonthDays } = MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd)); } break; } case 'week': { if (!calendar) throw new RangeError('a starting point is required for weeks balancing'); assertExists(relativeTo); const dateAdd = typeof calendar !== 'string' ? GetMethod(calendar, 'dateAdd') : undefined; // balance days up to weeks let newRelativeTo, oneWeekDays; ({ relativeTo: newRelativeTo, days: oneWeekDays } = MoveRelativeDate(calendar, relativeTo, oneWeek, dateAdd)); while (JSBI.greaterThanOrEqual(abs(days), JSBI.BigInt(MathAbs(oneWeekDays)))) { days = JSBI.subtract(days, JSBI.BigInt(oneWeekDays)); weeks = JSBI.add(weeks, signBI); relativeTo = newRelativeTo; ({ relativeTo: newRelativeTo, days: oneWeekDays } = MoveRelativeDate(calendar, relativeTo, oneWeek, dateAdd)); } break; } default: // no-op break; } return { years: JSBI.toNumber(years), months: JSBI.toNumber(months), weeks: JSBI.toNumber(weeks), days: JSBI.toNumber(days) }; } export function CalculateOffsetShift( relativeTo: ReturnType, y: number, mon: number, w: number, d: number ) { if (IsTemporalZonedDateTime(relativeTo)) { const instant = GetSlot(relativeTo, INSTANT); const timeZone = GetSlot(relativeTo, TIME_ZONE); const calendar = GetSlot(relativeTo, CALENDAR); const offsetBefore = GetOffsetNanosecondsFor(timeZone, instant); const after = AddZonedDateTime(instant, timeZone, calendar, y, mon, w, d, 0, 0, 0, 0, 0, 0); const TemporalInstant = GetIntrinsic('%Temporal.Instant%'); const instantAfter = new TemporalInstant(after); const offsetAfter = GetOffsetNanosecondsFor(timeZone, instantAfter); return offsetAfter - offsetBefore; } return 0; } export function CreateNegatedTemporalDuration(duration: Temporal.Duration) { const TemporalDuration = GetIntrinsic('%Temporal.Duration%'); return new TemporalDuration( -GetSlot(duration, YEARS), -GetSlot(duration, MONTHS), -GetSlot(duration, WEEKS), -GetSlot(duration, DAYS), -GetSlot(duration, HOURS), -GetSlot(duration, MINUTES), -GetSlot(duration, SECONDS), -GetSlot(duration, MILLISECONDS), -GetSlot(duration, MICROSECONDS), -GetSlot(duration, NANOSECONDS) ); } export function ConstrainToRange(value: number | undefined, min: number, max: number) { // Math.Max accepts undefined values and returns NaN. Undefined values are // used for optional params in the method below. return MathMin(max, MathMax(min, value as number)); } function ConstrainISODate(year: number, monthParam: number, dayParam?: number) { const month = ConstrainToRange(monthParam, 1, 12); const day = ConstrainToRange(dayParam, 1, ISODaysInMonth(year, month)); return { year, month, day }; } function ConstrainTime( hourParam: number, minuteParam: number, secondParam: number, millisecondParam: number, microsecondParam: number, nanosecondParam: number ) { const hour = ConstrainToRange(hourParam, 0, 23); const minute = ConstrainToRange(minuteParam, 0, 59); const second = ConstrainToRange(secondParam, 0, 59); const millisecond = ConstrainToRange(millisecondParam, 0, 999); const microsecond = ConstrainToRange(microsecondParam, 0, 999); const nanosecond = ConstrainToRange(nanosecondParam, 0, 999); return { hour, minute, second, millisecond, microsecond, nanosecond }; } export function RejectToRange(value: number, min: number, max: number) { if (value < min || value > max) throw new RangeError(`value out of range: ${min} <= ${value} <= ${max}`); } function RejectISODate(year: number, month: number, day: number) { RejectToRange(month, 1, 12); RejectToRange(day, 1, ISODaysInMonth(year, month)); } function RejectDateRange(year: number, month: number, day: number) { // Noon avoids trouble at edges of DateTime range (excludes midnight) RejectDateTimeRange(year, month, day, 12, 0, 0, 0, 0, 0); } export function RejectTime( hour: number, minute: number, second: number, millisecond: number, microsecond: number, nanosecond: number ) { RejectToRange(hour, 0, 23); RejectToRange(minute, 0, 59); RejectToRange(second, 0, 59); RejectToRange(millisecond, 0, 999); RejectToRange(microsecond, 0, 999); RejectToRange(nanosecond, 0, 999); } function RejectDateTime( year: number, month: number, day: number, hour: number, minute: number, second: number, millisecond: number, microsecond: number, nanosecond: number ) { RejectISODate(year, month, day); RejectTime(hour, minute, second, millisecond, microsecond, nanosecond); } function RejectDateTimeRange( year: number, month: number, day: number, hour: number, minute: number, second: number, millisecond: number, microsecond: number, nanosecond: number ) { RejectToRange(year, YEAR_MIN, YEAR_MAX); // Reject any DateTime 24 hours or more outside the Instant range if ( (year === YEAR_MIN && null == GetUTCEpochNanoseconds(year, month, day + 1, hour, minute, second, millisecond, microsecond, nanosecond - 1)) || (year === YEAR_MAX && null == GetUTCEpochNanoseconds(year, month, day - 1, hour, minute, second, millisecond, microsecond, nanosecond + 1)) ) { throw new RangeError('DateTime outside of supported range'); } } export function ValidateEpochNanoseconds(epochNanoseconds: JSBI) { if (JSBI.lessThan(epochNanoseconds, NS_MIN) || JSBI.greaterThan(epochNanoseconds, NS_MAX)) { throw new RangeError('Instant outside of supported range'); } } function RejectYearMonthRange(year: number, month: number) { RejectToRange(year, YEAR_MIN, YEAR_MAX); if (year === YEAR_MIN) { RejectToRange(month, 4, 12); } else if (year === YEAR_MAX) { RejectToRange(month, 1, 9); } } export function RejectDuration( y: number, mon: number, w: number, d: number, h: number, min: number, s: number, ms: number, µs: number, ns: number ) { const sign = DurationSign(y, mon, w, d, h, min, s, ms, µs, ns); for (const prop of [y, mon, w, d, h, min, s, ms, µs, ns]) { if (!NumberIsFinite(prop)) throw new RangeError('infinite values not allowed as duration fields'); const propSign = MathSign(prop); if (propSign !== 0 && propSign !== sign) throw new RangeError('mixed-sign values not allowed as duration fields'); } } export function DifferenceISODate( y1: number, m1: number, d1: number, y2: number, m2: number, d2: number, largestUnit: Allowed ) { switch (largestUnit) { case 'year': case 'month': { const sign = -CompareISODate(y1, m1, d1, y2, m2, d2); if (sign === 0) return { years: 0, months: 0, weeks: 0, days: 0 }; const start = { year: y1, month: m1, day: d1 }; const end = { year: y2, month: m2, day: d2 }; let years = end.year - start.year; let mid = AddISODate(y1, m1, d1, years, 0, 0, 0, 'constrain'); let midSign = -CompareISODate(mid.year, mid.month, mid.day, y2, m2, d2); if (midSign === 0) { return largestUnit === 'year' ? { years, months: 0, weeks: 0, days: 0 } : { years: 0, months: years * 12, weeks: 0, days: 0 }; } let months = end.month - start.month; if (midSign !== sign) { years -= sign; months += sign * 12; } mid = AddISODate(y1, m1, d1, years, months, 0, 0, 'constrain'); midSign = -CompareISODate(mid.year, mid.month, mid.day, y2, m2, d2); if (midSign === 0) { return largestUnit === 'year' ? { years, months, weeks: 0, days: 0 } : { years: 0, months: months + years * 12, weeks: 0, days: 0 }; } if (midSign !== sign) { // The end date is later in the month than mid date (or earlier for // negative durations). Back up one month. months -= sign; if (months === -sign) { years -= sign; months = 11 * sign; } mid = AddISODate(y1, m1, d1, years, months, 0, 0, 'constrain'); } let days = 0; // If we get here, months and years are correct (no overflow), and `mid` // is within the range from `start` to `end`. To count the days between // `mid` and `end`, there are 3 cases: // 1) same month: use simple subtraction // 2) end is previous month from intermediate (negative duration) // 3) end is next month from intermediate (positive duration) if (mid.month === end.month) { // 1) same month: use simple subtraction days = end.day - mid.day; } else if (sign < 0) { // 2) end is previous month from intermediate (negative duration) // Example: intermediate: Feb 1, end: Jan 30, DaysInMonth = 31, days = -2 days = -mid.day - (ISODaysInMonth(end.year, end.month) - end.day); } else { // 3) end is next month from intermediate (positive duration) // Example: intermediate: Jan 29, end: Feb 1, DaysInMonth = 31, days = 3 days = end.day + (ISODaysInMonth(mid.year, mid.month) - mid.day); } if (largestUnit === 'month') { months += years * 12; years = 0; } return { years, months, weeks: 0, days }; } case 'week': case 'day': { let larger, smaller, sign; if (CompareISODate(y1, m1, d1, y2, m2, d2) < 0) { smaller = { year: y1, month: m1, day: d1 }; larger = { year: y2, month: m2, day: d2 }; sign = 1; } else { smaller = { year: y2, month: m2, day: d2 }; larger = { year: y1, month: m1, day: d1 }; sign = -1; } let days = DayOfYear(larger.year, larger.month, larger.day) - DayOfYear(smaller.year, smaller.month, smaller.day); for (let year = smaller.year; year < larger.year; ++year) { days += LeapYear(year) ? 366 : 365; } let weeks = 0; if (largestUnit === 'week') { weeks = MathFloor(days / 7); days %= 7; } weeks *= sign; days *= sign; return { years: 0, months: 0, weeks, days }; } default: throw new Error('assert not reached'); } } function DifferenceTime( h1: number, min1: number, s1: number, ms1: number, µs1: number, ns1: number, h2: number, min2: number, s2: number, ms2: number, µs2: number, ns2: number ) { let hours = h2 - h1; let minutes = min2 - min1; let seconds = s2 - s1; let milliseconds = ms2 - ms1; let microseconds = µs2 - µs1; let nanoseconds = ns2 - ns1; const sign = DurationSign(0, 0, 0, 0, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); hours *= sign; minutes *= sign; seconds *= sign; milliseconds *= sign; microseconds *= sign; nanoseconds *= sign; let deltaDays = 0; ({ deltaDays, hour: hours, minute: minutes, second: seconds, millisecond: milliseconds, microsecond: microseconds, nanosecond: nanoseconds } = BalanceTime(hours, minutes, seconds, milliseconds, microseconds, nanoseconds)); if (deltaDays != 0) throw new Error('assertion failure in DifferenceTime: _bt_.[[Days]] should be 0'); hours *= sign; minutes *= sign; seconds *= sign; milliseconds *= sign; microseconds *= sign; nanoseconds *= sign; return { hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; } function DifferenceInstant( ns1: JSBI, ns2: JSBI, increment: number, smallestUnit: keyof typeof nsPerTimeUnit, largestUnit: keyof typeof nsPerTimeUnit, roundingMode: Temporal.RoundingMode ) { const diff = JSBI.subtract(ns2, ns1); let hours = 0; let minutes = 0; let nanoseconds = JSBI.toNumber(JSBI.remainder(diff, THOUSAND)); let microseconds = JSBI.toNumber(JSBI.remainder(JSBI.divide(diff, THOUSAND), THOUSAND)); let milliseconds = JSBI.toNumber(JSBI.remainder(JSBI.divide(diff, MILLION), THOUSAND)); let seconds = JSBI.toNumber(JSBI.divide(diff, BILLION)); ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = RoundDuration( 0, 0, 0, 0, 0, 0, seconds, milliseconds, microseconds, nanoseconds, increment, smallestUnit, roundingMode )); return BalanceDuration(0, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, largestUnit); } function DifferenceISODateTime( y1Param: number, mon1Param: number, d1Param: number, h1: number, min1: number, s1: number, ms1: number, µs1: number, ns1: number, y2: number, mon2: number, d2: number, h2: number, min2: number, s2: number, ms2: number, µs2: number, ns2: number, calendar: CalendarSlot, largestUnit: Temporal.DateTimeUnit, options: Temporal.DifferenceOptions | undefined ) { let y1 = y1Param; let mon1 = mon1Param; let d1 = d1Param; let { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = DifferenceTime( h1, min1, s1, ms1, µs1, ns1, h2, min2, s2, ms2, µs2, ns2 ); const timeSign = DurationSign(0, 0, 0, 0, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); const dateSign = CompareISODate(y2, mon2, d2, y1, mon1, d1); if (dateSign === -timeSign) { ({ year: y1, month: mon1, day: d1 } = BalanceISODate(y1, mon1, d1 - timeSign)); ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceDuration( -timeSign, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, largestUnit )); } const date1 = CreateTemporalDate(y1, mon1, d1, calendar); const date2 = CreateTemporalDate(y2, mon2, d2, calendar); const dateLargestUnit = LargerOfTwoTemporalUnits('day', largestUnit); const untilOptions = CopyOptions(options); untilOptions.largestUnit = dateLargestUnit; // TODO untilOptions doesn't want to compile as it seems that smallestUnit is not clamped? // Type 'SmallestUnit | undefined' is not assignable to type // 'SmallestUnit<"year" | "month" | "day" | "week"> | undefined'. // Type '"hour"' is not assignable to type // 'SmallestUnit<"year" | "month" | "day" | "week"> | undefined'.ts(2345) let { years, months, weeks, days } = CalendarDateUntil(calendar, date1, date2, untilOptions as any); // Signs of date part and time part may not agree; balance them together ({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceDuration( days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, largestUnit )); return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; } function DifferenceZonedDateTime( ns1: JSBI, ns2: JSBI, timeZone: string | Temporal.TimeZoneProtocol, calendar: CalendarSlot, largestUnit: Temporal.DateTimeUnit, options: Temporal.DifferenceOptions ) { const nsDiff = JSBI.subtract(ns2, ns1); if (JSBI.equal(nsDiff, ZERO)) { return { years: 0, months: 0, weeks: 0, days: 0, hours: 0, minutes: 0, seconds: 0, milliseconds: 0, microseconds: 0, nanoseconds: 0 }; } // Find the difference in dates only. const TemporalInstant = GetIntrinsic('%Temporal.Instant%'); const start = new TemporalInstant(ns1); const end = new TemporalInstant(ns2); const dtStart = GetPlainDateTimeFor(timeZone, start, calendar); const dtEnd = GetPlainDateTimeFor(timeZone, end, calendar); let { years, months, weeks, days } = DifferenceISODateTime( GetSlot(dtStart, ISO_YEAR), GetSlot(dtStart, ISO_MONTH), GetSlot(dtStart, ISO_DAY), GetSlot(dtStart, ISO_HOUR), GetSlot(dtStart, ISO_MINUTE), GetSlot(dtStart, ISO_SECOND), GetSlot(dtStart, ISO_MILLISECOND), GetSlot(dtStart, ISO_MICROSECOND), GetSlot(dtStart, ISO_NANOSECOND), GetSlot(dtEnd, ISO_YEAR), GetSlot(dtEnd, ISO_MONTH), GetSlot(dtEnd, ISO_DAY), GetSlot(dtEnd, ISO_HOUR), GetSlot(dtEnd, ISO_MINUTE), GetSlot(dtEnd, ISO_SECOND), GetSlot(dtEnd, ISO_MILLISECOND), GetSlot(dtEnd, ISO_MICROSECOND), GetSlot(dtEnd, ISO_NANOSECOND), calendar, largestUnit, options ); const intermediateNs = AddZonedDateTime(start, timeZone, calendar, years, months, weeks, 0, 0, 0, 0, 0, 0, 0); // may disambiguate let timeRemainderNs = JSBI.subtract(ns2, intermediateNs); const intermediate = CreateTemporalZonedDateTime(intermediateNs, timeZone, calendar); ({ nanoseconds: timeRemainderNs, days } = NanosecondsToDays(timeRemainderNs, intermediate)); // Finally, merge the date and time durations and return the merged result. const { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceDuration( 0, 0, 0, 0, 0, 0, JSBI.toNumber(timeRemainderNs), 'hour' ); return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; } type DifferenceOperation = 'since' | 'until'; function GetDifferenceSettings( op: DifferenceOperation, options: Temporal.DifferenceOptions, group: 'datetime' | 'date' | 'time', disallowed: (Temporal.DateTimeUnit | 'auto')[], fallbackSmallest: T, smallestLargestDefaultUnit: T ) { const ALLOWED_UNITS = SINGULAR_PLURAL_UNITS.reduce((allowed, unitInfo) => { const p = unitInfo[0]; const s = unitInfo[1]; const c = unitInfo[2]; if ((group === 'datetime' || c === group) && !disallowed.includes(s)) { allowed.push(s, p); } return allowed; }, [] as (Temporal.DateTimeUnit | Temporal.PluralUnit)[]); let largestUnit = GetTemporalUnit(options, 'largestUnit', group, 'auto'); if (disallowed.includes(largestUnit)) { throw new RangeError(`largestUnit must be one of ${ALLOWED_UNITS.join(', ')}, not ${largestUnit}`); } const roundingIncrement = ToTemporalRoundingIncrement(options); let roundingMode = ToTemporalRoundingMode(options, 'trunc'); if (op === 'since') roundingMode = NegateTemporalRoundingMode(roundingMode); const smallestUnit = GetTemporalUnit(options, 'smallestUnit', group, fallbackSmallest); if (disallowed.includes(smallestUnit)) { throw new RangeError(`smallestUnit must be one of ${ALLOWED_UNITS.join(', ')}, not ${smallestUnit}`); } const defaultLargestUnit = LargerOfTwoTemporalUnits(smallestLargestDefaultUnit, smallestUnit); if (largestUnit === 'auto') largestUnit = defaultLargestUnit; if (LargerOfTwoTemporalUnits(largestUnit, smallestUnit) !== largestUnit) { throw new RangeError(`largestUnit ${largestUnit} cannot be smaller than smallestUnit ${smallestUnit}`); } const MAX_DIFFERENCE_INCREMENTS: { [k in Temporal.DateTimeUnit]?: number } = { hour: 24, minute: 60, second: 60, millisecond: 1000, microsecond: 1000, nanosecond: 1000 }; const maximum = MAX_DIFFERENCE_INCREMENTS[smallestUnit]; if (maximum !== undefined) ValidateTemporalRoundingIncrement(roundingIncrement, maximum, false); return { largestUnit: largestUnit as T, roundingIncrement, roundingMode, smallestUnit: smallestUnit as T }; } export function DifferenceTemporalInstant( operation: DifferenceOperation, instant: Temporal.Instant, otherParam: InstantParams['until'][0], options: InstantParams['until'][1] | undefined ): Temporal.Duration { const sign = operation === 'since' ? -1 : 1; const other = ToTemporalInstant(otherParam); const resolvedOptions = CopyOptions(options); const settings = GetDifferenceSettings(operation, resolvedOptions, 'time', [], 'nanosecond', 'second'); const onens = GetSlot(instant, EPOCHNANOSECONDS); const twons = GetSlot(other, EPOCHNANOSECONDS); let { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = DifferenceInstant( onens, twons, settings.roundingIncrement, settings.smallestUnit, settings.largestUnit, settings.roundingMode ); const Duration = GetIntrinsic('%Temporal.Duration%'); return new Duration( 0, 0, 0, 0, sign * hours, sign * minutes, sign * seconds, sign * milliseconds, sign * microseconds, sign * nanoseconds ); } export function DifferenceTemporalPlainDate( operation: DifferenceOperation, plainDate: Temporal.PlainDate, otherParam: PlainDateParams['until'][0], options: PlainDateParams['until'][1] ): Temporal.Duration { const sign = operation === 'since' ? -1 : 1; const other = ToTemporalDate(otherParam); const calendar = GetSlot(plainDate, CALENDAR); const otherCalendar = GetSlot(other, CALENDAR); ThrowIfCalendarsNotEqual(calendar, otherCalendar, 'compute difference between dates'); const resolvedOptions = CopyOptions(options); const settings = GetDifferenceSettings(operation, resolvedOptions, 'date', [], 'day', 'day'); resolvedOptions.largestUnit = settings.largestUnit; let { years, months, weeks, days } = CalendarDateUntil(calendar, plainDate, other, resolvedOptions); if (settings.smallestUnit !== 'day' || settings.roundingIncrement !== 1) { ({ years, months, weeks, days } = RoundDuration( years, months, weeks, days, 0, 0, 0, 0, 0, 0, settings.roundingIncrement, settings.smallestUnit, settings.roundingMode, plainDate )); } const Duration = GetIntrinsic('%Temporal.Duration%'); return new Duration(sign * years, sign * months, sign * weeks, sign * days, 0, 0, 0, 0, 0, 0); } export function DifferenceTemporalPlainDateTime( operation: DifferenceOperation, plainDateTime: Temporal.PlainDateTime, otherParam: PlainDateTimeParams['until'][0], options: PlainDateTimeParams['until'][1] ): Temporal.Duration { const sign = operation === 'since' ? -1 : 1; const other = ToTemporalDateTime(otherParam); const calendar = GetSlot(plainDateTime, CALENDAR); const otherCalendar = GetSlot(other, CALENDAR); ThrowIfCalendarsNotEqual(calendar, otherCalendar, 'compute difference between dates'); const resolvedOptions = CopyOptions(options); const settings = GetDifferenceSettings(operation, resolvedOptions, 'datetime', [], 'nanosecond', 'day'); let { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = DifferenceISODateTime( GetSlot(plainDateTime, ISO_YEAR), GetSlot(plainDateTime, ISO_MONTH), GetSlot(plainDateTime, ISO_DAY), GetSlot(plainDateTime, ISO_HOUR), GetSlot(plainDateTime, ISO_MINUTE), GetSlot(plainDateTime, ISO_SECOND), GetSlot(plainDateTime, ISO_MILLISECOND), GetSlot(plainDateTime, ISO_MICROSECOND), GetSlot(plainDateTime, ISO_NANOSECOND), GetSlot(other, ISO_YEAR), GetSlot(other, ISO_MONTH), GetSlot(other, ISO_DAY), GetSlot(other, ISO_HOUR), GetSlot(other, ISO_MINUTE), GetSlot(other, ISO_SECOND), GetSlot(other, ISO_MILLISECOND), GetSlot(other, ISO_MICROSECOND), GetSlot(other, ISO_NANOSECOND), calendar, settings.largestUnit, resolvedOptions ); const relativeTo = TemporalDateTimeToDate(plainDateTime); ({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = RoundDuration( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, settings.roundingIncrement, settings.smallestUnit, settings.roundingMode, relativeTo )); ({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceDuration( days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, settings.largestUnit )); const Duration = GetIntrinsic('%Temporal.Duration%'); return new Duration( sign * years, sign * months, sign * weeks, sign * days, sign * hours, sign * minutes, sign * seconds, sign * milliseconds, sign * microseconds, sign * nanoseconds ); } export function DifferenceTemporalPlainTime( operation: DifferenceOperation, plainTime: Temporal.PlainTime, otherParam: PlainTimeParams['until'][0], options: PlainTimeParams['until'][1] ): Temporal.Duration { const sign = operation === 'since' ? -1 : 1; const other = ToTemporalTime(otherParam); const resolvedOptions = CopyOptions(options); const settings = GetDifferenceSettings(operation, resolvedOptions, 'time', [], 'nanosecond', 'hour'); let { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = DifferenceTime( GetSlot(plainTime, ISO_HOUR), GetSlot(plainTime, ISO_MINUTE), GetSlot(plainTime, ISO_SECOND), GetSlot(plainTime, ISO_MILLISECOND), GetSlot(plainTime, ISO_MICROSECOND), GetSlot(plainTime, ISO_NANOSECOND), GetSlot(other, ISO_HOUR), GetSlot(other, ISO_MINUTE), GetSlot(other, ISO_SECOND), GetSlot(other, ISO_MILLISECOND), GetSlot(other, ISO_MICROSECOND), GetSlot(other, ISO_NANOSECOND) ); ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = RoundDuration( 0, 0, 0, 0, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, settings.roundingIncrement, settings.smallestUnit, settings.roundingMode )); ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceDuration( 0, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, settings.largestUnit )); const Duration = GetIntrinsic('%Temporal.Duration%'); return new Duration( 0, 0, 0, 0, sign * hours, sign * minutes, sign * seconds, sign * milliseconds, sign * microseconds, sign * nanoseconds ); } export function DifferenceTemporalPlainYearMonth( operation: DifferenceOperation, yearMonth: Temporal.PlainYearMonth, otherParam: PlainYearMonthParams['until'][0], options: PlainYearMonthParams['until'][1] ): Temporal.Duration { const sign = operation === 'since' ? -1 : 1; const other = ToTemporalYearMonth(otherParam); const calendar = GetSlot(yearMonth, CALENDAR); const otherCalendar = GetSlot(other, CALENDAR); ThrowIfCalendarsNotEqual(calendar, otherCalendar, 'compute difference between months'); const resolvedOptions = CopyOptions(options); const settings = GetDifferenceSettings(operation, resolvedOptions, 'date', ['week', 'day'], 'month', 'year'); resolvedOptions.largestUnit = settings.largestUnit; const fieldNames = CalendarFields(calendar, ['monthCode', 'year']) as AnyTemporalKey[]; const thisFields = PrepareTemporalFields(yearMonth, fieldNames, []); thisFields.day = 1; const thisDate = CalendarDateFromFields(calendar, thisFields); const otherFields = PrepareTemporalFields(other, fieldNames, []); otherFields.day = 1; const otherDate = CalendarDateFromFields(calendar, otherFields); let { years, months } = CalendarDateUntil(calendar, thisDate, otherDate, resolvedOptions); if (settings.smallestUnit !== 'month' || settings.roundingIncrement !== 1) { ({ years, months } = RoundDuration( years, months, 0, 0, 0, 0, 0, 0, 0, 0, settings.roundingIncrement, settings.smallestUnit, settings.roundingMode, thisDate )); } const Duration = GetIntrinsic('%Temporal.Duration%'); return new Duration(sign * years, sign * months, 0, 0, 0, 0, 0, 0, 0, 0); } export function DifferenceTemporalZonedDateTime( operation: DifferenceOperation, zonedDateTime: Temporal.ZonedDateTime, otherParam: ZonedDateTimeParams['until'][0], options: ZonedDateTimeParams['until'][1] ): Temporal.Duration { const sign = operation === 'since' ? -1 : 1; const other = ToTemporalZonedDateTime(otherParam); const calendar = GetSlot(zonedDateTime, CALENDAR); const otherCalendar = GetSlot(other, CALENDAR); ThrowIfCalendarsNotEqual(calendar, otherCalendar, 'compute difference between dates'); const resolvedOptions = CopyOptions(options); const settings = GetDifferenceSettings(operation, resolvedOptions, 'datetime', [], 'nanosecond', 'hour'); resolvedOptions.largestUnit = settings.largestUnit; const ns1 = GetSlot(zonedDateTime, EPOCHNANOSECONDS); const ns2 = GetSlot(other, EPOCHNANOSECONDS); let years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds; if ( settings.largestUnit !== 'year' && settings.largestUnit !== 'month' && settings.largestUnit !== 'week' && settings.largestUnit !== 'day' ) { // The user is only asking for a time difference, so return difference of instants. years = 0; months = 0; weeks = 0; days = 0; ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = DifferenceInstant( ns1, ns2, settings.roundingIncrement, settings.smallestUnit as Temporal.TimeUnit, settings.largestUnit as Temporal.TimeUnit, settings.roundingMode )); } else { const timeZone = GetSlot(zonedDateTime, TIME_ZONE); if (!TimeZoneEquals(timeZone, GetSlot(other, TIME_ZONE))) { throw new RangeError( "When calculating difference between time zones, largestUnit must be 'hours' " + 'or smaller because day lengths can vary between time zones due to DST or time zone offset changes.' ); } ({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = DifferenceZonedDateTime(ns1, ns2, timeZone, calendar, settings.largestUnit, resolvedOptions)); ({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = RoundDuration( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, settings.roundingIncrement, settings.smallestUnit, settings.roundingMode, zonedDateTime )); ({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = AdjustRoundedDurationDays( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, settings.roundingIncrement, settings.smallestUnit, settings.roundingMode, zonedDateTime )); } const Duration = GetIntrinsic('%Temporal.Duration%'); return new Duration( sign * years, sign * months, sign * weeks, sign * days, sign * hours, sign * minutes, sign * seconds, sign * milliseconds, sign * microseconds, sign * nanoseconds ); } export function AddISODate( yearParam: number, monthParam: number, dayParam: number, yearsParam: number, monthsParam: number, weeksParam: number, daysParam: number, overflow: Temporal.ArithmeticOptions['overflow'] ) { let year = yearParam; let month = monthParam; let day = dayParam; let years = yearsParam; let months = monthsParam; let weeks = weeksParam; let days = daysParam; year += years; month += months; ({ year, month } = BalanceISOYearMonth(year, month)); ({ year, month, day } = RegulateISODate(year, month, day, overflow)); days += 7 * weeks; day += days; ({ year, month, day } = BalanceISODate(year, month, day)); return { year, month, day }; } function AddTime( hourParam: number, minuteParam: number, secondParam: number, millisecondParam: number, microsecondParam: number, nanosecondParam: number, hours: number, minutes: number, seconds: number, milliseconds: number, microseconds: number, nanoseconds: number ) { let hour = hourParam; let minute = minuteParam; let second = secondParam; let millisecond = millisecondParam; let microsecond = microsecondParam; let nanosecond = nanosecondParam; hour += hours; minute += minutes; second += seconds; millisecond += milliseconds; microsecond += microseconds; nanosecond += nanoseconds; let deltaDays = 0; ({ deltaDays, hour, minute, second, millisecond, microsecond, nanosecond } = BalanceTime( hour, minute, second, millisecond, microsecond, nanosecond )); return { deltaDays, hour, minute, second, millisecond, microsecond, nanosecond }; } function AddDuration( y1: number, mon1: number, w1: number, d1: number, h1: number, min1: number, s1: number, ms1: number, µs1: number, ns1: number, y2: number, mon2: number, w2: number, d2: number, h2: number, min2: number, s2: number, ms2: number, µs2: number, ns2: number, relativeTo: ReturnType ) { const largestUnit1 = DefaultTemporalLargestUnit(y1, mon1, w1, d1, h1, min1, s1, ms1, µs1, ns1); const largestUnit2 = DefaultTemporalLargestUnit(y2, mon2, w2, d2, h2, min2, s2, ms2, µs2, ns2); const largestUnit = LargerOfTwoTemporalUnits(largestUnit1, largestUnit2); let years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds; if (!relativeTo) { if (largestUnit === 'year' || largestUnit === 'month' || largestUnit === 'week') { throw new RangeError('relativeTo is required for years, months, or weeks arithmetic'); } years = months = weeks = 0; ({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceDuration( d1 + d2, JSBI.add(JSBI.BigInt(h1), JSBI.BigInt(h2)), JSBI.add(JSBI.BigInt(min1), JSBI.BigInt(min2)), JSBI.add(JSBI.BigInt(s1), JSBI.BigInt(s2)), JSBI.add(JSBI.BigInt(ms1), JSBI.BigInt(ms2)), JSBI.add(JSBI.BigInt(µs1), JSBI.BigInt(µs2)), JSBI.add(JSBI.BigInt(ns1), JSBI.BigInt(ns2)), largestUnit )); } else if (IsTemporalDate(relativeTo)) { const TemporalDuration = GetIntrinsic('%Temporal.Duration%'); const calendar = GetSlot(relativeTo, CALENDAR); const dateDuration1 = new TemporalDuration(y1, mon1, w1, d1, 0, 0, 0, 0, 0, 0); const dateDuration2 = new TemporalDuration(y2, mon2, w2, d2, 0, 0, 0, 0, 0, 0); const dateAdd = typeof calendar !== 'string' ? GetMethod(calendar, 'dateAdd') : undefined; const intermediate = CalendarDateAdd(calendar, relativeTo, dateDuration1, undefined, dateAdd); const end = CalendarDateAdd(calendar, intermediate, dateDuration2, undefined, dateAdd); const dateLargestUnit = LargerOfTwoTemporalUnits('day', largestUnit) as Temporal.DateUnit; const differenceOptions = ObjectCreate(null) as Temporal.DifferenceOptions; differenceOptions.largestUnit = dateLargestUnit; ({ years, months, weeks, days } = CalendarDateUntil(calendar, relativeTo, end, differenceOptions)); // Signs of date part and time part may not agree; balance them together ({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceDuration( days, JSBI.add(JSBI.BigInt(h1), JSBI.BigInt(h2)), JSBI.add(JSBI.BigInt(min1), JSBI.BigInt(min2)), JSBI.add(JSBI.BigInt(s1), JSBI.BigInt(s2)), JSBI.add(JSBI.BigInt(ms1), JSBI.BigInt(ms2)), JSBI.add(JSBI.BigInt(µs1), JSBI.BigInt(µs2)), JSBI.add(JSBI.BigInt(ns1), JSBI.BigInt(ns2)), largestUnit )); } else { // relativeTo is a ZonedDateTime const TemporalInstant = GetIntrinsic('%Temporal.Instant%'); const timeZone = GetSlot(relativeTo, TIME_ZONE); const calendar = GetSlot(relativeTo, CALENDAR); const intermediateNs = AddZonedDateTime( GetSlot(relativeTo, INSTANT), timeZone, calendar, y1, mon1, w1, d1, h1, min1, s1, ms1, µs1, ns1 ); const endNs = AddZonedDateTime( new TemporalInstant(intermediateNs), timeZone, calendar, y2, mon2, w2, d2, h2, min2, s2, ms2, µs2, ns2 ); if (largestUnit !== 'year' && largestUnit !== 'month' && largestUnit !== 'week' && largestUnit !== 'day') { // The user is only asking for a time difference, so return difference of instants. years = 0; months = 0; weeks = 0; days = 0; ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = DifferenceInstant( GetSlot(relativeTo, EPOCHNANOSECONDS), endNs, 1, 'nanosecond', largestUnit, 'halfExpand' )); } else { ({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = DifferenceZonedDateTime( GetSlot(relativeTo, EPOCHNANOSECONDS), endNs, timeZone, calendar, largestUnit, ObjectCreate(null) as Temporal.DifferenceOptions )); } } RejectDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; } function AddInstant( epochNanoseconds: JSBI, h: number | JSBI, min: number | JSBI, s: number | JSBI, ms: number | JSBI, µs: number | JSBI, ns: number | JSBI ) { let sum = ZERO; sum = JSBI.add(sum, JSBI.BigInt(ns)); sum = JSBI.add(sum, JSBI.multiply(JSBI.BigInt(µs), THOUSAND)); sum = JSBI.add(sum, JSBI.multiply(JSBI.BigInt(ms), MILLION)); sum = JSBI.add(sum, JSBI.multiply(JSBI.BigInt(s), BILLION)); sum = JSBI.add(sum, JSBI.multiply(JSBI.BigInt(min), JSBI.BigInt(60 * 1e9))); sum = JSBI.add(sum, JSBI.multiply(JSBI.BigInt(h), JSBI.BigInt(60 * 60 * 1e9))); const result = JSBI.add(epochNanoseconds, sum); ValidateEpochNanoseconds(result); return result; } function AddDateTime( year: number, month: number, day: number, hourParam: number, minuteParam: number, secondParam: number, millisecondParam: number, microsecondParam: number, nanosecondParam: number, calendar: CalendarSlot, years: number, months: number, weeks: number, daysParam: number, hours: number, minutes: number, seconds: number, milliseconds: number, microseconds: number, nanoseconds: number, options?: Temporal.ArithmeticOptions ) { let days = daysParam; // Add the time part let { deltaDays, hour, minute, second, millisecond, microsecond, nanosecond } = AddTime( hourParam, minuteParam, secondParam, millisecondParam, microsecondParam, nanosecondParam, hours, minutes, seconds, milliseconds, microseconds, nanoseconds ); days += deltaDays; // Delegate the date part addition to the calendar const TemporalDuration = GetIntrinsic('%Temporal.Duration%'); const datePart = CreateTemporalDate(year, month, day, calendar); const dateDuration = new TemporalDuration(years, months, weeks, days, 0, 0, 0, 0, 0, 0); const addedDate = CalendarDateAdd(calendar, datePart, dateDuration, options); return { year: GetSlot(addedDate, ISO_YEAR), month: GetSlot(addedDate, ISO_MONTH), day: GetSlot(addedDate, ISO_DAY), hour, minute, second, millisecond, microsecond, nanosecond }; } export function AddZonedDateTime( instant: Temporal.Instant, timeZone: string | Temporal.TimeZoneProtocol, calendar: CalendarSlot, years: number, months: number, weeks: number, days: number, h: number | JSBI, min: number | JSBI, s: number | JSBI, ms: number | JSBI, µs: number | JSBI, ns: number | JSBI, options?: Temporal.ArithmeticOptions ) { // If only time is to be added, then use Instant math. It's not OK to fall // through to the date/time code below because compatible disambiguation in // the PlainDateTime=>Instant conversion will change the offset of any // ZonedDateTime in the repeated clock time after a backwards transition. // When adding/subtracting time units and not dates, this disambiguation is // not expected and so is avoided below via a fast path for time-only // arithmetic. // BTW, this behavior is similar in spirit to offset: 'prefer' in `with`. const TemporalDuration = GetIntrinsic('%Temporal.Duration%'); if (DurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0) === 0) { return AddInstant(GetSlot(instant, EPOCHNANOSECONDS), h, min, s, ms, µs, ns); } // RFC 5545 requires the date portion to be added in calendar days and the // time portion to be added in exact time. const dt = GetPlainDateTimeFor(timeZone, instant, calendar); const datePart = CreateTemporalDate(GetSlot(dt, ISO_YEAR), GetSlot(dt, ISO_MONTH), GetSlot(dt, ISO_DAY), calendar); const dateDuration = new TemporalDuration(years, months, weeks, days, 0, 0, 0, 0, 0, 0); const addedDate = CalendarDateAdd(calendar, datePart, dateDuration, options); const dtIntermediate = CreateTemporalDateTime( GetSlot(addedDate, ISO_YEAR), GetSlot(addedDate, ISO_MONTH), GetSlot(addedDate, ISO_DAY), GetSlot(dt, ISO_HOUR), GetSlot(dt, ISO_MINUTE), GetSlot(dt, ISO_SECOND), GetSlot(dt, ISO_MILLISECOND), GetSlot(dt, ISO_MICROSECOND), GetSlot(dt, ISO_NANOSECOND), calendar ); // Note that 'compatible' is used below because this disambiguation behavior // is required by RFC 5545. const instantIntermediate = GetInstantFor(timeZone, dtIntermediate, 'compatible'); return AddInstant(GetSlot(instantIntermediate, EPOCHNANOSECONDS), h, min, s, ms, µs, ns); } type AddSubtractOperation = 'add' | 'subtract'; export function AddDurationToOrSubtractDurationFromDuration( operation: AddSubtractOperation, duration: Temporal.Duration, other: DurationParams['add'][0], optionsParam: DurationParams['add'][1] ): Temporal.Duration { const sign = operation === 'subtract' ? -1 : 1; let { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ToTemporalDurationRecord(other); const options = GetOptionsObject(optionsParam); const relativeTo = ToRelativeTemporalObject(options); ({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = AddDuration( GetSlot(duration, YEARS), GetSlot(duration, MONTHS), GetSlot(duration, WEEKS), GetSlot(duration, DAYS), GetSlot(duration, HOURS), GetSlot(duration, MINUTES), GetSlot(duration, SECONDS), GetSlot(duration, MILLISECONDS), GetSlot(duration, MICROSECONDS), GetSlot(duration, NANOSECONDS), sign * years, sign * months, sign * weeks, sign * days, sign * hours, sign * minutes, sign * seconds, sign * milliseconds, sign * microseconds, sign * nanoseconds, relativeTo )); const Duration = GetIntrinsic('%Temporal.Duration%'); return new Duration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); } export function AddDurationToOrSubtractDurationFromInstant( operation: AddSubtractOperation, instant: Temporal.Instant, durationLike: InstantParams['add'][0] ): Temporal.Instant { const sign = operation === 'subtract' ? -1 : 1; const { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ToLimitedTemporalDuration(durationLike, [ 'years', 'months', 'weeks', 'days' ]); const ns = AddInstant( GetSlot(instant, EPOCHNANOSECONDS), sign * hours, sign * minutes, sign * seconds, sign * milliseconds, sign * microseconds, sign * nanoseconds ); const Instant = GetIntrinsic('%Temporal.Instant%'); return new Instant(ns); } export function AddDurationToOrSubtractDurationFromPlainDateTime( operation: AddSubtractOperation, dateTime: Temporal.PlainDateTime, durationLike: PlainDateTimeParams['add'][0], optionsParam: PlainDateTimeParams['add'][1] ): Temporal.PlainDateTime { const sign = operation === 'subtract' ? -1 : 1; const { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ToTemporalDurationRecord(durationLike); const options = GetOptionsObject(optionsParam); const calendar = GetSlot(dateTime, CALENDAR); const { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = AddDateTime( GetSlot(dateTime, ISO_YEAR), GetSlot(dateTime, ISO_MONTH), GetSlot(dateTime, ISO_DAY), GetSlot(dateTime, ISO_HOUR), GetSlot(dateTime, ISO_MINUTE), GetSlot(dateTime, ISO_SECOND), GetSlot(dateTime, ISO_MILLISECOND), GetSlot(dateTime, ISO_MICROSECOND), GetSlot(dateTime, ISO_NANOSECOND), calendar, sign * years, sign * months, sign * weeks, sign * days, sign * hours, sign * minutes, sign * seconds, sign * milliseconds, sign * microseconds, sign * nanoseconds, options ); return CreateTemporalDateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar); } export function AddDurationToOrSubtractDurationFromPlainTime( operation: AddSubtractOperation, temporalTime: Temporal.PlainTime, durationLike: PlainTimeParams['add'][0] ): Temporal.PlainTime { const sign = operation === 'subtract' ? -1 : 1; const { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ToTemporalDurationRecord(durationLike); let { hour, minute, second, millisecond, microsecond, nanosecond } = AddTime( GetSlot(temporalTime, ISO_HOUR), GetSlot(temporalTime, ISO_MINUTE), GetSlot(temporalTime, ISO_SECOND), GetSlot(temporalTime, ISO_MILLISECOND), GetSlot(temporalTime, ISO_MICROSECOND), GetSlot(temporalTime, ISO_NANOSECOND), sign * hours, sign * minutes, sign * seconds, sign * milliseconds, sign * microseconds, sign * nanoseconds ); ({ hour, minute, second, millisecond, microsecond, nanosecond } = RegulateTime( hour, minute, second, millisecond, microsecond, nanosecond, 'reject' )); const PlainTime = GetIntrinsic('%Temporal.PlainTime%'); return new PlainTime(hour, minute, second, millisecond, microsecond, nanosecond); } export function AddDurationToOrSubtractDurationFromPlainYearMonth( operation: AddSubtractOperation, yearMonth: Temporal.PlainYearMonth, durationLike: PlainYearMonthParams['add'][0], optionsParam: PlainYearMonthParams['add'][1] ): Temporal.PlainYearMonth { let duration = ToTemporalDurationRecord(durationLike); if (operation === 'subtract') { duration = { years: -duration.years, months: -duration.months, weeks: -duration.weeks, days: -duration.days, hours: -duration.hours, minutes: -duration.minutes, seconds: -duration.seconds, milliseconds: -duration.milliseconds, microseconds: -duration.microseconds, nanoseconds: -duration.nanoseconds }; } let { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration; ({ days } = BalanceDuration(days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, 'day')); const options = GetOptionsObject(optionsParam); const calendar = GetSlot(yearMonth, CALENDAR); const fieldNames = CalendarFields(calendar, ['monthCode', 'year'] as const); const fields = PrepareTemporalFields(yearMonth, fieldNames, []); const fieldsCopy = ObjectCreate(null); CopyDataProperties(fieldsCopy, fields, []); fields.day = 1; // PrepareTemporalFields returns a type where 'day' is potentially undefined, // but TS doesn't narrow the type as a result of the assignment above. uncheckedAssertNarrowedType(fields, '`day` is guaranteed to be non-undefined'); let startDate = CalendarDateFromFields(calendar, fields); const sign = DurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0); const dateAdd = GetMethod(calendar, 'dateAdd'); const Duration = GetIntrinsic('%Temporal.Duration%'); if (sign < 0) { const oneMonthDuration = new Duration(0, 1, 0, 0, 0, 0, 0, 0, 0, 0); const nextMonth = CalendarDateAdd(calendar, startDate, oneMonthDuration, undefined, dateAdd); const minusDayDuration = new Duration(0, 0, 0, -1, 0, 0, 0, 0, 0, 0); const endOfMonth = CalendarDateAdd(calendar, nextMonth, minusDayDuration, undefined, dateAdd); fieldsCopy.day = CalendarDay(calendar, endOfMonth); startDate = CalendarDateFromFields(calendar, fieldsCopy); } const durationToAdd = new Duration(years, months, weeks, days, 0, 0, 0, 0, 0, 0); const optionsCopy = CopyOptions(options); const addedDate = CalendarDateAdd(calendar, startDate, durationToAdd, options, dateAdd); const addedDateFields = PrepareTemporalFields(addedDate, fieldNames, []); return CalendarYearMonthFromFields(calendar, addedDateFields, optionsCopy); } export function AddDurationToOrSubtractDurationFromZonedDateTime( operation: AddSubtractOperation, zonedDateTime: Temporal.ZonedDateTime, durationLike: ZonedDateTimeParams['add'][0], optionsParam: ZonedDateTimeParams['add'][1] ): Temporal.ZonedDateTime { const sign = operation === 'subtract' ? -1 : 1; const { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ToTemporalDurationRecord(durationLike); const options = GetOptionsObject(optionsParam); const timeZone = GetSlot(zonedDateTime, TIME_ZONE); const calendar = GetSlot(zonedDateTime, CALENDAR); const epochNanoseconds = AddZonedDateTime( GetSlot(zonedDateTime, INSTANT), timeZone, calendar, sign * years, sign * months, sign * weeks, sign * days, sign * hours, sign * minutes, sign * seconds, sign * milliseconds, sign * microseconds, sign * nanoseconds, options ); return CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar); } function RoundNumberToIncrement(quantity: JSBI, increment: JSBI, mode: Temporal.RoundingMode) { if (JSBI.equal(increment, ONE)) return quantity; let { quotient, remainder } = divmod(quantity, increment); if (JSBI.equal(remainder, ZERO)) return quantity; const sign = JSBI.lessThan(remainder, ZERO) ? -1 : 1; const tiebreaker = abs(JSBI.multiply(remainder, JSBI.BigInt(2))); const tie = JSBI.equal(tiebreaker, increment); const expandIsNearer = JSBI.greaterThan(tiebreaker, increment); switch (mode) { case 'ceil': if (sign > 0) quotient = JSBI.add(quotient, JSBI.BigInt(sign)); break; case 'floor': if (sign < 0) quotient = JSBI.add(quotient, JSBI.BigInt(sign)); break; case 'expand': // always expand if there is a remainder quotient = JSBI.add(quotient, JSBI.BigInt(sign)); break; case 'trunc': // no change needed, because divmod is a truncation break; case 'halfCeil': if (expandIsNearer || (tie && sign > 0)) { quotient = JSBI.add(quotient, JSBI.BigInt(sign)); } break; case 'halfFloor': if (expandIsNearer || (tie && sign < 0)) { quotient = JSBI.add(quotient, JSBI.BigInt(sign)); } break; case 'halfExpand': // "half up away from zero" if (expandIsNearer || tie) { quotient = JSBI.add(quotient, JSBI.BigInt(sign)); } break; case 'halfTrunc': if (expandIsNearer) { quotient = JSBI.add(quotient, JSBI.BigInt(sign)); } break; case 'halfEven': if (expandIsNearer || (tie && JSBI.toNumber(JSBI.remainder(abs(quotient), JSBI.BigInt(2))) === 1)) { quotient = JSBI.add(quotient, JSBI.BigInt(sign)); } break; } return JSBI.multiply(quotient, increment); } export function RoundInstant( epochNs: JSBI, increment: number, unit: keyof typeof nsPerTimeUnit, roundingMode: Temporal.RoundingMode ) { let { remainder } = NonNegativeBigIntDivmod(epochNs, DAY_NANOS); const wholeDays = JSBI.subtract(epochNs, remainder); const roundedRemainder = RoundNumberToIncrement( remainder, JSBI.BigInt(nsPerTimeUnit[unit] * increment), roundingMode ); return JSBI.add(wholeDays, roundedRemainder); } export function RoundISODateTime( yearParam: number, monthParam: number, dayParam: number, hourParam: number, minuteParam: number, secondParam: number, millisecondParam: number, microsecondParam: number, nanosecondParam: number, increment: number, unit: UnitSmallerThanOrEqualTo<'day'>, roundingMode: Temporal.RoundingMode, dayLengthNs = 86400e9 ) { const { deltaDays, hour, minute, second, millisecond, microsecond, nanosecond } = RoundTime( hourParam, minuteParam, secondParam, millisecondParam, microsecondParam, nanosecondParam, increment, unit, roundingMode, dayLengthNs ); const { year, month, day } = BalanceISODate(yearParam, monthParam, dayParam + deltaDays); return { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond }; } export function RoundTime( hour: number, minute: number, second: number, millisecond: number, microsecond: number, nanosecond: number, increment: number, unit: keyof typeof nsPerTimeUnit | 'day', roundingMode: Temporal.RoundingMode, dayLengthNs = 86400e9 ) { let quantity = ZERO; switch (unit) { case 'day': case 'hour': quantity = JSBI.BigInt(hour); // fall through case 'minute': quantity = JSBI.add(JSBI.multiply(quantity, SIXTY), JSBI.BigInt(minute)); // fall through case 'second': quantity = JSBI.add(JSBI.multiply(quantity, SIXTY), JSBI.BigInt(second)); // fall through case 'millisecond': quantity = JSBI.add(JSBI.multiply(quantity, THOUSAND), JSBI.BigInt(millisecond)); // fall through case 'microsecond': quantity = JSBI.add(JSBI.multiply(quantity, THOUSAND), JSBI.BigInt(microsecond)); // fall through case 'nanosecond': quantity = JSBI.add(JSBI.multiply(quantity, THOUSAND), JSBI.BigInt(nanosecond)); } const nsPerUnit = unit === 'day' ? dayLengthNs : nsPerTimeUnit[unit]; const rounded = RoundNumberToIncrement(quantity, JSBI.BigInt(nsPerUnit * increment), roundingMode); const result = JSBI.toNumber(JSBI.divide(rounded, JSBI.BigInt(nsPerUnit))); switch (unit) { case 'day': return { deltaDays: result, hour: 0, minute: 0, second: 0, millisecond: 0, microsecond: 0, nanosecond: 0 }; case 'hour': return BalanceTime(result, 0, 0, 0, 0, 0); case 'minute': return BalanceTime(hour, result, 0, 0, 0, 0); case 'second': return BalanceTime(hour, minute, result, 0, 0, 0); case 'millisecond': return BalanceTime(hour, minute, second, result, 0, 0); case 'microsecond': return BalanceTime(hour, minute, second, millisecond, result, 0); case 'nanosecond': return BalanceTime(hour, minute, second, millisecond, microsecond, result); default: throw new Error(`Invalid unit ${unit}`); } } function DaysUntil( earlier: Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime, later: Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime ) { return DifferenceISODate( GetSlot(earlier, ISO_YEAR), GetSlot(earlier, ISO_MONTH), GetSlot(earlier, ISO_DAY), GetSlot(later, ISO_YEAR), GetSlot(later, ISO_MONTH), GetSlot(later, ISO_DAY), 'day' ).days; } function MoveRelativeDate( calendar: CalendarSlot, relativeToParam: NonNullable>, duration: Temporal.Duration, dateAdd: Temporal.CalendarProtocol['dateAdd'] | undefined ) { const later = CalendarDateAdd(calendar, relativeToParam, duration, undefined, dateAdd); const days = DaysUntil(relativeToParam, later); return { relativeTo: later, days }; } export function MoveRelativeZonedDateTime( relativeTo: Temporal.ZonedDateTime, years: number, months: number, weeks: number, days: number ) { const timeZone = GetSlot(relativeTo, TIME_ZONE); const calendar = GetSlot(relativeTo, CALENDAR); const intermediateNs = AddZonedDateTime( GetSlot(relativeTo, INSTANT), timeZone, calendar, years, months, weeks, days, 0, 0, 0, 0, 0, 0 ); return CreateTemporalZonedDateTime(intermediateNs, timeZone, calendar); } export function AdjustRoundedDurationDays( yearsParam: number, monthsParam: number, weeksParam: number, daysParam: number, hoursParam: number, minutesParam: number, secondsParam: number, millisecondsParam: number, microsecondsParam: number, nanosecondsParam: number, increment: number, unit: Temporal.DateTimeUnit, roundingMode: Temporal.RoundingMode, relativeTo: ReturnType ) { let years = yearsParam; let months = monthsParam; let weeks = weeksParam; let days = daysParam; let hours = hoursParam; let minutes = minutesParam; let seconds = secondsParam; let milliseconds = millisecondsParam; let microseconds = microsecondsParam; let nanoseconds = nanosecondsParam; if ( !IsTemporalZonedDateTime(relativeTo) || unit === 'year' || unit === 'month' || unit === 'week' || unit === 'day' || (unit === 'nanosecond' && increment === 1) ) { return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; } // There's one more round of rounding possible: if relativeTo is a // ZonedDateTime, the time units could have rounded up into enough hours // to exceed the day length. If this happens, grow the date part by a // single day and re-run exact time rounding on the smaller remainder. DO // NOT RECURSE, because once the extra hours are sucked up into the date // duration, there's no way for another full day to come from the next // round of rounding. And if it were possible (e.g. contrived calendar // with 30-minute-long "days") then it'd risk an infinite loop. let timeRemainderNs = TotalDurationNanoseconds( 0, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, 0 ); const direction = MathSign(JSBI.toNumber(timeRemainderNs)); const timeZone = GetSlot(relativeTo, TIME_ZONE); const calendar = GetSlot(relativeTo, CALENDAR); const dayStart = AddZonedDateTime( GetSlot(relativeTo, INSTANT), timeZone, calendar, years, months, weeks, days, 0, 0, 0, 0, 0, 0 ); const TemporalInstant = GetIntrinsic('%Temporal.Instant%'); const dayEnd = AddZonedDateTime( new TemporalInstant(dayStart), timeZone, calendar, 0, 0, 0, direction, 0, 0, 0, 0, 0, 0 ); const dayLengthNs = JSBI.subtract(dayEnd, dayStart); if ( JSBI.greaterThanOrEqual(JSBI.multiply(JSBI.subtract(timeRemainderNs, dayLengthNs), JSBI.BigInt(direction)), ZERO) ) { ({ years, months, weeks, days } = AddDuration( years, months, weeks, days, 0, 0, 0, 0, 0, 0, 0, 0, 0, direction, 0, 0, 0, 0, 0, 0, relativeTo )); timeRemainderNs = RoundInstant(JSBI.subtract(timeRemainderNs, dayLengthNs), increment, unit, roundingMode); ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceDuration( 0, 0, 0, 0, 0, 0, JSBI.toNumber(timeRemainderNs), 'hour' )); } return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; } export function RoundDuration( yearsParam: number, monthsParam: number, weeksParam: number, daysParam: number, hoursParam: number, minutesParam: number, secondsParam: number, millisecondsParam: number, microsecondsParam: number, nanosecondsParam: number, increment: number, unit: Temporal.DateTimeUnit, roundingMode: Temporal.RoundingMode, relativeToParam: ReturnType = undefined ) { let years = yearsParam; let months = monthsParam; let weeks = weeksParam; let days = daysParam; let hours = hoursParam; let minutes = minutesParam; let seconds = secondsParam; let milliseconds = millisecondsParam; let microseconds = microsecondsParam; let nanoseconds = JSBI.BigInt(nanosecondsParam); const TemporalDuration = GetIntrinsic('%Temporal.Duration%'); let calendar, zdtRelative; // A cast is used below because relativeTo will be either PlainDate or // undefined for the rest of this long method (after any ZDT=>PlainDate // conversion below), and TS isn't smart enough to know that the type has // changed. See https://github.com/microsoft/TypeScript/issues/27706. let relativeTo = relativeToParam as Temporal.PlainDate | undefined; if (relativeTo) { if (IsTemporalZonedDateTime(relativeTo)) { zdtRelative = relativeTo; relativeTo = ToTemporalDate(relativeTo); } else if (!IsTemporalDate(relativeTo)) { throw new TypeError('starting point must be PlainDate or ZonedDateTime'); } calendar = GetSlot(relativeTo, CALENDAR); } // First convert time units up to days, if rounding to days or higher units. // If rounding relative to a ZonedDateTime, then some days may not be 24h. // TS doesn't know that `dayLengthNs` is only used if the unit is day or // larger. We'll cast away `undefined` when it's used lower down below. let dayLengthNs: JSBI | undefined; if (unit === 'year' || unit === 'month' || unit === 'week' || unit === 'day') { nanoseconds = TotalDurationNanoseconds(0, hours, minutes, seconds, milliseconds, microseconds, nanosecondsParam, 0); let intermediate; if (zdtRelative) { intermediate = MoveRelativeZonedDateTime(zdtRelative, years, months, weeks, days); } let deltaDays; let dayLength: number; ({ days: deltaDays, nanoseconds, dayLengthNs: dayLength } = NanosecondsToDays(nanoseconds, intermediate)); dayLengthNs = JSBI.BigInt(dayLength); days += deltaDays; hours = minutes = seconds = milliseconds = microseconds = 0; } let total: number; switch (unit) { case 'year': { if (!calendar) throw new RangeError('A starting point is required for years rounding'); assertExists(relativeTo); // convert months and weeks to days by calculating difference( // relativeTo + years, relativeTo + { years, months, weeks }) const yearsDuration = new TemporalDuration(years); const dateAdd = typeof calendar !== 'string' ? GetMethod(calendar, 'dateAdd') : undefined; const yearsLater = CalendarDateAdd(calendar, relativeTo, yearsDuration, undefined, dateAdd); const yearsMonthsWeeks = new TemporalDuration(years, months, weeks); const yearsMonthsWeeksLater = CalendarDateAdd(calendar, relativeTo, yearsMonthsWeeks, undefined, dateAdd); const monthsWeeksInDays = DaysUntil(yearsLater, yearsMonthsWeeksLater); relativeTo = yearsLater; days += monthsWeeksInDays; const wholeDays = new TemporalDuration(0, 0, 0, days); const wholeDaysLater = CalendarDateAdd(calendar, relativeTo, wholeDays, undefined, dateAdd); const untilOptions = ObjectCreate(null) as Temporal.DifferenceOptions; untilOptions.largestUnit = 'year'; const yearsPassed = CalendarDateUntil(calendar, relativeTo, wholeDaysLater, untilOptions).years; years += yearsPassed; const oldRelativeTo = relativeTo; const yearsPassedDuration = new TemporalDuration(yearsPassed); relativeTo = CalendarDateAdd(calendar, relativeTo, yearsPassedDuration, undefined, dateAdd); const daysPassed = DaysUntil(oldRelativeTo, relativeTo); days -= daysPassed; const oneYear = new TemporalDuration(days < 0 ? -1 : 1); let { days: oneYearDays } = MoveRelativeDate(calendar, relativeTo, oneYear, dateAdd); // Note that `nanoseconds` below (here and in similar code for months, // weeks, and days further below) isn't actually nanoseconds for the // full date range. Instead, it's a BigInt representation of total // days multiplied by the number of nanoseconds in the last day of // the duration. This lets us do days-or-larger rounding using BigInt // math which reduces precision loss. oneYearDays = MathAbs(oneYearDays); // dayLengthNs is never undefined if unit is `day` or larger. assertExists(dayLengthNs); const divisor = JSBI.multiply(JSBI.BigInt(oneYearDays), dayLengthNs); nanoseconds = JSBI.add( JSBI.add(JSBI.multiply(divisor, JSBI.BigInt(years)), JSBI.multiply(JSBI.BigInt(days), dayLengthNs)), nanoseconds ); const rounded = RoundNumberToIncrement(nanoseconds, JSBI.multiply(divisor, JSBI.BigInt(increment)), roundingMode); total = BigIntDivideToNumber(nanoseconds, divisor); years = JSBI.toNumber(JSBI.divide(rounded, divisor)); nanoseconds = ZERO; months = weeks = days = 0; break; } case 'month': { if (!calendar) throw new RangeError('A starting point is required for months rounding'); assertExists(relativeTo); // convert weeks to days by calculating difference(relativeTo + // { years, months }, relativeTo + { years, months, weeks }) const yearsMonths = new TemporalDuration(years, months); const dateAdd = typeof calendar !== 'string' ? GetMethod(calendar, 'dateAdd') : undefined; const yearsMonthsLater = CalendarDateAdd(calendar, relativeTo, yearsMonths, undefined, dateAdd); const yearsMonthsWeeks = new TemporalDuration(years, months, weeks); const yearsMonthsWeeksLater = CalendarDateAdd(calendar, relativeTo, yearsMonthsWeeks, undefined, dateAdd); const weeksInDays = DaysUntil(yearsMonthsLater, yearsMonthsWeeksLater); relativeTo = yearsMonthsLater; days += weeksInDays; // Months may be different lengths of days depending on the calendar, // convert days to months in a loop as described above under 'years'. const sign = MathSign(days); const oneMonth = new TemporalDuration(0, days < 0 ? -1 : 1); let oneMonthDays: number; ({ relativeTo, days: oneMonthDays } = MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd)); while (MathAbs(days) >= MathAbs(oneMonthDays)) { months += sign; days -= oneMonthDays; ({ relativeTo, days: oneMonthDays } = MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd)); } oneMonthDays = MathAbs(oneMonthDays); // dayLengthNs is never undefined if unit is `day` or larger. assertExists(dayLengthNs); const divisor = JSBI.multiply(JSBI.BigInt(oneMonthDays), dayLengthNs); nanoseconds = JSBI.add( JSBI.add(JSBI.multiply(divisor, JSBI.BigInt(months)), JSBI.multiply(JSBI.BigInt(days), dayLengthNs)), nanoseconds ); const rounded = RoundNumberToIncrement(nanoseconds, JSBI.multiply(divisor, JSBI.BigInt(increment)), roundingMode); total = BigIntDivideToNumber(nanoseconds, divisor); months = JSBI.toNumber(JSBI.divide(rounded, divisor)); nanoseconds = ZERO; weeks = days = 0; break; } case 'week': { if (!calendar) throw new RangeError('A starting point is required for weeks rounding'); assertExists(relativeTo); // Weeks may be different lengths of days depending on the calendar, // convert days to weeks in a loop as described above under 'years'. const sign = MathSign(days); const oneWeek = new TemporalDuration(0, 0, days < 0 ? -1 : 1); const dateAdd = typeof calendar !== 'string' ? GetMethod(calendar, 'dateAdd') : undefined; let oneWeekDays; ({ relativeTo, days: oneWeekDays } = MoveRelativeDate(calendar, relativeTo, oneWeek, dateAdd)); while (MathAbs(days) >= MathAbs(oneWeekDays)) { weeks += sign; days -= oneWeekDays; ({ relativeTo, days: oneWeekDays } = MoveRelativeDate(calendar, relativeTo, oneWeek, dateAdd)); } oneWeekDays = MathAbs(oneWeekDays); // dayLengthNs is never undefined if unit is `day` or larger. assertExists(dayLengthNs); const divisor = JSBI.multiply(JSBI.BigInt(oneWeekDays), dayLengthNs); nanoseconds = JSBI.add( JSBI.add(JSBI.multiply(divisor, JSBI.BigInt(weeks)), JSBI.multiply(JSBI.BigInt(days), dayLengthNs)), nanoseconds ); const rounded = RoundNumberToIncrement(nanoseconds, JSBI.multiply(divisor, JSBI.BigInt(increment)), roundingMode); total = BigIntDivideToNumber(nanoseconds, divisor); weeks = JSBI.toNumber(JSBI.divide(rounded, divisor)); nanoseconds = ZERO; days = 0; break; } case 'day': { // dayLengthNs is never undefined if unit is `day` or larger. assertExists(dayLengthNs); const divisor = dayLengthNs; nanoseconds = JSBI.add(JSBI.multiply(divisor, JSBI.BigInt(days)), nanoseconds); const rounded = RoundNumberToIncrement(nanoseconds, JSBI.multiply(divisor, JSBI.BigInt(increment)), roundingMode); total = BigIntDivideToNumber(nanoseconds, divisor); days = JSBI.toNumber(JSBI.divide(rounded, divisor)); nanoseconds = ZERO; break; } case 'hour': { const divisor = 3600e9; let allNanoseconds = JSBI.multiply(JSBI.BigInt(hours), JSBI.BigInt(3600e9)); allNanoseconds = JSBI.add(allNanoseconds, JSBI.multiply(JSBI.BigInt(minutes), JSBI.BigInt(60e9))); allNanoseconds = JSBI.add(allNanoseconds, JSBI.multiply(JSBI.BigInt(seconds), BILLION)); allNanoseconds = JSBI.add(allNanoseconds, JSBI.multiply(JSBI.BigInt(milliseconds), MILLION)); allNanoseconds = JSBI.add(allNanoseconds, JSBI.multiply(JSBI.BigInt(microseconds), THOUSAND)); allNanoseconds = JSBI.add(allNanoseconds, nanoseconds); total = BigIntDivideToNumber(allNanoseconds, JSBI.BigInt(divisor)); const rounded = RoundNumberToIncrement(allNanoseconds, JSBI.BigInt(divisor * increment), roundingMode); hours = JSBI.toNumber(JSBI.divide(rounded, JSBI.BigInt(divisor))); nanoseconds = ZERO; minutes = seconds = milliseconds = microseconds = 0; break; } case 'minute': { const divisor = 60e9; let allNanoseconds = JSBI.multiply(JSBI.BigInt(minutes), JSBI.BigInt(60e9)); allNanoseconds = JSBI.add(allNanoseconds, JSBI.multiply(JSBI.BigInt(seconds), BILLION)); allNanoseconds = JSBI.add(allNanoseconds, JSBI.multiply(JSBI.BigInt(milliseconds), MILLION)); allNanoseconds = JSBI.add(allNanoseconds, JSBI.multiply(JSBI.BigInt(microseconds), THOUSAND)); allNanoseconds = JSBI.add(allNanoseconds, nanoseconds); total = BigIntDivideToNumber(allNanoseconds, JSBI.BigInt(divisor)); const rounded = RoundNumberToIncrement(allNanoseconds, JSBI.BigInt(divisor * increment), roundingMode); minutes = JSBI.toNumber(JSBI.divide(rounded, JSBI.BigInt(divisor))); nanoseconds = ZERO; seconds = milliseconds = microseconds = 0; break; } case 'second': { const divisor = 1e9; let allNanoseconds = JSBI.multiply(JSBI.BigInt(seconds), BILLION); allNanoseconds = JSBI.add(allNanoseconds, JSBI.multiply(JSBI.BigInt(milliseconds), MILLION)); allNanoseconds = JSBI.add(allNanoseconds, JSBI.multiply(JSBI.BigInt(microseconds), THOUSAND)); allNanoseconds = JSBI.add(allNanoseconds, nanoseconds); total = BigIntDivideToNumber(allNanoseconds, JSBI.BigInt(divisor)); const rounded = RoundNumberToIncrement(allNanoseconds, JSBI.BigInt(divisor * increment), roundingMode); seconds = JSBI.toNumber(JSBI.divide(rounded, JSBI.BigInt(divisor))); nanoseconds = ZERO; milliseconds = microseconds = 0; break; } case 'millisecond': { const divisor = 1e6; let allNanoseconds = JSBI.multiply(JSBI.BigInt(milliseconds), MILLION); allNanoseconds = JSBI.add(allNanoseconds, JSBI.multiply(JSBI.BigInt(microseconds), THOUSAND)); allNanoseconds = JSBI.add(allNanoseconds, nanoseconds); total = BigIntDivideToNumber(allNanoseconds, JSBI.BigInt(divisor)); const rounded = RoundNumberToIncrement(allNanoseconds, JSBI.BigInt(divisor * increment), roundingMode); milliseconds = JSBI.toNumber(JSBI.divide(rounded, JSBI.BigInt(divisor))); nanoseconds = ZERO; microseconds = 0; break; } case 'microsecond': { const divisor = 1e3; let allNanoseconds = JSBI.multiply(JSBI.BigInt(microseconds), THOUSAND); allNanoseconds = JSBI.add(allNanoseconds, nanoseconds); total = BigIntDivideToNumber(allNanoseconds, JSBI.BigInt(divisor)); const rounded = RoundNumberToIncrement(allNanoseconds, JSBI.BigInt(divisor * increment), roundingMode); microseconds = JSBI.toNumber(JSBI.divide(rounded, JSBI.BigInt(divisor))); nanoseconds = ZERO; break; } case 'nanosecond': { total = JSBI.toNumber(nanoseconds); nanoseconds = RoundNumberToIncrement(JSBI.BigInt(nanoseconds), JSBI.BigInt(increment), roundingMode); break; } } return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds: JSBI.toNumber(nanoseconds), total }; } export function CompareISODate(y1: number, m1: number, d1: number, y2: number, m2: number, d2: number) { for (const [x, y] of [ [y1, y2], [m1, m2], [d1, d2] ]) { if (x !== y) return ComparisonResult(x - y); } return 0; } // Not abstract operations from the spec function NonNegativeBigIntDivmod(x: JSBI, y: JSBI) { let { quotient, remainder } = divmod(x, y); if (JSBI.lessThan(remainder, ZERO)) { quotient = JSBI.subtract(quotient, ONE); remainder = JSBI.add(remainder, y); } return { quotient, remainder }; } export function BigIntFloorDiv(left: JSBI, right: JSBI) { const { quotient, remainder } = divmod(left, right); if (!isZero(remainder) && !isNegativeJSBI(left) != !isNegativeJSBI(right)) { return JSBI.subtract(quotient, ONE); } return quotient; } /** Divide two JSBIs, and return the result as a Number, including the remainder. */ export function BigIntDivideToNumber(dividend: JSBI, divisor: JSBI) { const { quotient, remainder } = divmod(dividend, divisor); const result = JSBI.toNumber(quotient) + JSBI.toNumber(remainder) / JSBI.toNumber(divisor); return result; } // Defaults to native bigint, or something "native bigint-like". // For users of Temporal that are running in environments without native BigInt, // the only guarantee we should give is that the returned object's toString will // return a string containing an accurate base 10 value of this bigint. This // form factor should correctly interop with other bigint compat libraries // easily. type ExternalBigInt = bigint; export function ToBigIntExternal(arg: unknown): ExternalBigInt { const jsbiBI = ToBigInt(arg); if (typeof (globalThis as any).BigInt !== 'undefined') return (globalThis as any).BigInt(jsbiBI.toString(10)); return jsbiBI as unknown as ExternalBigInt; } export function ToBigInt(arg: unknown): JSBI { let prim = arg; if (typeof arg === 'object') { const toPrimFn = (arg as { [Symbol.toPrimitive]: unknown })[Symbol.toPrimitive]; if (toPrimFn && typeof toPrimFn === 'function') { prim = ReflectApply(toPrimFn, arg, ['number']); } } // The AO ToBigInt throws on numbers because it does not allow implicit // conversion between number and bigint (unlike the bigint constructor). if (typeof prim === 'number') { throw new TypeError('cannot convert number to bigint'); } if (typeof prim === 'bigint') { // JSBI doesn't know anything about the bigint type, and intentionally // assumes it doesn't exist. Passing one to the BigInt function will throw // an error. return JSBI.BigInt(prim.toString(10)); } // JSBI will properly coerce types into a BigInt the same as the native BigInt // constructor will, with the exception of native bigint which is handled // above. // As of 2023-04-07, the only runtime type that neither of those can handle is // 'symbol', and both native bigint and the JSBI.BigInt function will throw an // error if they are given a Symbol. return JSBI.BigInt(prim as string | boolean | object); } // Note: This method returns values with bogus nanoseconds based on the previous iteration's // milliseconds. That way there is a guarantee that the full nanoseconds are always going to be // increasing at least and that the microsecond and nanosecond fields are likely to be non-zero. export const SystemUTCEpochNanoSeconds: () => JSBI = (() => { let ns = JSBI.BigInt(Date.now() % 1e6); return () => { const ms = JSBI.BigInt(Date.now()); const result = JSBI.add(JSBI.multiply(ms, MILLION), ns); ns = JSBI.remainder(ms, MILLION); if (JSBI.greaterThan(result, NS_MAX)) return NS_MAX; if (JSBI.lessThan(result, NS_MIN)) return NS_MIN; return result; }; })(); export function DefaultTimeZone() { return new IntlDateTimeFormat().resolvedOptions().timeZone; } export function ComparisonResult(value: number) { return value < 0 ? -1 : value > 0 ? 1 : (value as 0); } export function GetOptionsObject(options: T) { if (options === undefined) return ObjectCreate(null) as NonNullable; if (IsObject(options) && options !== null) return options; throw new TypeError(`Options parameter must be an object, not ${options === null ? 'null' : `${typeof options}`}`); } export function CreateOnePropObject(propName: K, propValue: V): { [k in K]: V } { const o = ObjectCreate(null); o[propName] = propValue; return o; } function CopyOptions(options: T | undefined) { const optionsCopy = ObjectCreate(null) as T; CopyDataProperties(optionsCopy, GetOptionsObject(options), []); return optionsCopy; } type StringlyTypedKeys = Exclude; function GetOption

, O extends Partial>>( options: O, property: P, allowedValues: ReadonlyArray, fallback: undefined ): O[P]; function GetOption< P extends StringlyTypedKeys, O extends Partial>, Fallback extends Required[P] | undefined >( options: O, property: P, allowedValues: ReadonlyArray, fallback: Fallback ): Fallback extends undefined ? O[P] | undefined : Required[P]; function GetOption< P extends StringlyTypedKeys, O extends Partial>, Fallback extends Required[P] | undefined >( options: O, property: P, allowedValues: ReadonlyArray, fallback: O[P] ): Fallback extends undefined ? O[P] | undefined : Required[P] { let value = options[property]; if (value !== undefined) { value = ToString(value) as O[P]; if (!allowedValues.includes(value)) { throw new RangeError(`${property} must be one of ${allowedValues.join(', ')}, not ${value}`); } return value; } return fallback; } export function IsBuiltinCalendar(id: string): id is BuiltinCalendarId { return BUILTIN_CALENDAR_IDS.includes(ASCIILowercase(id)); } export function ASCIILowercase(str: T): T { // The spec defines this operation distinct from String.prototype.lowercase, // so we'll follow the spec here. Note that nasty security issues that can // happen for some use cases if you're comparing case-modified non-ASCII // values. For example, Turkish's "I" character was the source of a security // issue involving "file://" URLs. See // https://haacked.com/archive/2012/07/05/turkish-i-problem-and-why-you-should-care.aspx/. return str.replace(/[A-Z]/g, (l) => { const code = l.charCodeAt(0); return String.fromCharCode(code + 0x20); }) as T; } const OFFSET = new RegExp(`^${PARSE.offset.source}$`); function bisect( getState: (epochNs: JSBI) => number, leftParam: JSBI, rightParam: JSBI, lstateParam: number = getState(leftParam), rstateParam: number = getState(rightParam) ) { // This doesn't make much sense - why do these get converted unnecessarily? let left = JSBI.BigInt(leftParam); let right = JSBI.BigInt(rightParam); let lstate = lstateParam; let rstate = rstateParam; while (JSBI.greaterThan(JSBI.subtract(right, left), ONE)) { const middle = JSBI.divide(JSBI.add(left, right), JSBI.BigInt(2)); const mstate = getState(middle); if (mstate === lstate) { left = middle; lstate = mstate; } else if (mstate === rstate) { right = middle; rstate = mstate; } else { throw new Error(`invalid state in bisection ${lstate} - ${mstate} - ${rstate}`); } } return right; } const nsPerTimeUnit = { hour: 3600e9, minute: 60e9, second: 1e9, millisecond: 1e6, microsecond: 1e3, nanosecond: 1 };