@js-temporal/polyfill
Version:
Polyfill for Temporal (https://github.com/tc39/proposal-temporal), an ECMA TC39 Stage 3 proposal
1,389 lines (1,287 loc) • 185 kB
text/typescript
import { DEBUG, ENABLE_ASSERTS } from './debug';
import JSBI from 'jsbi';
import type { Temporal } from '..';
import { assert, assertNotReached } from './assert';
import { abs, compare, DAY_NANOS_JSBI, divmod, ensureJSBI, isEven, MILLION, ONE, TWO, ZERO } from './bigintmath';
import type { CalendarImpl } from './calendar';
import type {
AnyTemporalLikeType,
UnitSmallerThanOrEqualTo,
InstantParams,
PlainMonthDayParams,
ZonedDateTimeParams,
PlainDateParams,
PlainTimeParams,
DurationParams,
PlainDateTimeParams,
PlainYearMonthParams,
BuiltinCalendarId,
Keys,
AnyTemporalKey,
FieldKey,
InternalDuration,
ISODateTime,
ISODate,
TimeRecord,
ISODateToFieldsType,
DateDuration,
CalendarFieldsRecord,
MonthDayFromFieldsObject,
Overflow,
Resolve,
AnySlottedType
} from './internaltypes';
import { GetIntrinsic } from './intrinsicclass';
import { ApplyUnsignedRoundingMode, FMAPowerOf10, GetUnsignedRoundingMode, TruncatingDivModByPowerOf10 } from './math';
import { TimeDuration } from './timeduration';
import {
CreateSlots,
GetSlot,
HasSlot,
SetSlot,
EPOCHNANOSECONDS,
ISO_DATE,
ISO_DATE_TIME,
TIME,
DATE_BRAND,
YEAR_MONTH_BRAND,
MONTH_DAY_BRAND,
TIME_ZONE,
CALENDAR,
YEARS,
MONTHS,
WEEKS,
DAYS,
HOURS,
MINUTES,
SECONDS,
MILLISECONDS,
MICROSECONDS,
NANOSECONDS
} from './slots';
const DAY_MS = 86400_000;
export const DAY_NANOS = DAY_MS * 1e6;
const MINUTE_NANOS = 60e9;
// Instant range is 100 million days (inclusive) before or after epoch.
const MS_MAX = DAY_MS * 1e8;
const NS_MAX = epochMsToNs(MS_MAX);
const NS_MIN = JSBI.unaryMinus(NS_MAX);
// PlainDateTime range is 24 hours wider (exclusive) than the Instant range on
// both ends, to allow for valid Instant=>PlainDateTime conversion for all
// built-in time zones (whose offsets must have a magnitude less than 24 hours).
const DATETIME_NS_MIN = JSBI.add(JSBI.subtract(NS_MIN, DAY_NANOS_JSBI), ONE);
const DATETIME_NS_MAX = JSBI.subtract(JSBI.add(NS_MAX, DAY_NANOS_JSBI), ONE);
// The pattern of leap years in the ISO 8601 calendar repeats every 400 years.
// The constant below is the number of nanoseconds in 400 years. It is used to
// avoid overflows when dealing with values at the edge legacy Date's range.
const MS_IN_400_YEAR_CYCLE = (400 * 365 + 97) * DAY_MS;
const YEAR_MIN = -271821;
const YEAR_MAX = 275760;
const BEFORE_FIRST_DST = Date.UTC(1847, 0, 1); // 1847-01-01T00:00:00Z
const BUILTIN_CALENDAR_IDS = [
'iso8601',
'hebrew',
'islamic',
'islamic-umalqura',
'islamic-tbla',
'islamic-civil',
'islamic-rgsa',
'islamicc',
'persian',
'ethiopic',
'ethioaa',
'ethiopic-amete-alem',
'coptic',
'chinese',
'dangi',
'roc',
'indian',
'buddhist',
'japanese',
'gregory'
];
const ICU_LEGACY_TIME_ZONE_IDS = new Set([
'ACT',
'AET',
'AGT',
'ART',
'AST',
'BET',
'BST',
'CAT',
'CNT',
'CST',
'CTT',
'EAT',
'ECT',
'IET',
'IST',
'JST',
'MIT',
'NET',
'NST',
'PLT',
'PNT',
'PRT',
'PST',
'SST',
'VST'
]);
/* 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 */
/**
* 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 (ENABLE_ASSERTS) {
if (arg == null) {
throw new Error('Expected arg to be set.');
}
}
}
/** Similar to assertExists, but returns the argument. */
function castExists<A>(arg: A): NonNullable<A> {
assertExists(arg);
return arg;
}
// 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 Number(value);
}
function IsIntegralNumber(argument: unknown) {
if (typeof argument !== 'number' || Number.isNaN(argument) || argument === Infinity || argument === -Infinity) {
return false;
}
const absValue = Math.abs(argument);
return Math.floor(absValue) === absValue;
}
export function ToString(value: unknown): string {
if (typeof value === 'symbol') {
throw new TypeError('Cannot convert a Symbol value to a String');
}
return String(value);
}
export function ToIntegerWithTruncation(value: unknown): number {
const number = ToNumber(value);
if (number === 0) return 0;
if (Number.isNaN(number) || number === Infinity || number === -Infinity) {
throw new RangeError('invalid number value');
}
const integer = Math.trunc(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 (Number.isNaN(number)) throw new RangeError('not a number');
if (number === Infinity || number === -Infinity) 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 ToZeroPaddedDecimalString(n: number, minLength: number) {
if (DEBUG) {
if (!IsIntegralNumber(n) || n < 0) {
throw new RangeError('Assertion failed: `${n}` must be a non-negative integer');
}
}
const s = String(n);
return s.padStart(minLength, '0');
}
// This convenience function isn't in the spec, but is useful in the polyfill
// for DRY and better error messages.
export function RequireString(value: unknown): string {
if (typeof value !== 'string') {
// Use String() to ensure that Symbols won't throw
throw new TypeError(`expected a string, not ${String(value)}`);
}
return value;
}
function ToSyntacticallyValidMonthCode(valueParam: unknown) {
const value = RequireString(ToPrimitive(valueParam, String));
if (
value.length < 3 ||
value.length > 4 ||
value[0] !== 'M' ||
'0123456789'.indexOf(value[1]) === -1 ||
'0123456789'.indexOf(value[2]) === -1 ||
(value[1] + value[2] === '00' && value[3] !== 'L') ||
(value[3] !== 'L' && value[3] !== undefined)
) {
throw new RangeError(`bad month code ${value}; must match M01-M99 or M00L-M99L`);
}
return value;
}
function ToOffsetString(valueParam: unknown) {
const value = RequireString(ToPrimitive(valueParam, String));
ParseDateTimeUTCOffset(value);
return value;
}
// Limited implementation of ToPrimitive that only handles the string case,
// because that's all that's used in this polyfill.
function ToPrimitive(value: unknown, preferredType: typeof String): string | number {
assertExists(preferredType === String);
if (IsObject(value)) {
const result = value?.toString();
if (typeof result === 'string' || typeof result === 'number') return result;
throw new TypeError('Cannot convert object to primitive value');
}
return value;
}
const CALENDAR_FIELD_KEYS: readonly FieldKey[] = [
'era',
'eraYear',
'year',
'month',
'monthCode',
'day',
'hour',
'minute',
'second',
'millisecond',
'microsecond',
'nanosecond',
'offset',
'timeZone'
] as const;
type BuiltinCastFunction = (v: unknown) => string | number;
const BUILTIN_CASTS: Partial<Record<FieldKey, BuiltinCastFunction>> = {
era: ToString,
eraYear: ToIntegerWithTruncation,
year: ToIntegerWithTruncation,
month: ToPositiveIntegerWithTruncation,
monthCode: ToSyntacticallyValidMonthCode,
day: ToPositiveIntegerWithTruncation,
hour: ToIntegerWithTruncation,
minute: ToIntegerWithTruncation,
second: ToIntegerWithTruncation,
millisecond: ToIntegerWithTruncation,
microsecond: ToIntegerWithTruncation,
nanosecond: ToIntegerWithTruncation,
offset: ToOffsetString,
timeZone: ToTemporalTimeZoneIdentifier
};
const BUILTIN_DEFAULTS: Partial<Record<FieldKey, number>> = {
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
microsecond: 0,
nanosecond: 0
};
// each item is [plural, singular, category, (length in ns)]
const TEMPORAL_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 = Object.fromEntries(TEMPORAL_UNITS.map((e) => [e[0], e[1]] as const));
const PLURAL_FOR = Object.fromEntries(TEMPORAL_UNITS.map(([p, s]) => [s, p] as const));
const UNITS_DESCENDING = TEMPORAL_UNITS.map(([, s]) => s);
type TimeUnitOrDay = Temporal.TimeUnit | 'day';
const NS_PER_TIME_UNIT = {
day: DAY_NANOS,
hour: 3600e9,
minute: 60e9,
second: 1e9,
millisecond: 1e6,
microsecond: 1e3,
nanosecond: 1
};
const DURATION_FIELDS = [
'days',
'hours',
'microseconds',
'milliseconds',
'minutes',
'months',
'nanoseconds',
'seconds',
'weeks',
'years'
] as const;
import * as PARSE from './regex';
// Save the original Intl.DateTimeFormat, it will likely be overwritten with the
// one from this polyfill. Caching the formatter below may be reentrant, so we
// need to use the original one
const OriginalIntlDateTimeFormat = Intl.DateTimeFormat;
const IntlDateTimeFormatEnUsCache = new Map<string, Intl.DateTimeFormat>();
function getIntlDateTimeFormatEnUsForTimeZone(timeZoneIdentifier: string) {
const lowercaseIdentifier = ASCIILowercase(timeZoneIdentifier);
let instance = IntlDateTimeFormatEnUsCache.get(lowercaseIdentifier);
if (instance === undefined) {
instance = new OriginalIntlDateTimeFormat('en-us', {
timeZone: lowercaseIdentifier,
hour12: false,
era: 'short',
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
});
IntlDateTimeFormatEnUsCache.set(lowercaseIdentifier, instance);
}
return instance;
}
export function ToObject<T>(value: T): T extends Record<string, unknown> ? T : Record<PropertyKey, unknown> {
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 PropertyKey, T extends Record<K, unknown>>(
target: T,
source: T | undefined,
excludedKeys: K[],
excludedValues?: unknown[]
) {
if (typeof source === 'undefined' || source === null) return;
const keys = Reflect.ownKeys(source) as (keyof T)[];
for (let index = 0; index < keys.length; index++) {
const nextKey = keys[index];
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 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, TIME);
}
export function IsTemporalDateTime(item: unknown): item is Temporal.PlainDateTime {
return HasSlot(item, ISO_DATE_TIME);
}
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 CheckReceiver<T extends AnySlottedType>(
item: unknown,
test: (item: unknown) => item is T
): asserts item is T {
if (!test(item)) throw new TypeError('invalid receiver: method called with the wrong type of this-object');
}
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 FormatCalendarAnnotation(id: BuiltinCalendarId, showCalendar: Temporal.ShowCalendarOption['calendarName']) {
if (showCalendar === 'never') return '';
if (showCalendar === 'auto' && id === 'iso8601') return '';
const flag = showCalendar === 'critical' ? '!' : '';
return `[${flag}u-ca=${id}]`;
}
// Not a separate abstract operation in the spec, because it only occurs in one
// place: ParseISODateTime. In the code it's more convenient to split up
// ParseISODateTime for the YYYY-MM, MM-DD, and THH:MM:SS parse goals, so it's
// repeated four times.
function processAnnotations(annotations: string) {
let calendar;
let calendarWasCritical = false;
// Avoid the user code minefield of matchAll.
let match;
PARSE.annotation.lastIndex = 0;
while ((match = PARSE.annotation.exec(annotations))) {
const { 1: critical, 2: key, 3: value } = match;
if (key === 'u-ca') {
if (calendar === undefined) {
calendar = value;
calendarWasCritical = critical === '!';
} else if (critical === '!' || calendarWasCritical) {
throw new RangeError(`Invalid annotations in ${annotations}: more than one u-ca present with critical flag`);
}
} else if (critical === '!') {
throw new RangeError(`Unrecognized annotation: !${key}=${value}`);
}
}
return calendar;
}
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 RFC 9557 string: ${isoString}`);
const calendar = processAnnotations(match[16]);
let yearString = match[1];
if (yearString === '-000000') throw new RangeError(`invalid RFC 9557 string: ${isoString}`);
const year = +yearString;
const month = +(match[2] ?? match[4] ?? 1);
const day = +(match[3] ?? match[5] ?? 1);
const hasTime = match[6] !== undefined;
const hour = +(match[6] ?? 0);
const minute = +(match[7] ?? match[10] ?? 0);
let second = +(match[8] ?? match[11] ?? 0);
if (second === 60) second = 59;
const fraction = (match[9] ?? match[12] ?? '') + '000000000';
const millisecond = +fraction.slice(0, 3);
const microsecond = +fraction.slice(3, 6);
const nanosecond = +fraction.slice(6, 9);
let offset;
let z = false;
if (match[13]) {
offset = undefined;
z = true;
} else if (match[14]) {
offset = match[14];
}
const tzAnnotation = match[15];
RejectDateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond);
return {
year,
month,
day,
time: hasTime ? { hour, minute, second, millisecond, microsecond, nanosecond } : ('start-of-day' as const),
tzAnnotation,
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.tzAnnotation) 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, calendar;
if (match) {
calendar = processAnnotations(match[10]);
hour = +(match[1] ?? 0);
minute = +(match[2] ?? match[5] ?? 0);
second = +(match[3] ?? match[6] ?? 0);
if (second === 60) second = 59;
const fraction = (match[4] ?? match[7] ?? '') + '000000000';
millisecond = +fraction.slice(0, 3);
microsecond = +fraction.slice(3, 6);
nanosecond = +fraction.slice(6, 9);
if (match[8]) throw new RangeError('Z designator not supported for PlainTime');
} else {
let time, z;
({ time, z, calendar } = ParseISODateTime(isoString));
if (time === 'start-of-day') throw new RangeError(`time is missing in string: ${isoString}`);
if (z) throw new RangeError('Z designator not supported for PlainTime');
({ hour, minute, second, millisecond, microsecond, nanosecond } = time);
}
RejectTime(hour, minute, second, millisecond, microsecond, nanosecond);
// if it's a date-time string, OK
if (/[tT ][0-9][0-9]/.test(isoString)) {
return { hour, minute, second, millisecond, microsecond, nanosecond, calendar };
}
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, calendar };
}
}
throw new RangeError(`invalid RFC 9557 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) {
calendar = processAnnotations(match[3]);
let yearString = match[1];
if (yearString === '-000000') throw new RangeError(`invalid RFC 9557 string: ${isoString}`);
year = +yearString;
month = +match[2];
referenceISODay = 1;
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) {
calendar = processAnnotations(match[3]);
month = +match[1];
day = +match[2];
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 };
}
const TIMEZONE_IDENTIFIER = new RegExp(`^${PARSE.timeZoneID.source}$`, 'i');
const OFFSET_IDENTIFIER = new RegExp(`^${PARSE.offsetIdentifier.source}$`);
function throwBadTimeZoneStringError(timeZoneString: string): never {
// Offset identifiers only support minute precision, but offsets in ISO
// strings support nanosecond precision. If the identifier is invalid but
// it's a valid ISO offset, then it has sub-minute precision. Show a clearer
// error message in that case.
const msg = OFFSET.test(timeZoneString) ? 'Seconds not allowed in offset time zone' : 'Invalid time zone';
throw new RangeError(`${msg}: ${timeZoneString}`);
}
export function ParseTimeZoneIdentifier(
identifier: string
): { tzName: string; offsetMinutes?: undefined } | { tzName?: undefined; offsetMinutes: number } {
if (!TIMEZONE_IDENTIFIER.test(identifier)) {
throwBadTimeZoneStringError(identifier);
}
if (OFFSET_IDENTIFIER.test(identifier)) {
const offsetNanoseconds = ParseDateTimeUTCOffset(identifier);
// The regex limits the input to minutes precision, so we know that the
// division below will result in an integer.
return { offsetMinutes: offsetNanoseconds / 60e9 };
}
return { tzName: identifier };
}
// This operation doesn't exist in the spec, but in the polyfill it's split from
// ParseTemporalTimeZoneString so that parsing can be tested separately from the
// logic of converting parsed values into a named or offset identifier.
// ts-prune-ignore-next TODO: remove if test/validStrings is converted to TS.
export function ParseTemporalTimeZoneStringRaw(timeZoneString: string): {
tzAnnotation: string;
offset: string | undefined;
z: boolean;
} {
if (TIMEZONE_IDENTIFIER.test(timeZoneString)) {
return { tzAnnotation: timeZoneString, offset: undefined, z: false };
}
try {
// Try parsing ISO string instead
const { tzAnnotation, offset, z } = ParseISODateTime(timeZoneString);
if (z || tzAnnotation || offset) {
return { tzAnnotation, offset, z };
}
} catch {
// fall through
}
throwBadTimeZoneStringError(timeZoneString);
}
function ParseTemporalTimeZoneString(stringIdent: string): ReturnType<typeof ParseTimeZoneIdentifier> {
const { tzAnnotation, offset, z } = ParseTemporalTimeZoneStringRaw(stringIdent);
if (tzAnnotation) return ParseTimeZoneIdentifier(tzAnnotation);
if (z) return ParseTimeZoneIdentifier('UTC');
if (offset) return ParseTimeZoneIdentifier(offset);
/* c8 ignore next */ assertNotReached();
}
// ts-prune-ignore-next TODO: remove if test/validStrings is converted to TS.
export function ParseTemporalDurationStringRaw(isoString: string) {
const match = PARSE.duration.exec(isoString);
if (!match) throw new RangeError(`invalid duration: ${isoString}`);
if (match.every((part, i) => i < 2 || part === undefined)) {
throw new RangeError(`invalid duration: ${isoString}`);
}
const sign = match[1] === '-' ? -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 = ToIntegerWithTruncation((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 = ToIntegerWithTruncation((fMinutes + '000000000').slice(0, 9)) * 60 * sign;
} else {
seconds = secondsStr === undefined ? 0 : ToIntegerWithTruncation(secondsStr) * sign;
if (fSeconds !== undefined) {
excessNanoseconds = ToIntegerWithTruncation((fSeconds + '000000000').slice(0, 9)) * sign;
}
}
}
const nanoseconds = excessNanoseconds % 1000;
const microseconds = Math.trunc(excessNanoseconds / 1000) % 1000;
const milliseconds = Math.trunc(excessNanoseconds / 1e6) % 1000;
seconds += Math.trunc(excessNanoseconds / 1e9) % 60;
minutes += Math.trunc(excessNanoseconds / 60e9);
RejectDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds);
return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds };
}
function ParseTemporalDurationString(isoString: string) {
const { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } =
ParseTemporalDurationStringRaw(isoString);
const TemporalDuration = GetIntrinsic('%Temporal.Duration%');
return new TemporalDuration(
years,
months,
weeks,
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds
);
}
export function RegulateISODate(yearParam: number, monthParam: number, dayParam: number, overflow: 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: 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 = ConstrainToRange(hour, 0, 23);
minute = ConstrainToRange(minute, 0, 59);
second = ConstrainToRange(second, 0, 59);
millisecond = ConstrainToRange(millisecond, 0, 999);
microsecond = ConstrainToRange(microsecond, 0, 999);
nanosecond = ConstrainToRange(nanosecond, 0, 999);
break;
}
return { hour, minute, second, millisecond, microsecond, nanosecond };
}
export 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 (let index = 0; index < DURATION_FIELDS.length; index++) {
const property = DURATION_FIELDS[index];
const value = temporalDurationLike[property];
if (value !== undefined) {
any = true;
result[property] = ToIntegerIfIntegral(value);
}
}
if (!any) {
throw new TypeError('invalid duration-like');
}
return result;
}
export function AdjustDateDurationRecord(
{ years, months, weeks, days }: DateDuration,
newDays: number,
newWeeks?: number,
newMonths?: number
) {
return {
years,
months: newMonths ?? months,
weeks: newWeeks ?? weeks,
days: newDays ?? days
};
}
export function ZeroDateDuration() {
return { years: 0, months: 0, weeks: 0, days: 0 };
}
export function CombineISODateAndTimeRecord(isoDate: ISODate, time: TimeRecord) {
return { isoDate, time };
}
export function MidnightTimeRecord() {
return { deltaDays: 0, hour: 0, minute: 0, second: 0, millisecond: 0, microsecond: 0, nanosecond: 0 };
}
export function NoonTimeRecord() {
return { deltaDays: 0, hour: 12, minute: 0, second: 0, millisecond: 0, microsecond: 0, nanosecond: 0 };
}
export function GetTemporalOverflowOption(options: Temporal.AssignmentOptions) {
return GetOption(options, 'overflow', ['constrain', 'reject'], 'constrain');
}
export function GetTemporalDisambiguationOption(options: Temporal.ToInstantOptions) {
return GetOption(options, 'disambiguation', ['compatible', 'earlier', 'later', 'reject'], 'compatible');
}
export function GetRoundingModeOption(
options: { roundingMode?: Temporal.RoundingMode },
fallback: Temporal.RoundingMode
) {
return GetOption(
options,
'roundingMode',
['ceil', 'floor', 'expand', 'trunc', 'halfCeil', 'halfFloor', 'halfExpand', 'halfTrunc', 'halfEven'],
fallback
);
}
function NegateRoundingMode(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 GetTemporalOffsetOption(
options: Temporal.OffsetDisambiguationOptions,
fallback: Required<Temporal.OffsetDisambiguationOptions>['offset']
) {
return GetOption(options, 'offset', ['prefer', 'use', 'ignore', 'reject'], fallback);
}
export function GetTemporalShowCalendarNameOption(options: Temporal.ShowCalendarOption) {
return GetOption(options, 'calendarName', ['auto', 'always', 'never', 'critical'], 'auto');
}
export function GetTemporalShowTimeZoneNameOption(options: Temporal.ZonedDateTimeToStringOptions) {
return GetOption(options, 'timeZoneName', ['auto', 'never', 'critical'], 'auto');
}
export function GetTemporalShowOffsetOption(options: Temporal.ZonedDateTimeToStringOptions) {
return GetOption(options, 'offset', ['auto', 'never'], 'auto');
}
export function GetDirectionOption(options: { direction?: 'next' | 'previous' }) {
return GetOption(options, 'direction', ['next', 'previous'], REQUIRED);
}
export function GetTemporalRoundingIncrementOption(options: { roundingIncrement?: number }) {
let increment = options.roundingIncrement;
if (increment === undefined) return 1;
const integerIncrement = ToIntegerWithTruncation(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 GetTemporalFractionalSecondDigitsOption(
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 = Math.floor(digitsValue);
if (!Number.isFinite(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'>;
}
interface SecondsStringPrecisionRecord {
precision: Temporal.ToStringPrecisionOptions['fractionalSecondDigits'] | 'minute';
unit: UnitSmallerThanOrEqualTo<'minute'>;
increment: number;
}
export function ToSecondsStringPrecisionRecord(
smallestUnit: Temporal.ToStringPrecisionOptions['smallestUnit'],
precision: Temporal.ToStringPrecisionOptions['fractionalSecondDigits']
): SecondsStringPrecisionRecord {
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 GetTemporalUnitValuedOption<
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 GetTemporalUnitValuedOption<
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 GetTemporalUnitValuedOption<
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 {
let allowedSingular: Array<Temporal.DateTimeUnit | 'auto'> = [];
for (let index = 0; index < TEMPORAL_UNITS.length; index++) {
const unitInfo = TEMPORAL_UNITS[index];
const singular = unitInfo[1];
const category = unitInfo[2];
if (unitGroup === 'datetime' || unitGroup === category) {
allowedSingular.push(singular);
}
}
allowedSingular = allowedSingular.concat(extraValues);
let defaultVal: typeof REQUIRED | Temporal.DateTimeUnit | 'auto' | undefined = requiredOrDefault;
if (defaultVal === REQUIRED) {
defaultVal = undefined;
} else if (defaultVal !== undefined) {
allowedSingular.push(defaultVal);
}
let allowedValues: Array<Temporal.DateTimeUnit | Temporal.PluralUnit<Temporal.DateTimeUnit> | 'auto'> = [];
allowedValues = allowedValues.concat(allowedSingular);
for (let index = 0; index < allowedSingular.length; index++) {
const singular = allowedSingular[index];
const plural = PLURAL_FOR[singular];
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
return (retval && retval in SINGULAR_FOR ? SINGULAR_FOR[retval] : retval) as R;
}
export function GetTemporalRelativeToOption(options: {
relativeTo?:
| Temporal.ZonedDateTime
| Temporal.PlainDateTime
| Temporal.ZonedDateTimeLike
| Temporal.PlainDateTimeLike
| string
| undefined;
}):
| { zonedRelativeTo?: Temporal.ZonedDateTime; plainRelativeTo?: never }
| { plainRelativeTo?: Temporal.PlainDate; zonedRelativeTo?: never } {
const relativeTo = options.relativeTo;
if (relativeTo === undefined) return {};
let offsetBehaviour: OffsetBehaviour = 'option';
let matchMinutes = false;
let isoDate, time, calendar, timeZone, offset;
if (IsObject(relativeTo)) {
if (IsTemporalZonedDateTime(relativeTo)) {
return { zonedRelativeTo: relativeTo };
}
if (IsTemporalDate(relativeTo)) return { plainRelativeTo: relativeTo };
if (IsTemporalDateTime(relativeTo)) {
return {
plainRelativeTo: CreateTemporalDate(GetSlot(relativeTo, ISO_DATE_TIME).isoDate, GetSlot(relativeTo, CALENDAR))
};
}
calendar = GetTemporalCalendarIdentifierWithISODefault(relativeTo);
const fields = PrepareCalendarFields(
calendar,
relativeTo,
['year', 'month', 'monthCode', 'day'],
['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond', 'offset', 'timeZone'],
[]
);
({ isoDate, time } = InterpretTemporalDateTimeFields(calendar, fields, 'constrain'));
({ offset, timeZone } = fields);
if (offset === undefined) offsetBehaviour = 'wall';
} else {
let tzAnnotation, z, year, month, day;
({ year, month, day, time, calendar, tzAnnotation, offset, z } = ParseISODateTime(RequireString(relativeTo)));
if (tzAnnotation) {
timeZone = ToTemporalTimeZoneIdentifier(tzAnnotation);
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';
calendar = CanonicalizeCalendar(calendar);
isoDate = { year, month, day };
}
if (timeZone === undefined) {
return { plainRelativeTo: CreateTemporalDate(isoDate, calendar) };
}
const offsetNs = offsetBehaviour === 'option' ? ParseDateTimeUTCOffset(castExists(offset)) : 0;
const epochNanoseconds = InterpretISODateTimeOffset(
isoDate,
time,
offsetBehaviour,
offsetNs,
timeZone,
'compatible',
'reject',
matchMinutes
);
return { zonedRelativeTo: CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar) };
}
export function DefaultTemporalLargestUnit(duration: Temporal.Duration) {
if (GetSlot(duration, YEARS) !== 0) return 'year';
if (GetSlot(duration, MONTHS) !== 0) return 'month';
if (GetSlot(duration, WEEKS) !== 0) return 'week';
if (GetSlot(duration, DAYS) !== 0) return 'day';
if (GetSlot(duration, HOURS) !== 0) return 'hour';
if (GetSlot(duration, MINUTES) !== 0) return 'minute';
if (GetSlot(duration, SECONDS) !== 0) return 'second';
if (GetSlot(duration, MILLISECONDS) !== 0) return 'millisecond';
if (GetSlot(duration, MICROSECONDS) !== 0) return 'microsecond';
return 'nanosecond';
}
export function LargerOfTwoTemporalUnits<T1 extends Temporal.DateTimeUnit, T2 extends Temporal.DateTimeUnit>(
unit1: T1,
unit2: T2
) {
const i1 = UNITS_DESCENDING.indexOf(unit1);
const i2 = UNITS_DESCENDING.indexOf(unit2);
if (i1 > i2) {
return unit2;
}
return unit1;
}
export function IsCalendarUnit(unit: Temporal.DateTimeUnit): unit is Exclude<Temporal.DateUnit, 'day'> {
return unit === 'year' || unit === 'month' || unit === 'week';
}
export function TemporalUnitCategory(unit: Temporal.DateTimeUnit) {
if (IsCalendarUnit(unit) || unit === 'day') return 'date';
return 'time';
}
function calendarImplForID(calendar: BuiltinCalendarId) {
return GetIntrinsic('%calendarImpl%')(calendar);
}
export function calendarImplForObj(
temporalObj:
| Temporal.PlainDate
| Temporal.PlainDateTime
| Temporal.PlainMonthDay
| Temporal.PlainYearMonth
| Temporal.ZonedDateTime
) {
return GetIntrinsic('%calendarImpl%')(GetSlot(temporalObj, CALENDAR));
}
type ISODateToFieldsReturn<Type extends ISODateToFieldsType> = Resolve<{
year: Type extends 'date' | 'year-month' ? number : never;
monthCode: string;
day: Type extends 'date' | 'month-day' ? number : never;
}>;
export function ISODateToFields(calendar: BuiltinCalendarId, isoDate: ISODate): ISODateToFieldsReturn<'date'>;
export function ISODateToFields<T extends ISODateToFieldsType>(
calendar: BuiltinCalendarId,
isoDate: ISODate,
type: T
): ISODateToFieldsReturn<T>;
export function ISODateToFields(calendar: BuiltinCalendarId, isoDate: ISODate, type = 'date') {
const fields = Object.create(null);
const calendarImpl = calendarImplForID(calendar);
const calendarDate = calendarImpl.isoToDate(isoDate, { year: true, monthCode: true, day: true });
fields.monthCode = calendarDate.monthCode;
if (type === 'month-day' || type === 'date') {
fields.day = calendarDate.day;
}
if (type === 'year-month' || type === 'date') {
fields.year = calendarDate.year;
}
return fields;
}
type Prop<T, K> = T extends unknown ? (K extends keyof T ? T[K] : undefined) : 'ThisShouldNeverHappen';
type FieldObjectWithRequired<FieldKeys extends FieldKey> = 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 CalendarFieldsRecord that aren't in FieldKeys,
// which are optional properties and their value types explicitly include
// undefined.
{
-readonly [k in FieldKeys]: Exclude<Prop<CalendarFieldsRecord, k>, undefined>;
} & {
-readonly [k in Exclude<Keys<CalendarFieldsRecord>, FieldKeys>]?: Prop<CalendarFieldsRecord, k> | undefined;
}
>;
type PrepareCalendarFieldsReturn<
FieldKeys extends FieldKey,
RequiredFieldsOpt extends ReadonlyArray<FieldKey> | 'partial'
> = RequiredFieldsOpt extends 'partial' ? Partial<CalendarFieldsRecord> : FieldObjectWithRequired<FieldKeys>;
export function PrepareCalendarFields<
FieldKeys extends FieldKey,
RequiredFields extends ReadonlyArray<FieldKey> | 'partial'
>(
calendar: BuiltinCalendarId,
bag: Partial<Record<FieldKeys, unknown>>,
calendarFieldNames: Array<FieldKeys>,
nonCalendarFieldNames: Array<FieldKeys>,
requiredFields: RequiredFields
): PrepareCalendarFieldsReturn<FieldKeys, RequiredFields> {
const extraFieldNames = calendarImplForID(calendar).extraFields(calendarFieldNames) as FieldKeys[];
const fields = calendarFieldNames.concat(nonCalendarFieldNames, extraFieldNames);
const result: Partial<Record<AnyTemporalKey, unknown>> = Object.create(null);
let any = false;
fields.sort();
for (let index = 0; index < fields.length; index++) {
const property = fields[index];
const value = bag[property];
if (value !== undefined) {
any = true;
result[property] = castExists(BUILTIN_CASTS[property])(value);
} else if (requiredFields !== 'partial') {
if (requiredFields.includes(property)) {
throw new TypeError(`required property '${property}' missing or undefined`);
}
result[property] = BUILTIN_DEFAULTS[property];
}
}
if (requiredFields === 'partial' && !any) {
throw new TypeError('no supported properties found');
}
return result as unknown as PrepareCalendarFieldsReturn<FieldKeys, RequiredFields>;
}
type FieldCompleteness = 'complete' | 'partial';
export function ToTemporalTimeRecord(bag: Partial<Record<keyof TimeRecord, string | number>>): 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'
): 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'];
let any = false;
const result: Partial<TimeRecord> = Object.create(null);
for (let index = 0; index < fields.length; index++) {
const field = fields[index];
const value = bag[field];
if (value !== undefined) {
result[field] = ToIntegerWithTruncation(value);
any = true;
} else if (completeness === 'complete') {
result[field] = 0;
}
}
if (!any) throw new TypeError('invalid time-like');
return result;
}
export function ToTemporalDate(
item: PlainDateParams['from'][0],
options?: PlainDateParams['from'][1]
): Temporal.PlainDate {
if (IsObject(item)) {
if (IsTemporalDate(item)) {
GetTemporalOverflowOption(GetOptionsObject(options));
return CreateTemporalDate(GetSlot(item, ISO_DATE), GetSlot(item, CALENDAR));
}
if (IsTemporalZonedDateTime(item)) {
const isoDateTime = GetISODateTimeFor(GetSlot(item, TIME_ZONE), GetSlot(item, EPOCHNANOSECONDS));
GetTemporalOverflowOption(GetOptionsObject(options)); // validate and ignore
const isoDate = isoDateTime.isoDate;
return CreateTemporalDate(isoDate, GetSlot(item, CALENDAR));
}
if (IsTemporalDateTime(item)) {
GetTemporalOverflowOption(GetOptionsObject(options)); // validate and ignore
return CreateTemporalDate(GetSlot(item, ISO_DATE_TIME).isoDate, GetSlot(item, CALENDAR));
}
const calendar = GetTemporalCalendarIdentifierWithISODefault(item);
const fields = PrepareCalendarFields(calendar, item, ['year', 'month', 'monthCode', 'day'], [], []);
const overflow = GetTemporalOverflowOption(GetOptionsObject(options));
const isoDate = CalendarDateFromFields(calendar, fields, overflow);
return CreateTemporalDate(isoDate, calendar);
}
let { year, month, day, calendar, z } = ParseTemporalDateString(RequireString(item));
if (z) throw new RangeError('Z designator not supported for PlainDate');
if (!calendar) calendar = 'iso8601';
calendar = CanonicalizeCalendar(calendar);
uncheckedAssertNarrowedType<BuiltinCalendarId>(calendar, 'lowercased and canonicalized');
GetTemporalOverflowOption(GetOptionsObject(options)); // validate and ignore
return CreateTemporalDate({ year, month, day }, calendar);
}
export function InterpretTemporalDateTimeFields(
calendar: BuiltinC