Skip to content

Catch up on proposal-temporal PRs #111

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 68 additions & 55 deletions lib/ecmascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ function abs(x: JSBI): JSBI {
return x;
}

const BUILTIN_CASTS = new Map<PrimitivePropertyNames, (v: unknown) => string | number>([
type BuiltinCastFunction = (v: unknown) => string | number;
const BUILTIN_CASTS = new Map<PrimitivePropertyNames, BuiltinCastFunction>([
['year', ToIntegerThrowOnInfinity],
['month', ToPositiveInteger],
['monthCode', ToString],
Expand Down Expand Up @@ -306,7 +307,7 @@ function ParseTemporalTimeZone(stringIdent: string) {
let { ianaName, offset, z } = ParseTemporalTimeZoneString(stringIdent);
if (ianaName) return ianaName;
if (z) return 'UTC';
return offset;
return offset; // if !ianaName && !z then offset must be present
}

function FormatCalendarAnnotation(id: string, showCalendar: Temporal.ShowCalendarOption['calendarName']) {
Expand All @@ -315,9 +316,9 @@ function FormatCalendarAnnotation(id: string, showCalendar: Temporal.ShowCalenda
return `[u-ca=${id}]`;
}

function ParseISODateTime(isoString: string, { zoneRequired }: { zoneRequired: boolean }) {
const regex = zoneRequired ? PARSE.instant : PARSE.datetime;
const match = regex.exec(isoString);
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)}`;
Expand Down Expand Up @@ -380,19 +381,23 @@ function ParseISODateTime(isoString: string, { zoneRequired }: { zoneRequired: b
}

function ParseTemporalInstantString(isoString: string) {
return ParseISODateTime(isoString, { zoneRequired: true });
const result = ParseISODateTime(isoString);
if (!result.z && !result.offset) throw new RangeError('Temporal.Instant requires a time zone offset');
return result;
}

function ParseTemporalZonedDateTimeString(isoString: string) {
return ParseISODateTime(isoString, { zoneRequired: true });
const result = ParseISODateTime(isoString);
if (!result.ianaName) throw new RangeError('Temporal.ZonedDateTime requires a time zone ID in brackets');
return result;
}

function ParseTemporalDateTimeString(isoString: string) {
return ParseISODateTime(isoString, { zoneRequired: false });
return ParseISODateTime(isoString);
}

function ParseTemporalDateString(isoString: string) {
return ParseISODateTime(isoString, { zoneRequired: false });
return ParseISODateTime(isoString);
}

function ParseTemporalTimeString(isoString: string) {
Expand All @@ -410,9 +415,7 @@ function ParseTemporalTimeString(isoString: string) {
calendar = match[15];
} else {
let z;
({ hour, minute, second, millisecond, microsecond, nanosecond, calendar, z } = ParseISODateTime(isoString, {
zoneRequired: false
}));
({ hour, minute, second, millisecond, microsecond, nanosecond, calendar, z } = ParseISODateTime(isoString));
if (z) throw new RangeError('Z designator not supported for PlainTime');
}
return { hour, minute, second, millisecond, microsecond, nanosecond, calendar };
Expand All @@ -429,7 +432,7 @@ function ParseTemporalYearMonthString(isoString: string) {
calendar = match[3];
} else {
let z;
({ year, month, calendar, day: referenceISODay, z } = ParseISODateTime(isoString, { zoneRequired: false }));
({ year, month, calendar, day: referenceISODay, z } = ParseISODateTime(isoString));
if (z) throw new RangeError('Z designator not supported for PlainYearMonth');
}
return { year, month, calendar, referenceISODay };
Expand All @@ -443,7 +446,7 @@ function ParseTemporalMonthDayString(isoString: string) {
day = ToInteger(match[2]);
} else {
let z;
({ month, day, calendar, year: referenceISOYear, z } = ParseISODateTime(isoString, { zoneRequired: false }));
({ month, day, calendar, year: referenceISOYear, z } = ParseISODateTime(isoString));
if (z) throw new RangeError('Z designator not supported for PlainMonthDay');
}
return { month, day, calendar, referenceISOYear };
Expand All @@ -458,18 +461,22 @@ function ParseTemporalTimeZoneString(stringIdent: string): Partial<{
let canonicalIdent = GetCanonicalTimeZoneIdentifier(stringIdent);
if (canonicalIdent) {
canonicalIdent = canonicalIdent.toString();
if (ParseOffsetString(canonicalIdent) !== null) return { offset: canonicalIdent };
if (TestTimeZoneOffsetString(canonicalIdent)) return { offset: canonicalIdent };
return { ianaName: canonicalIdent };
}
} catch {
// fall through
}
try {
// Try parsing ISO string instead
return ParseISODateTime(stringIdent, { zoneRequired: true });
const result = ParseISODateTime(stringIdent);
if (result.z || result.offset || result.ianaName) {
return result;
}
} catch {
throw new RangeError(`Invalid time zone: ${stringIdent}`);
// fall through
}
throw new RangeError(`Invalid time zone: ${stringIdent}`);
}

function ParseTemporalDurationString(isoString: string) {
Expand Down Expand Up @@ -514,8 +521,7 @@ function ParseTemporalInstant(isoString: string) {

const epochNs = GetEpochFromISOParts(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond);
if (epochNs === null) throw new RangeError('DateTime outside of supported range');
if (!z && !offset) throw new RangeError('Temporal.Instant requires a time zone offset');
const offsetNs = z ? 0 : ParseOffsetString(offset);
const offsetNs = z ? 0 : ParseTimeZoneOffsetString(offset);
return JSBI.subtract(epochNs, JSBI.BigInt(offsetNs));
}

Expand Down Expand Up @@ -982,7 +988,7 @@ export function ToRelativeTemporalObject(options: {
} else {
let ianaName, z;
({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, ianaName, offset, z } =
ParseISODateTime(ToString(relativeTo), { zoneRequired: false }));
ParseISODateTime(ToString(relativeTo)));
if (ianaName) timeZone = ianaName;
if (z) {
offsetBehaviour = 'exact';
Expand All @@ -996,7 +1002,7 @@ export function ToRelativeTemporalObject(options: {
if (timeZone) {
timeZone = ToTemporalTimeZone(timeZone);
let offsetNs = 0;
if (offsetBehaviour === 'option') offsetNs = ParseOffsetString(ToString(offset));
if (offsetBehaviour === 'option') offsetNs = ParseTimeZoneOffsetString(ToString(offset));
const epochNanoseconds = InterpretISODateTimeOffset(
year,
month,
Expand Down Expand Up @@ -1063,35 +1069,38 @@ export function LargerOfTwoTemporalUnits<T1 extends Temporal.DateTimeUnit, T2 ex
return unit1;
}

export function ToPartialRecord<B extends AnyTemporalLikeType>(
bag: B,
fields: readonly (keyof B)[],
callerCast?: (value: unknown) => unknown
) {
if (!IsObject(bag)) return false;
let any: B;
export function ToPartialRecord<B extends AnyTemporalLikeType>(bagParam: B, fieldsParam: ReadonlyArray<keyof B>) {
// External callers are limited to specific types, but this function's
// implementation uses generic property types. The casts below (and at the
// end) convert to/from generic records.
const bag = bagParam as Record<PrimitivePropertyNames & keyof B, string | number | undefined>;
const fields = fieldsParam as ReadonlyArray<keyof B & PrimitivePropertyNames>;
let any = false;
let result = {} as typeof bag;
for (const property of fields) {
const value = bag[property];
if (value !== undefined) {
any = any || ({} as B);
if (callerCast === undefined && BUILTIN_CASTS.has(property as PrimitivePropertyNames)) {
any[property] = BUILTIN_CASTS.get(property as PrimitivePropertyNames)(value) as unknown as B[keyof B];
} else if (callerCast !== undefined) {
any[property] = callerCast(value) as unknown as B[keyof B];
any = true;
if (BUILTIN_CASTS.has(property)) {
result[property] = (BUILTIN_CASTS.get(property) as BuiltinCastFunction)(value);
} else {
any[property] = value;
result[property] = value;
}
}
}
return any ? any : false;
return any ? (result as B) : false;
}

export function PrepareTemporalFields<B extends AnyTemporalLikeType>(
bag: B,
fields: ReadonlyArray<FieldRecord<B>>
): Required<B> | undefined {
if (!IsObject(bag)) return undefined;
const result = {} as B;
bagParam: B,
fieldsParam: ReadonlyArray<FieldRecord<B>>
): Required<B> {
// External callers are limited to specific types, but this function's
// implementation uses generic property types. The casts below (and at the
// end) convert to/from generic records.
const bag = bagParam as Record<PrimitivePropertyNames & keyof B, string | number | undefined>;
const fields = fieldsParam as ReadonlyArray<FieldRecord<typeof bag>>;
const result = {} as typeof bag;
let any = false;
for (const fieldRecord of fields) {
const [property, defaultValue] = fieldRecord;
Expand All @@ -1100,15 +1109,11 @@ export function PrepareTemporalFields<B extends AnyTemporalLikeType>(
if (fieldRecord.length === 1) {
throw new TypeError(`required property '${property}' missing or undefined`);
}
// TODO: two TS issues here:
// 1. `undefined` was stripped from the type of `defaultValue`. Will it
// come back when strictNullChecks is enabled?
// 2. Can types be improved to remove the need for the type assertion?
value = defaultValue as unknown as typeof bag[typeof property];
value = defaultValue;
} else {
any = true;
if (BUILTIN_CASTS.has(property as PrimitivePropertyNames)) {
value = BUILTIN_CASTS.get(property as PrimitivePropertyNames)(value) as unknown as typeof bag[keyof B];
value = (BUILTIN_CASTS.get(property) as BuiltinCastFunction)(value);
}
}
result[property] = value;
Expand All @@ -1117,12 +1122,12 @@ export function PrepareTemporalFields<B extends AnyTemporalLikeType>(
throw new TypeError('no supported properties found');
}
if (
((result as Temporal.PlainDateLike)['era'] === undefined) !==
((result as Temporal.PlainDateLike)['eraYear'] === undefined)
((result as { era: unknown })['era'] === undefined) !==
((result as { eraYear: unknown })['eraYear'] === undefined)
) {
throw new RangeError("properties 'era' and 'eraYear' must be provided together");
}
return result as Required<B>;
return result as unknown as Required<B>;
}

// field access in the following operations is intentionally alphabetical
Expand Down Expand Up @@ -1663,7 +1668,7 @@ export function ToTemporalZonedDateTime(
matchMinute = true; // ISO strings may specify offset with less precision
}
let offsetNs = 0;
if (offsetBehaviour === 'option') offsetNs = ParseOffsetString(offset);
if (offsetBehaviour === 'option') offsetNs = ParseTimeZoneOffsetString(offset);
const disambiguation = ToTemporalDisambiguation(options);
const offsetOpt = ToTemporalOffset(options, 'reject');
const epochNanoseconds = InterpretISODateTimeOffset(
Expand Down Expand Up @@ -2083,7 +2088,7 @@ export function ToTemporalCalendar(calendarLikeParam: CalendarParams['from'][0])
if (IsBuiltinCalendar(identifier)) return new TemporalCalendar(identifier);
let calendar;
try {
({ calendar } = ParseISODateTime(identifier, { zoneRequired: false }));
({ calendar } = ParseISODateTime(identifier));
} catch {
throw new RangeError(`Invalid calendar: ${identifier}`);
}
Expand Down Expand Up @@ -2682,9 +2687,15 @@ export function TemporalZonedDateTimeToString(
return result;
}

export function ParseOffsetString(string: string): number {
export function TestTimeZoneOffsetString(string: string) {
return OFFSET.test(StringCtor(string));
}

export function ParseTimeZoneOffsetString(string: string): number {
const match = OFFSET.exec(StringCtor(string));
if (!match) return null;
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);
Expand All @@ -2694,8 +2705,10 @@ export function ParseOffsetString(string: string): number {
}

export function GetCanonicalTimeZoneIdentifier(timeZoneIdentifier: string): string {
const offsetNs = ParseOffsetString(timeZoneIdentifier);
if (offsetNs !== null) return FormatTimeZoneOffsetString(offsetNs);
if (TestTimeZoneOffsetString(timeZoneIdentifier)) {
const offsetNs = ParseTimeZoneOffsetString(timeZoneIdentifier);
return FormatTimeZoneOffsetString(offsetNs);
}
const formatter = getIntlDateTimeFormatEnUsForTimeZone(StringCtor(timeZoneIdentifier));
return formatter.resolvedOptions().timeZone;
}
Expand Down
6 changes: 1 addition & 5 deletions lib/regex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,10 @@ export const offset = /([+\u2212-])([01][0-9]|2[0-3])(?::?([0-5][0-9])(?::?([0-5
const zonesplit = new RegExp(`(?:([zZ])|(?:${offset.source})?)(?:\\[(${timeZoneID.source})\\])?`);
const calendar = new RegExp(`\\[u-ca=(${calendarID.source})\\]`);

export const instant = new RegExp(
export const zoneddatetime = new RegExp(
`^${datesplit.source}(?:(?:T|\\s+)${timesplit.source})?${zonesplit.source}(?:${calendar.source})?$`,
'i'
);
export const datetime = new RegExp(
`^${datesplit.source}(?:(?:T|\\s+)${timesplit.source})?(?:${zonesplit.source})?(?:${calendar.source})?$`,
'i'
);

export const time = new RegExp(`^${timesplit.source}(?:${zonesplit.source})?(?:${calendar.source})?$`, 'i');

Expand Down
14 changes: 7 additions & 7 deletions lib/timezone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ export class TimeZone implements Temporal.TimeZone {
const instant = ES.ToTemporalInstant(instantParam);
const id = GetSlot(this, TIMEZONE_ID);

const offsetNs = ES.ParseOffsetString(id);
if (offsetNs !== null) return offsetNs;

if (ES.TestTimeZoneOffsetString(id)) {
return ES.ParseTimeZoneOffsetString(id);
}
return ES.GetIANATimeZoneOffsetNanoseconds(GetSlot(instant, EPOCHNANOSECONDS), id);
}
getOffsetStringFor(instantParam: Params['getOffsetStringFor'][0]): Return['getOffsetStringFor'] {
Expand Down Expand Up @@ -84,8 +84,7 @@ export class TimeZone implements Temporal.TimeZone {
const Instant = GetIntrinsic('%Temporal.Instant%');
const id = GetSlot(this, TIMEZONE_ID);

const offsetNs = ES.ParseOffsetString(id);
if (offsetNs !== null) {
if (ES.TestTimeZoneOffsetString(id)) {
const epochNs = ES.GetEpochFromISOParts(
GetSlot(dateTime, ISO_YEAR),
GetSlot(dateTime, ISO_MONTH),
Expand All @@ -98,6 +97,7 @@ export class TimeZone implements Temporal.TimeZone {
GetSlot(dateTime, ISO_NANOSECOND)
);
if (epochNs === null) throw new RangeError('DateTime outside of supported range');
const offsetNs = ES.ParseTimeZoneOffsetString(id);
return [new Instant(JSBI.subtract(epochNs, JSBI.BigInt(offsetNs)))];
}

Expand All @@ -121,7 +121,7 @@ export class TimeZone implements Temporal.TimeZone {
const id = GetSlot(this, TIMEZONE_ID);

// Offset time zones or UTC have no transitions
if (ES.ParseOffsetString(id) !== null || id === 'UTC') {
if (ES.TestTimeZoneOffsetString(id) || id === 'UTC') {
return null;
}

Expand All @@ -136,7 +136,7 @@ export class TimeZone implements Temporal.TimeZone {
const id = GetSlot(this, TIMEZONE_ID);

// Offset time zones or UTC have no transitions
if (ES.ParseOffsetString(id) !== null || id === 'UTC') {
if (ES.TestTimeZoneOffsetString(id) || id === 'UTC') {
return null;
}

Expand Down
2 changes: 1 addition & 1 deletion lib/zoneddatetime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ export class ZonedDateTime implements Temporal.ZonedDateTime {
fields = ES.PrepareTemporalFields(fields, entries as any);
const { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } =
ES.InterpretTemporalDateTimeFields(calendar, fields, options);
const offsetNs = ES.ParseOffsetString(fields.offset);
const offsetNs = ES.ParseTimeZoneOffsetString(fields.offset);
const epochNanoseconds = ES.InterpretISODateTimeOffset(
year,
month,
Expand Down
Loading