mirror of
https://github.com/bendtherules/ask262.git
synced 2026-08-18 13:21:55 +00:00
301 lines
14 KiB
TypeScript
301 lines
14 KiB
TypeScript
import type { ISODateRecord } from '../../intrinsics/Temporal/PlainDate.mts';
|
||
import type { ISODateTimeRecord } from '../../intrinsics/Temporal/PlainDateTime.mts';
|
||
import { ParseTemporalTimeZoneString, ParseTimeZoneIdentifier } from '../../parser/TemporalParser.mts';
|
||
import {
|
||
HourFromTime, MinFromTime, SecFromTime, msFromTime,
|
||
} from '../date-objects.mts';
|
||
import { isTemporalZonedDateTimeObject } from '../../intrinsics/Temporal/ZonedDateTime.mts';
|
||
import { R } from '../spec-types.mjs';
|
||
import { abs } from '../math.mts';
|
||
import {
|
||
IsOffsetTimeZoneIdentifier, GetNamedTimeZoneEpochNanoseconds, GetUTCEpochNanoseconds, RoundingMode,
|
||
AvailableNamedTimeZoneIdentifiers,
|
||
GetNamedTimeZoneOffsetNanoseconds,
|
||
} from './addition.mts';
|
||
import type { TimeZoneIdentifier } from './addition.mts';
|
||
import {
|
||
RoundNumberToIncrement, EpochTimeToDate, EpochTimeToEpochYear, EpochTimeToMonthInYear, CheckISODaysRange,
|
||
FormatTimeString,
|
||
} from './temporal.mts';
|
||
import {
|
||
Assert, JSStringValue, ObjectValue, Value, type PlainCompletion, Q,
|
||
Throw,
|
||
X,
|
||
AddDaysToISODate,
|
||
AddTime,
|
||
BalanceISODateTime,
|
||
CombineISODateAndTimeRecord,
|
||
CreateISODateRecord,
|
||
CreateTimeRecord,
|
||
IsValidEpochNanoseconds,
|
||
MidnightTimeRecord,
|
||
nsPerDay,
|
||
TimeDurationFromComponents,
|
||
} from '#self';
|
||
|
||
// https://tc39.es/proposal-temporal/#sec-temporal-getavailablenamedtimezoneidentifier
|
||
export function GetAvailableNamedTimeZoneIdentifier(timeZoneIdentifier: TimeZoneIdentifier): TimeZoneIdentifierRecord | undefined {
|
||
for (const record of AvailableNamedTimeZoneIdentifiers()) {
|
||
if (record.Identifier.toLowerCase() === timeZoneIdentifier.toLowerCase()) {
|
||
return record;
|
||
}
|
||
}
|
||
return undefined;
|
||
}
|
||
|
||
/** https://tc39.es/ecma262/#sec-time-zone-identifier-record */
|
||
export interface TimeZoneIdentifierRecord {
|
||
readonly Identifier: TimeZoneIdentifier;
|
||
readonly PrimaryIdentifier: TimeZoneIdentifier;
|
||
}
|
||
|
||
// https://tc39.es/proposal-temporal/#sec-temporal-getisopartsfromepoch
|
||
export function GetISOPartsFromEpoch(epochNanoseconds: number): ISODateTimeRecord {
|
||
Assert(IsValidEpochNanoseconds(epochNanoseconds));
|
||
const remainderNs = epochNanoseconds % 1e6;
|
||
const epochMilliseconds = (epochNanoseconds - remainderNs) / 1e6;
|
||
const year = EpochTimeToEpochYear(epochMilliseconds);
|
||
const month = EpochTimeToMonthInYear(epochMilliseconds) + 1;
|
||
const day = EpochTimeToDate(epochMilliseconds);
|
||
const hour = R(HourFromTime(Value(epochMilliseconds)));
|
||
const minute = R(MinFromTime(Value(epochMilliseconds)));
|
||
const second = R(SecFromTime(Value(epochMilliseconds)));
|
||
const millisecond = R(msFromTime(Value(epochMilliseconds)));
|
||
const microsecond = Math.floor(remainderNs / 1000);
|
||
Assert(microsecond < 1000);
|
||
const nanosecond = remainderNs % 1000;
|
||
const isoDate = CreateISODateRecord(year, month, day);
|
||
const time = CreateTimeRecord(hour, minute, second, millisecond, microsecond, nanosecond);
|
||
return CombineISODateAndTimeRecord(isoDate, time);
|
||
}
|
||
|
||
// https://tc39.es/proposal-temporal/#sec-temporal-getnamedtimezonenexttransition
|
||
export function GetNamedTimeZoneNextTransition(timeZoneIdentifier: TimeZoneIdentifier, _epochNanoseconds: bigint): bigint | null {
|
||
Assert(timeZoneIdentifier === 'UTC');
|
||
return null;
|
||
}
|
||
|
||
// https://tc39.es/proposal-temporal/#sec-temporal-getnamedtimezoneprevioustransition
|
||
export function GetNamedTimeZonePreviousTransition(timeZoneIdentifier: TimeZoneIdentifier, _epochNanoseconds: bigint): bigint | null {
|
||
Assert(timeZoneIdentifier === 'UTC');
|
||
return null;
|
||
}
|
||
|
||
// https://tc39.es/proposal-temporal/#sec-temporal-formatoffsettimezoneidentifier
|
||
export function FormatOffsetTimeZoneIdentifier(offsetMinutes: number, style: 'separated' | 'unseparated' = 'separated'): TimeZoneIdentifier {
|
||
const sign = offsetMinutes >= 0 ? '+' : '-';
|
||
const absoluteMinutes = Math.abs(offsetMinutes);
|
||
const hour = Math.floor(absoluteMinutes / 60);
|
||
const minute = absoluteMinutes % 60;
|
||
const timeString = FormatTimeString(hour, minute, 0, 0, 'minute', style);
|
||
return sign + timeString as TimeZoneIdentifier;
|
||
}
|
||
|
||
// https://tc39.es/proposal-temporal/#sec-temporal-formatutcoffsetnanoseconds
|
||
export function FormatUTCOffsetNanoseconds(offsetNanoseconds: number): string {
|
||
const sign = offsetNanoseconds >= 0 ? '+' : '-';
|
||
const absoluteNanoseconds = Math.abs(offsetNanoseconds);
|
||
const hour = Math.floor(absoluteNanoseconds / (3600 * 1e9));
|
||
const minute = Math.floor(absoluteNanoseconds / (60 * 1e9)) % 60;
|
||
const second = Math.floor(absoluteNanoseconds / 1e9) % 60;
|
||
const subSecondNanoseconds = absoluteNanoseconds % 1e9;
|
||
const precision: 'minute' | 'auto' = second === 0 && subSecondNanoseconds === 0 ? 'minute' : 'auto';
|
||
const timeString = FormatTimeString(hour, minute, second, subSecondNanoseconds, precision);
|
||
return sign + timeString;
|
||
}
|
||
|
||
// https://tc39.es/proposal-temporal/#sec-temporal-formatdatetimeutcoffsetrounded
|
||
export function FormatDateTimeUTCOffsetRounded(offsetNanoseconds: number): string {
|
||
offsetNanoseconds = RoundNumberToIncrement(offsetNanoseconds, 60 * 1e9, RoundingMode.HalfExpand);
|
||
const offsetMinutes = offsetNanoseconds / (60 * 1e9);
|
||
return FormatOffsetTimeZoneIdentifier(offsetMinutes);
|
||
}
|
||
|
||
// https://tc39.es/proposal-temporal/#sec-temporal-totemporaltimezoneidentifier
|
||
export function ToTemporalTimeZoneIdentifier(temporalTimeZoneLike: Value | string): PlainCompletion<TimeZoneIdentifier> {
|
||
if (temporalTimeZoneLike instanceof ObjectValue && isTemporalZonedDateTimeObject(temporalTimeZoneLike)) {
|
||
return temporalTimeZoneLike.TimeZone;
|
||
}
|
||
if (!(temporalTimeZoneLike instanceof JSStringValue) && typeof temporalTimeZoneLike !== 'string') {
|
||
return Throw.TypeError('$1 is not a string', temporalTimeZoneLike);
|
||
}
|
||
const temporalTimeZoneLikeString = temporalTimeZoneLike instanceof JSStringValue ? temporalTimeZoneLike.stringValue() : temporalTimeZoneLike;
|
||
const parseResult = Q(ParseTemporalTimeZoneString(temporalTimeZoneLikeString));
|
||
const offsetMinutes = parseResult.OffsetMinutes;
|
||
if (offsetMinutes !== undefined) {
|
||
return FormatOffsetTimeZoneIdentifier(offsetMinutes);
|
||
}
|
||
const name = parseResult.Name;
|
||
const timeZoneIdentifierRecord = GetAvailableNamedTimeZoneIdentifier(name! as TimeZoneIdentifier);
|
||
if (timeZoneIdentifierRecord === undefined) {
|
||
return Throw.RangeError('Invalid time zone identifier: $1', temporalTimeZoneLikeString);
|
||
}
|
||
return timeZoneIdentifierRecord.Identifier;
|
||
}
|
||
|
||
// https://tc39.es/proposal-temporal/#sec-temporal-getoffsetnanosecondsfor
|
||
export function GetOffsetNanosecondsFor(timeZone: TimeZoneIdentifier, epochNs: bigint): number {
|
||
const parseResult = X(ParseTimeZoneIdentifier(timeZone));
|
||
if (parseResult.OffsetMinutes !== undefined) {
|
||
return parseResult.OffsetMinutes * (60 * 1e9);
|
||
}
|
||
return GetNamedTimeZoneOffsetNanoseconds(parseResult.Name!, epochNs);
|
||
}
|
||
|
||
// https://tc39.es/proposal-temporal/#sec-temporal-getisodatetimefor
|
||
export function GetISODateTimeFor(timeZone: TimeZoneIdentifier, epochNs: bigint): ISODateTimeRecord {
|
||
const offsetNanoseconds = GetOffsetNanosecondsFor(timeZone, epochNs);
|
||
const result = GetISOPartsFromEpoch(Number(epochNs));
|
||
return BalanceISODateTime(
|
||
result.ISODate.Year,
|
||
result.ISODate.Month,
|
||
result.ISODate.Day,
|
||
result.Time.Hour,
|
||
result.Time.Minute,
|
||
result.Time.Second,
|
||
result.Time.Millisecond,
|
||
result.Time.Microsecond,
|
||
result.Time.Nanosecond + offsetNanoseconds,
|
||
);
|
||
}
|
||
|
||
// https://tc39.es/proposal-temporal/#sec-temporal-getepochnanosecondsfor
|
||
export function GetEpochNanosecondsFor(
|
||
timeZone: TimeZoneIdentifier,
|
||
isoDateTime: ISODateTimeRecord,
|
||
disambiguation: 'compatible' | 'earlier' | 'later' | 'reject',
|
||
): PlainCompletion<bigint> {
|
||
const possibleEpochNs = Q(GetPossibleEpochNanoseconds(timeZone, isoDateTime));
|
||
return DisambiguatePossibleEpochNanoseconds(possibleEpochNs, timeZone, isoDateTime, disambiguation);
|
||
}
|
||
|
||
// https://tc39.es/proposal-temporal/#sec-temporal-disambiguatepossibleepochnanoseconds
|
||
export function DisambiguatePossibleEpochNanoseconds(
|
||
possibleEpochNs: readonly bigint[],
|
||
timeZone: TimeZoneIdentifier,
|
||
isoDateTime: ISODateTimeRecord,
|
||
disambiguation: 'compatible' | 'earlier' | 'later' | 'reject',
|
||
): PlainCompletion<bigint> {
|
||
let n = possibleEpochNs.length;
|
||
if (n === 1) {
|
||
return possibleEpochNs[0];
|
||
}
|
||
if (n !== 0) {
|
||
if (disambiguation === 'earlier' || disambiguation === 'compatible') {
|
||
return possibleEpochNs[0];
|
||
}
|
||
if (disambiguation === 'later') {
|
||
return possibleEpochNs[n - 1];
|
||
}
|
||
Assert(disambiguation === 'reject');
|
||
return Throw.RangeError('Multiple possible epoch nanoseconds');
|
||
}
|
||
Assert(n === 0);
|
||
if (disambiguation === 'reject') {
|
||
return Throw.RangeError('No possible epoch nanoseconds');
|
||
}
|
||
const before: ISODateTimeRecord = null!;
|
||
Assert(!!before, 'TODO(temporal): 6. Let before be the latest possible ISO Date-Time Record for which CompareISODateTime(before, isoDateTime) = -1 and ! GetPossibleEpochNanoseconds(timeZone, before) is not empty.');
|
||
const after: ISODateTimeRecord = null!;
|
||
Assert(!!after, 'TODO(temporal): 7. Let after be the earliest possible ISO Date-Time Record for which CompareISODateTime(after, isoDateTime) = 1 and ! GetPossibleEpochNanoseconds(timeZone, after) is not empty.');
|
||
const beforePossible = X(GetPossibleEpochNanoseconds(timeZone, before));
|
||
Assert(beforePossible.length === 1);
|
||
const afterPossible = X(GetPossibleEpochNanoseconds(timeZone, after));
|
||
Assert(afterPossible.length === 1);
|
||
const offsetBefore = GetOffsetNanosecondsFor(timeZone, beforePossible[0]);
|
||
const offsetAfter = GetOffsetNanosecondsFor(timeZone, afterPossible[0]);
|
||
const naneseconds = offsetAfter - offsetBefore;
|
||
Assert(abs(naneseconds) <= nsPerDay);
|
||
if (disambiguation === 'earlier') {
|
||
const timeDuration = TimeDurationFromComponents(0, 0, 0, 0, 0, -naneseconds);
|
||
const earlierTime = AddTime(isoDateTime.Time, timeDuration);
|
||
const earlierDate = AddDaysToISODate(isoDateTime.ISODate, earlierTime.Days);
|
||
const earlierDateTime = CombineISODateAndTimeRecord(earlierDate, earlierTime);
|
||
possibleEpochNs = Q(GetPossibleEpochNanoseconds(timeZone, earlierDateTime));
|
||
Assert(possibleEpochNs.length > 0);
|
||
return possibleEpochNs[0];
|
||
}
|
||
Assert(disambiguation === 'compatible' || disambiguation === 'later');
|
||
const timeDuration = TimeDurationFromComponents(0, 0, 0, 0, 0, naneseconds);
|
||
const laterTime = AddTime(isoDateTime.Time, timeDuration);
|
||
const laterDate = AddDaysToISODate(isoDateTime.ISODate, laterTime.Days);
|
||
const laterDateTime = CombineISODateAndTimeRecord(laterDate, laterTime);
|
||
possibleEpochNs = Q(GetPossibleEpochNanoseconds(timeZone, laterDateTime));
|
||
n = possibleEpochNs.length;
|
||
Assert(n > 0);
|
||
return possibleEpochNs[n - 1];
|
||
}
|
||
|
||
// https://tc39.es/proposal-temporal/#sec-temporal-getpossibleepochnanoseconds
|
||
export function GetPossibleEpochNanoseconds(
|
||
timeZone: TimeZoneIdentifier,
|
||
isoDateTime: ISODateTimeRecord,
|
||
): PlainCompletion<bigint[]> {
|
||
const parseResult = X(ParseTimeZoneIdentifier(timeZone));
|
||
let possibleEpochNanoseconds: bigint[];
|
||
if (parseResult.OffsetMinutes !== undefined) {
|
||
const balanced = BalanceISODateTime(
|
||
isoDateTime.ISODate.Year,
|
||
isoDateTime.ISODate.Month,
|
||
isoDateTime.ISODate.Day,
|
||
isoDateTime.Time.Hour,
|
||
isoDateTime.Time.Minute - parseResult.OffsetMinutes,
|
||
isoDateTime.Time.Second,
|
||
isoDateTime.Time.Millisecond,
|
||
isoDateTime.Time.Microsecond,
|
||
isoDateTime.Time.Nanosecond,
|
||
);
|
||
Q(CheckISODaysRange(balanced.ISODate));
|
||
const epochNanoseconds = GetUTCEpochNanoseconds(balanced);
|
||
possibleEpochNanoseconds = [epochNanoseconds];
|
||
} else {
|
||
possibleEpochNanoseconds = GetNamedTimeZoneEpochNanoseconds(parseResult.Name! as TimeZoneIdentifier, isoDateTime);
|
||
}
|
||
for (const epochNanoseconds of possibleEpochNanoseconds) {
|
||
if (!IsValidEpochNanoseconds(epochNanoseconds)) {
|
||
return Throw.RangeError('$1 is not a valid epoch nanoseconds', epochNanoseconds);
|
||
}
|
||
}
|
||
return possibleEpochNanoseconds;
|
||
}
|
||
|
||
// It determines the exact time that corresponds to the first valid wall-clock time in the calendar date isoDate in timeZone.
|
||
/** https://tc39.es/proposal-temporal/#sec-temporal-getstartofday */
|
||
export function GetStartOfDay(
|
||
timeZone: TimeZoneIdentifier,
|
||
isoDate: ISODateRecord,
|
||
): PlainCompletion<bigint> {
|
||
const isoDateTime = CombineISODateAndTimeRecord(isoDate, MidnightTimeRecord());
|
||
const possibleEpochNs = Q(GetPossibleEpochNanoseconds(timeZone, isoDateTime));
|
||
if (possibleEpochNs.length) {
|
||
return possibleEpochNs[0];
|
||
}
|
||
Assert(IsOffsetTimeZoneIdentifier(timeZone) === false);
|
||
// TODO(temporal)
|
||
const isoDateTimeAfter: ISODateTimeRecord = null!;
|
||
Assert(!!isoDateTimeAfter, 'TODO: isoDateTimeAfter is the ISO Date-Time Record for which DifferenceISODateTime(isoDateTime, isoDateTimeAfter, "iso8601", hour).[[Time]] is the smallest possible value > 0 for which possibleEpochNsAfter is not empty (i.e., isoDateTimeAfter represents the first local time after the transition).');
|
||
// const possibleEpochNsAfter = GetNamedTimeZoneEpochNanoseconds(timeZone, isoDateTimeAfter!);
|
||
// Assert(possibleEpochNsAfter.length === 1);
|
||
// return possibleEpochNsAfter[0];
|
||
return 0n;
|
||
}
|
||
|
||
// https://tc39.es/proposal-temporal/#sec-temporal-timezoneequals
|
||
export function TimeZoneEquals(one: TimeZoneIdentifier, two: TimeZoneIdentifier): boolean {
|
||
if (one === two) {
|
||
return true;
|
||
}
|
||
if (!IsOffsetTimeZoneIdentifier(one) && !IsOffsetTimeZoneIdentifier(two)) {
|
||
const recordOne = GetAvailableNamedTimeZoneIdentifier(one);
|
||
const recordTwo = GetAvailableNamedTimeZoneIdentifier(two);
|
||
Assert(recordOne !== undefined);
|
||
Assert(recordTwo !== undefined);
|
||
if (recordOne.PrimaryIdentifier === recordTwo.PrimaryIdentifier) {
|
||
return true;
|
||
}
|
||
}
|
||
// TODO(temporal)
|
||
// 3. Assert: If one and two are both offset time zone identifiers, they do not represent the same number of offset minutes.
|
||
return false;
|
||
}
|