Skip to content
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

Editorial: refactor time zone offset handling (prepare for #2607) #2621

Merged
merged 1 commit into from
Jun 29, 2023
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
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"@tc39/ecma262-biblio": "=2.1.2577",
"@typescript-eslint/eslint-plugin": "^5.59.9",
"@typescript-eslint/parser": "^5.59.9",
"ecmarkup": "^17.0.0",
"ecmarkup": "^17.0.1",
"eslint": "^8.42.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
Expand Down
87 changes: 44 additions & 43 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -360,24 +360,6 @@ export function RejectTemporalLikeObject(item) {
}
}

export function CanonicalizeTimeZoneOffsetString(offsetString) {
const offsetNs = ParseTimeZoneOffsetString(offsetString);
return FormatTimeZoneOffsetString(offsetNs);
}

export function ParseTemporalTimeZone(stringIdent) {
const { tzName, offset, z } = ParseTemporalTimeZoneString(stringIdent);
if (tzName) {
if (IsTimeZoneOffsetString(tzName)) return CanonicalizeTimeZoneOffsetString(tzName);
const record = GetAvailableNamedTimeZoneIdentifier(tzName);
if (!record) throw new RangeError(`Unrecognized time zone ${tzName}`);
return record.primaryIdentifier;
}
if (z) return 'UTC';
// if !tzName && !z then offset must be present
return CanonicalizeTimeZoneOffsetString(offset);
}

