6561 lines
232 KiB
TypeScript
Raw Normal View History

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<T = unknown>(
arg: unknown,
justification: string
): asserts arg is T extends typeof arg ? T : never {}
/* eslint-enable */
type ArrayElement<ArrayType> = ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
type ArrayWithNewKeys<T, Keys> = Array<ArrayElement<T> | 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<A>(arg: A): asserts arg is NonNullable<A> {
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<T> = Exclude<T, string>;
function GetMethod<T extends { [s in M]?: (...args: any[]) => 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<T>
>(obj: T, methodName: M): Stringless<T>[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<T, A extends readonly any[], R>(
target: (this: T, ...args: A) => R,
thisArgument: T,
argumentsList: Readonly<A>
): 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<T>(
value: T
): value is Exclude<T, string | null | undefined | number | bigint | symbol | boolean>;
export function IsObject(value: unknown): value is Record<string | number | symbol, unknown> {
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<AnyTemporalKey, BuiltinCastFunction>([
['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<string, Intl.DateTimeFormat>();
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<T>(value: T): T extends Record<string, unknown> ? 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<K extends string | symbol, T extends Record<K, unknown>>(
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<typeof DURATION_FIELDS[number], number | undefined> = {
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<Temporal.OffsetDisambiguationOptions>['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<Temporal.ToStringPrecisionOptions['fractionalSecondDigits'], 'auto'>;
}
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> | Temporal.DateTimeUnit;
largestUnit?: Temporal.PluralUnit<Temporal.DateTimeUnit> | Temporal.DateTimeUnit | 'auto';
unit?: Temporal.PluralUnit<Temporal.DateTimeUnit> | 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<D, typeof REQUIRED> | 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<D, typeof REQUIRED> | E
>(options: TemporalUnitOptionsBag, key: U, unitGroup: T, requiredOrDefault: D, extraValues: ReadonlyArray<E>): 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<D, typeof REQUIRED> | E
>(
options: TemporalUnitOptionsBag,
key: keyof typeof options,
unitGroup: T,
requiredOrDefault: D,
extraValues: ReadonlyArray<E> | never[] = []
): R {
const allowedSingular: Array<Temporal.DateTimeUnit | 'auto'> = [];
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<Temporal.DateTimeUnit | Temporal.PluralUnit<Temporal.DateTimeUnit> | 'auto'> = [
...allowedSingular
];
for (const singular of allowedSingular) {
const plural = PLURAL_FOR.get(singular as Parameters<typeof PLURAL_FOR.get>[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<Temporal.DateTimeUnit>)) {
// 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<Temporal.DateTimeUnit>)! 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<typeof fieldNames, 'timeZone' | 'offset'>;
(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<T1 extends Temporal.DateTimeUnit, T2 extends Temporal.DateTimeUnit>(
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<K extends AnyTemporalKey> =
// Conditional typing maps over all of the types given in AnyTemporalLikeType
// union
K extends unknown ? OwnerOf<K, AnyTemporalLikeType> : '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<K extends AnyTemporalKey, T> =
// Distribute the union before passing to Required
// Without distributing, this is
// Required<ZonedDateTimeLike | DurationLike> extends Record
// vs (with distribution)
// Required<ZonedDateTimeLike> extends Record<....> | Required<DurationLike> extends Record<....>
T extends unknown
? // All the keys in the Like-types are optional, so in order for them to
// 'extend Record<K,...>', 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<Record<K, ..>>
// will always be extended by any object (as all the keys are optional).
Required<T> extends Record<K, unknown>
? T
: // never is the 'identity' type for unions - nothing will be added or
// removed from the union.
never
: 'ThisShouldNeverHappen';
type Prop<T, K> = 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<T> =
// 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<OwnerT, FieldKeys extends AnyTemporalKey> = 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<Prop<OwnerT, k>, undefined>;
} & {
-readonly [k in Exclude<Keys<OwnerT>, FieldKeys>]?: Prop<OwnerT, k> | undefined;
}
>;
type PrepareTemporalFieldsReturn<
FieldKeys extends AnyTemporalKey,
RequiredFieldsOpt extends ReadonlyArray<FieldKeys> | FieldCompleteness,
OwnerT extends Owner<FieldKeys>
> = RequiredFieldsOpt extends 'partial' ? Partial<OwnerT> : FieldObjectFromOwners<OwnerT, FieldKeys>;
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<FieldKeys> | FieldCompleteness
>(
bag: Partial<Record<FieldKeys, unknown>>,
fields: Array<FieldKeys>,
requiredFields: RequiredFields,
{ emptySourceErrorMessage }: FieldPrepareOptions = { emptySourceErrorMessage: 'no supported properties found' }
): PrepareTemporalFieldsReturn<FieldKeys, RequiredFields, Owner<FieldKeys>> {
const result: Partial<Record<AnyTemporalKey, unknown>> = 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<FieldKeys, RequiredFields, Owner<FieldKeys>>;
}
interface TimeRecord {
hour?: number;
minute?: number;
second?: number;
microsecond?: number;
millisecond?: number;
nanosecond?: number;
}
export function ToTemporalTimeRecord(bag: Partial<Record<keyof TimeRecord, string | number>>): Required<TimeRecord>;
export function ToTemporalTimeRecord(
bag: Partial<Record<keyof TimeRecord, string | number | undefined>>,
completeness: 'partial'
): Partial<TimeRecord>;
export function ToTemporalTimeRecord(
bag: Partial<Record<keyof TimeRecord, string | number>>,
completeness: 'complete'
): Required<TimeRecord>;
export function ToTemporalTimeRecord(
bag: Partial<Record<keyof TimeRecord, string | number | undefined>>,
completeness: FieldCompleteness = 'complete'
): Partial<TimeRecord> {
// 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<TimeRecord> = {};
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<Temporal.PlainDateTimeLike> & Parameters<typeof CalendarDateFromFields>[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<PlainTimeParams['from'][1]>['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<Temporal.ToInstantOptions['disambiguation']>,
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<Temporal.ToInstantOptions['disambiguation']>;
let offsetOpt: NonNullable<Temporal.OffsetDisambiguationOptions['offset']>;
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<K extends AnyTemporalKey>(calendar: CalendarSlot, fieldNamesParam: ReadonlyArray<K>) {
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<Base extends Record<string, unknown>, ToAdd extends Record<string, unknown>>(
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<Omit<Temporal.CalendarProtocol, 'toString' | 'toJSON'>>;
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<CalendarProtocolParams['dateFromFields'][1]>,
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<Temporal.TimeZoneProtocol, 'getOffsetNanosecondsFor' | 'getPossibleInstantsFor'>
>;
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<Temporal.ToInstantOptions['disambiguation']>
) {
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<Temporal.ToInstantOptions['disambiguation']>
) {
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<typeof ToSecondsStringPrecisionRecord>['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<typeof ToSecondsStringPrecisionRecord>['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<typeof ToSecondsStringPrecisionRecord>['unit'];
increment: number;
roundingMode: ReturnType<typeof ToTemporalRoundingMode>;
}
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<typeof ToSecondsStringPrecisionRecord>['precision'],
showCalendar: ReturnType<typeof ToCalendarNameOption> = '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<typeof ToSecondsStringPrecisionRecord>['precision'],
showCalendar: ReturnType<typeof ToCalendarNameOption> = 'auto',
showTimeZone: ReturnType<typeof ToTimeZoneNameOption> = 'auto',
showOffset: ReturnType<typeof ToShowOffsetOption> = '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<typeof ToRelativeTemporalObject>) {
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<Temporal.DateTimeUnit>
);
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<typeof ToRelativeTemporalObject> = 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<typeof ToRelativeTemporalObject> = 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<typeof ToRelativeTemporalObject>
): {
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<typeof largestUnit>;
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<typeof ToRelativeTemporalObject>
): {
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<typeof ToRelativeTemporalObject>,
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<Allowed extends Temporal.DateTimeUnit>(
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<Temporal.DateTimeUnit> | 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<DateTimeUnit> | 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<Temporal.DateTimeUnit>
) {
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<T extends Temporal.DateTimeUnit>(
op: DifferenceOperation,
options: Temporal.DifferenceOptions<T>,
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<Temporal.DateTimeUnit>)[]);
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<typeof ToRelativeTemporalObject>
) {
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<Temporal.DateUnit>;
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<Temporal.DateTimeUnit>
));
}
}
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<typeof fields & { day: number }>(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<ReturnType<typeof ToRelativeTemporalObject>>,
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<typeof ToRelativeTemporalObject>
) {
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<typeof ToRelativeTemporalObject> = 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<typeof unit>;
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<T>(options: T) {
if (options === undefined) return ObjectCreate(null) as NonNullable<T>;
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<K extends string, V>(propName: K, propValue: V): { [k in K]: V } {
const o = ObjectCreate(null);
o[propName] = propValue;
return o;
}
function CopyOptions<T extends { [s in K]?: unknown }, K extends string & keyof T>(options: T | undefined) {
const optionsCopy = ObjectCreate(null) as T;
CopyDataProperties(optionsCopy, GetOptionsObject(options), []);
return optionsCopy;
}
type StringlyTypedKeys<T> = Exclude<keyof T, symbol | number>;
function GetOption<P extends StringlyTypedKeys<O>, O extends Partial<Record<P, unknown>>>(
options: O,
property: P,
allowedValues: ReadonlyArray<O[P]>,
fallback: undefined
): O[P];
function GetOption<
P extends StringlyTypedKeys<O>,
O extends Partial<Record<P, unknown>>,
Fallback extends Required<O>[P] | undefined
>(
options: O,
property: P,
allowedValues: ReadonlyArray<O[P]>,
fallback: Fallback
): Fallback extends undefined ? O[P] | undefined : Required<O>[P];
function GetOption<
P extends StringlyTypedKeys<O>,
O extends Partial<Record<P, unknown>>,
Fallback extends Required<O>[P] | undefined
>(
options: O,
property: P,
allowedValues: ReadonlyArray<O[P]>,
fallback: O[P]
): Fallback extends undefined ? O[P] | undefined : Required<O>[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<T extends string>(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
};