export function MaybeFormatCalendarAnnotation(calendar, showCalendar) {
if (showCalendar === 'never') return '';
return FormatCalendarAnnotation(ToTemporalCalendarIdentifier(calendar), showCalendar);
Expand Down Expand Up @@ -570,6 +552,18 @@ export function ParseTemporalMonthDayString(isoString) {
return { month, day, calendar, referenceISOYear };
}

const TIMEZONE_IDENTIFIER = new RegExp(`^${PARSE.timeZoneID.source}$`, 'i');
const OFFSET_IDENTIFIER = new RegExp(`^${PARSE.offsetIdentifier.source}$`);

export function ParseTimeZoneIdentifier(identifier) {
if (!TIMEZONE_IDENTIFIER.test(identifier)) throw new RangeError(`Invalid time zone identifier: ${identifier}`);
if (OFFSET_IDENTIFIER.test(identifier)) {
const { offsetNanoseconds } = ParseDateTimeUTCOffset(identifier);
return { offsetNanoseconds };
}
return { tzName: identifier };
}

export function ParseTemporalTimeZoneString(stringIdent) {
const bareID = new RegExp(`^${PARSE.timeZoneID.source}$`, 'i');
if (bareID.test(stringIdent)) return { tzName: stringIdent };
Expand Down Expand Up @@ -641,7 +635,7 @@ export function ParseTemporalInstant(isoString) {
ParseTemporalInstantString(isoString);

if (!z && !offset) throw new RangeError('Temporal.Instant requires a time zone offset');
const offsetNs = z ? 0 : ParseTimeZoneOffsetString(offset);
const offsetNs = z ? 0 : ParseDateTimeUTCOffset(offset).offsetNanoseconds;
({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = BalanceISODateTime(
year,
month,
Expand Down Expand Up @@ -1005,7 +999,7 @@ export function ToRelativeTemporalObject(options) {
calendar = ASCIILowercase(calendar);
}
if (timeZone === undefined) return CreateTemporalDate(year, month, day, calendar);
const offsetNs = offsetBehaviour === 'option' ? ParseTimeZoneOffsetString(offset) : 0;
const offsetNs = offsetBehaviour === 'option' ? ParseDateTimeUTCOffset(offset).offsetNanoseconds : 0;
const epochNanoseconds = InterpretISODateTimeOffset(
year,
month,
Expand Down Expand Up @@ -1406,7 +1400,7 @@ export function InterpretISODateTimeOffset(
// the user-provided offset doesn't match any instants for this time
// zone and date/time.
if (offsetOpt === 'reject') {
const offsetStr = FormatTimeZoneOffsetString(offsetNs);
const offsetStr = FormatOffsetTimeZoneIdentifier(offsetNs);
const timeZoneString = IsTemporalTimeZone(timeZone) ? GetSlot(timeZone, TIMEZONE_ID) : 'time zone';
throw new RangeError(`Offset ${offsetStr} is invalid for ${dt} in ${timeZoneString}`);
}
Expand Down Expand Up @@ -1469,7 +1463,7 @@ export function ToTemporalZonedDateTime(item, options) {
ToTemporalOverflow(options); // validate and ignore
}
let offsetNs = 0;
if (offsetBehaviour === 'option') offsetNs = ParseTimeZoneOffsetString(offset);
if (offsetBehaviour === 'option') offsetNs = ParseDateTimeUTCOffset(offset).offsetNanoseconds;
const epochNanoseconds = InterpretISODateTimeOffset(
year,
month,
Expand Down Expand Up @@ -2099,7 +2093,20 @@ export function ToTemporalTimeZoneSlotValue(temporalTimeZoneLike) {
return temporalTimeZoneLike;
}
const identifier = ToString(temporalTimeZoneLike);
return ParseTemporalTimeZone(identifier);
const { tzName, offset, z } = ParseTemporalTimeZoneString(identifier);
if (tzName) {
// tzName is any valid identifier string in brackets, and could be an offset identifier
const { offsetNanoseconds } = ParseTimeZoneIdentifier(tzName);
if (offsetNanoseconds !== undefined) return FormatOffsetTimeZoneIdentifier(offsetNanoseconds);

const record = GetAvailableNamedTimeZoneIdentifier(tzName);
if (!record) throw new RangeError(`Unrecognized time zone ${tzName}`);
return record.primaryIdentifier;
}
if (z) return 'UTC';
// if !tzName && !z then offset must be present
const { offsetNanoseconds } = ParseDateTimeUTCOffset(offset);
return FormatOffsetTimeZoneIdentifier(offsetNanoseconds);
}

export function ToTemporalTimeZoneIdentifier(slotValue) {
Expand Down Expand Up @@ -2162,7 +2169,7 @@ export function GetOffsetNanosecondsFor(timeZone, instant, getOffsetNanosecondsF

export function GetOffsetStringFor(timeZone, instant) {
const offsetNs = GetOffsetNanosecondsFor(timeZone, instant);
return FormatTimeZoneOffsetString(offsetNs);
return FormatOffsetTimeZoneIdentifier(offsetNs);
}

export function GetPlainDateTimeFor(timeZone, instant, calendar) {
Expand Down Expand Up @@ -2384,7 +2391,7 @@ export function TemporalInstantToString(instant, timeZone, precision) {
let timeZoneString = 'Z';
if (timeZone !== undefined) {
const offsetNs = GetOffsetNanosecondsFor(outputTimeZone, instant);
timeZoneString = FormatISOTimeZoneOffsetString(offsetNs);
timeZoneString = FormatDateTimeUTCOffsetRounded(offsetNs);
}
return `${year}-${month}-${day}T${hour}:${minute}${seconds}${timeZoneString}`;
}
Expand Down Expand Up @@ -2564,7 +2571,7 @@ export function TemporalZonedDateTimeToString(
let result = `${year}-${month}-${day}T${hour}:${minute}${seconds}`;
if (showOffset !== 'never') {
const offsetNs = GetOffsetNanosecondsFor(tz, instant);
result += FormatISOTimeZoneOffsetString(offsetNs);
result += FormatDateTimeUTCOffsetRounded(offsetNs);
}
if (showTimeZone !== 'never') {
const identifier = ToTemporalTimeZoneIdentifier(tz);
Expand All @@ -2575,11 +2582,11 @@ export function TemporalZonedDateTimeToString(
return result;
}

export function IsTimeZoneOffsetString(string) {
export function IsOffsetTimeZoneIdentifier(string) {
return OFFSET.test(string);
}

export function ParseTimeZoneOffsetString(string) {
export function ParseDateTimeUTCOffset(string) {
const match = OFFSET.exec(string);
if (!match) {
throw new RangeError(`invalid time zone offset: ${string}`);
Expand All @@ -2589,7 +2596,9 @@ export function ParseTimeZoneOffsetString(string) {
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);
const offsetNanoseconds = sign * (((hours * 60 + minutes) * 60 + seconds) * 1e9 + nanoseconds);
const hasSubMinutePrecision = match[4] !== undefined || match[5] !== undefined;
return { offsetNanoseconds, hasSubMinutePrecision };
}

let canonicalTimeZoneIdsCache = undefined;
Expand Down Expand Up @@ -2702,17 +2711,16 @@ export function GetNamedTimeZoneOffsetNanoseconds(id, epochNanoseconds) {
return +utc.minus(epochNanoseconds);
}

export function FormatTimeZoneOffsetString(offsetNanoseconds) {
export function FormatOffsetTimeZoneIdentifier(offsetNanoseconds) {
const sign = offsetNanoseconds < 0 ? '-' : '+';
offsetNanoseconds = MathAbs(offsetNanoseconds);
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 minutes = MathFloor(offsetNanoseconds / 60e9) % 60;
const minuteString = ISODateTimePartString(minutes);
const seconds = MathFloor(offsetNanoseconds / 1e9) % 60;
const secondString = ISODateTimePartString(seconds);
const nanoseconds = offsetNanoseconds % 1e9;
let post = '';
if (nanoseconds) {
let fraction = `${nanoseconds}`.padStart(9, '0');
Expand All @@ -2724,16 +2732,9 @@ export function FormatTimeZoneOffsetString(offsetNanoseconds) {
return `${sign}${hourString}:${minuteString}${post}`;
}

export function FormatISOTimeZoneOffsetString(offsetNanoseconds) {
export function FormatDateTimeUTCOffsetRounded(offsetNanoseconds) {
offsetNanoseconds = RoundNumberToIncrement(bigInt(offsetNanoseconds), 60e9, 'halfExpand').toJSNumber();
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}`;
return FormatOffsetTimeZoneIdentifier(offsetNanoseconds);
}

export function GetUTCEpochNanoseconds(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond) {
Expand Down
2 changes: 1 addition & 1 deletion polyfill/lib/intl.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function DateTimeFormat(locale = undefined, options = undefined) {
this[TZ_ORIGINAL] = ro.timeZone;
} else {
const id = ES.ToString(timeZoneOption);
if (ES.IsTimeZoneOffsetString(id)) {
if (ES.IsOffsetTimeZoneIdentifier(id)) {
// Note: https://github.com/tc39/ecma402/issues/683 will remove this
throw new RangeError('Intl.DateTimeFormat does not currently support offset time zones');
}
Expand Down
1 change: 1 addition & 0 deletions polyfill/lib/regex.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const datesplit = new RegExp(
const timesplit = /(\d{2})(?::(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?|(\d{2})(?:(\d{2})(?:[.,](\d{1,9}))?)?)?/;
export const offset = /([+\u2212-])([01][0-9]|2[0-3])(?::?([0-5][0-9])(?::?([0-5][0-9])(?:[.,](\d{1,9}))?)?)?/;
const offsetpart = new RegExp(`([zZ])|${offset.source}?`);
export const offsetIdentifier = offset;
export const annotation = /\[(!)?([a-z_][a-z0-9_-]*)=([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\]/g;

export const zoneddatetime = new RegExp(
Expand Down
20 changes: 10 additions & 10 deletions polyfill/lib/timezone.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ export class TimeZone {
throw new RangeError('missing argument: identifier is required');
}
let stringIdentifier = ES.ToString(identifier);
if (ES.IsTimeZoneOffsetString(stringIdentifier)) {
stringIdentifier = ES.CanonicalizeTimeZoneOffsetString(stringIdentifier);
const parseResult = ES.ParseTimeZoneIdentifier(identifier);
if (parseResult.offsetNanoseconds !== undefined) {
stringIdentifier = ES.FormatOffsetTimeZoneIdentifier(parseResult.offsetNanoseconds);
} else {
const record = ES.GetAvailableNamedTimeZoneIdentifier(stringIdentifier);
if (!record) throw new RangeError(`Invalid time zone identifier: ${stringIdentifier}`);
Expand All @@ -55,9 +56,8 @@ export class TimeZone {
instant = ES.ToTemporalInstant(instant);
const id = GetSlot(this, TIMEZONE_ID);

if (ES.IsTimeZoneOffsetString(id)) {
return ES.ParseTimeZoneOffsetString(id);
}
const offsetNanoseconds = ES.ParseTimeZoneIdentifier(id).offsetNanoseconds;
if (offsetNanoseconds !== undefined) return offsetNanoseconds;

return ES.GetNamedTimeZoneOffsetNanoseconds(id, GetSlot(instant, EPOCHNANOSECONDS));
}
Expand Down Expand Up @@ -85,7 +85,8 @@ export class TimeZone {
const Instant = GetIntrinsic('%Temporal.Instant%');
const id = GetSlot(this, TIMEZONE_ID);

if (ES.IsTimeZoneOffsetString(id)) {
const offsetNanoseconds = ES.ParseTimeZoneIdentifier(id).offsetNanoseconds;
if (offsetNanoseconds !== undefined) {
const epochNs = ES.GetUTCEpochNanoseconds(
GetSlot(dateTime, ISO_YEAR),
GetSlot(dateTime, ISO_MONTH),
Expand All @@ -98,8 +99,7 @@ export class TimeZone {
GetSlot(dateTime, ISO_NANOSECOND)
);
if (epochNs === null) throw new RangeError('DateTime outside of supported range');
const offsetNs = ES.ParseTimeZoneOffsetString(id);
return [new Instant(epochNs.minus(offsetNs))];
return [new Instant(epochNs.minus(offsetNanoseconds))];
}

const possibleEpochNs = ES.GetNamedTimeZoneEpochNanoseconds(
Expand All @@ -122,7 +122,7 @@ export class TimeZone {
const id = GetSlot(this, TIMEZONE_ID);

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

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

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

Expand Down
4 changes: 2 additions & 2 deletions polyfill/lib/zoneddatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ export class ZonedDateTime {

let { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } =
ES.InterpretTemporalDateTimeFields(calendar, fields, options);
const offsetNs = ES.ParseTimeZoneOffsetString(fields.offset);
const offsetNs = ES.ParseDateTimeUTCOffset(fields.offset).offsetNanoseconds;
const timeZone = GetSlot(this, TIME_ZONE);
const epochNanoseconds = ES.InterpretISODateTimeOffset(
year,
Expand Down Expand Up @@ -472,7 +472,7 @@ export class ZonedDateTime {
}

const timeZoneIdentifier = ES.ToTemporalTimeZoneIdentifier(GetSlot(this, TIME_ZONE));
if (ES.IsTimeZoneOffsetString(timeZoneIdentifier)) {
if (ES.IsOffsetTimeZoneIdentifier(timeZoneIdentifier)) {
// Note: https://github.com/tc39/ecma402/issues/683 will remove this
throw new RangeError('toLocaleString does not currently support offset time zones');
} else {
Expand Down
16 changes: 8 additions & 8 deletions polyfill/test/validStrings.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,9 @@ const temporalSign = withCode(
);
const temporalDecimalFraction = fraction;
function saveOffset(data, result) {
data.offset = ES.CanonicalizeTimeZoneOffsetString(result);
data.offset = ES.FormatOffsetTimeZoneIdentifier(ES.ParseDateTimeUTCOffset(result).offsetNanoseconds);
}
const utcOffset = withCode(
const utcOffsetSubMinutePrecision = withCode(
seq(
temporalSign,
hour,
Expand All @@ -261,7 +261,7 @@ const utcOffset = withCode(
),
saveOffset
);
const timeZoneUTCOffset = choice(utcDesignator, utcOffset);
const dateTimeUTCOffset = choice(utcDesignator, utcOffsetSubMinutePrecision);
const timeZoneUTCOffsetName = seq(
sign,
hour,
Expand Down Expand Up @@ -294,7 +294,7 @@ const timeSpec = seq(
timeHour,
choice([':', timeMinute, [':', timeSecond, [timeFraction]]], seq(timeMinute, [timeSecond, [timeFraction]]))
);
const timeSpecWithOptionalOffsetNotAmbiguous = withSyntaxConstraints(seq(timeSpec, [timeZoneUTCOffset]), (result) => {
const timeSpecWithOptionalOffsetNotAmbiguous = withSyntaxConstraints(seq(timeSpec, [dateTimeUTCOffset]), (result) => {
if (/^(?:(?!02-?30)(?:0[1-9]|1[012])-?(?:0[1-9]|[12][0-9]|30)|(?:0[13578]|10|12)-?31)$/.test(result)) {
throw new SyntaxError('valid PlainMonthDay');
}
Expand All @@ -312,17 +312,17 @@ const date = withSyntaxConstraints(
choice(seq(dateYear, '-', dateMonth, '-', dateDay), seq(dateYear, dateMonth, dateDay)),
validateDayOfMonth
);
const dateTime = seq(date, [dateTimeSeparator, timeSpec, [timeZoneUTCOffset]]);
const dateTime = seq(date, [dateTimeSeparator, timeSpec, [dateTimeUTCOffset]]);
const annotatedTime = choice(
seq(timeDesignator, timeSpec, [timeZoneUTCOffset], [timeZoneAnnotation], [annotations]),
seq(timeDesignator, timeSpec, [dateTimeUTCOffset], [timeZoneAnnotation], [annotations]),
seq(timeSpecWithOptionalOffsetNotAmbiguous, [timeZoneAnnotation], [annotations])
);
const annotatedDateTime = seq(dateTime, [timeZoneAnnotation], [annotations]);
const annotatedDateTimeTimeRequired = seq(
date,
dateTimeSeparator,
timeSpec,
[timeZoneUTCOffset],
[dateTimeUTCOffset],
[timeZoneAnnotation],
[annotations]
);
Expand Down Expand Up @@ -411,7 +411,7 @@ const duration = seq(
choice(durationDate, durationTime)
);

const instant = seq(date, dateTimeSeparator, timeSpec, timeZoneUTCOffset, [timeZoneAnnotation], [annotations]);
const instant = seq(date, dateTimeSeparator, timeSpec, dateTimeUTCOffset, [timeZoneAnnotation], [annotations]);
const zonedDateTime = seq(dateTime, timeZoneAnnotation, [annotations]);

// goal elements
Expand Down
Loading