From 2ed580cf13ad99f9d4d94198dc6734a0a7f3d93f Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 28 Jun 2023 21:42:06 -0700 Subject: [PATCH] Editorial: refactor time zone offset handling This commit refactors spec text and polyfill code for time zone offsets, especially to split the handling of offsets in ISO strings from offsets used as time zone identifiers. This will help prepare for a later normative commit where time zone identifiers are limited to minutes precision while ISO string offset inputs and ZonedDateTime's `offset` property still support nanosecond precision. --- package-lock.json | 14 +- package.json | 2 +- polyfill/lib/ecmascript.mjs | 87 +++++------ polyfill/lib/intl.mjs | 2 +- polyfill/lib/regex.mjs | 1 + polyfill/lib/timezone.mjs | 20 +-- polyfill/lib/zoneddatetime.mjs | 4 +- polyfill/test/validStrings.mjs | 16 +- spec/abstractops.html | 61 +++++--- spec/instant.html | 5 +- spec/intl.html | 3 +- spec/mainadditions.html | 271 ++++++++++++++++++++++++++++++++- spec/timezone.html | 131 +++++++++------- spec/zoneddatetime.html | 8 +- 14 files changed, 471 insertions(+), 154 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8b64fdad77..b66f4f5919 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,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", @@ -1018,9 +1018,9 @@ } }, "node_modules/ecmarkup": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/ecmarkup/-/ecmarkup-17.0.0.tgz", - "integrity": "sha512-eQr9Vn9IPIH3rrbYEGPqfAwDJ9pg1zrOSZXc8HQwVMQ9d5tb+BsoPeKw5W1SinL09yZalcbLyqnX7rC393VRdA==", + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/ecmarkup/-/ecmarkup-17.0.1.tgz", + "integrity": "sha512-UEfqnRBbqyBdy82gpArzDf5fopvP3Rs/ogE0g2kKVlB/TEEcnY2h0w21/sVFddLtVwOzdDLJ6Kql+oBOQLtufw==", "dev": true, "dependencies": { "chalk": "^4.1.2", @@ -3566,9 +3566,9 @@ } }, "ecmarkup": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/ecmarkup/-/ecmarkup-17.0.0.tgz", - "integrity": "sha512-eQr9Vn9IPIH3rrbYEGPqfAwDJ9pg1zrOSZXc8HQwVMQ9d5tb+BsoPeKw5W1SinL09yZalcbLyqnX7rC393VRdA==", + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/ecmarkup/-/ecmarkup-17.0.1.tgz", + "integrity": "sha512-UEfqnRBbqyBdy82gpArzDf5fopvP3Rs/ogE0g2kKVlB/TEEcnY2h0w21/sVFddLtVwOzdDLJ6Kql+oBOQLtufw==", "dev": true, "requires": { "chalk": "^4.1.2", diff --git a/package.json b/package.json index c3918f5973..47ae04969f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/polyfill/lib/ecmascript.mjs b/polyfill/lib/ecmascript.mjs index 25d54d96f2..e7f04c0bc6 100644 --- a/polyfill/lib/ecmascript.mjs +++ b/polyfill/lib/ecmascript.mjs @@ -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); @@ -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 }; @@ -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, @@ -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, @@ -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}`); } @@ -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, @@ -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) { @@ -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) { @@ -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}`; } @@ -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); @@ -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}`); @@ -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; @@ -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'); @@ -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) { diff --git a/polyfill/lib/intl.mjs b/polyfill/lib/intl.mjs index e3c6a40369..9d78ee4301 100644 --- a/polyfill/lib/intl.mjs +++ b/polyfill/lib/intl.mjs @@ -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'); } diff --git a/polyfill/lib/regex.mjs b/polyfill/lib/regex.mjs index aa9cd193bc..e07db6dffb 100644 --- a/polyfill/lib/regex.mjs +++ b/polyfill/lib/regex.mjs @@ -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( diff --git a/polyfill/lib/timezone.mjs b/polyfill/lib/timezone.mjs index 9796e980f6..7c6c8a2e85 100644 --- a/polyfill/lib/timezone.mjs +++ b/polyfill/lib/timezone.mjs @@ -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}`); @@ -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)); } @@ -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), @@ -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( @@ -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; } @@ -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; } diff --git a/polyfill/lib/zoneddatetime.mjs b/polyfill/lib/zoneddatetime.mjs index 1a593c7f15..cbd4f8c8ce 100644 --- a/polyfill/lib/zoneddatetime.mjs +++ b/polyfill/lib/zoneddatetime.mjs @@ -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, @@ -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 { diff --git a/polyfill/test/validStrings.mjs b/polyfill/test/validStrings.mjs index 3f8e131226..554ff063a7 100644 --- a/polyfill/test/validStrings.mjs +++ b/polyfill/test/validStrings.mjs @@ -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, @@ -261,7 +261,7 @@ const utcOffset = withCode( ), saveOffset ); -const timeZoneUTCOffset = choice(utcDesignator, utcOffset); +const dateTimeUTCOffset = choice(utcDesignator, utcOffsetSubMinutePrecision); const timeZoneUTCOffsetName = seq( sign, hour, @@ -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'); } @@ -312,9 +312,9 @@ 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]); @@ -322,7 +322,7 @@ const annotatedDateTimeTimeRequired = seq( date, dateTimeSeparator, timeSpec, - [timeZoneUTCOffset], + [dateTimeUTCOffset], [timeZoneAnnotation], [annotations] ); @@ -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 diff --git a/spec/abstractops.html b/spec/abstractops.html index 2d3be834f7..8ce2455ce3 100644 --- a/spec/abstractops.html +++ b/spec/abstractops.html @@ -622,8 +622,7 @@

ToRelativeTemporalObject ( _options_ )

1. If _timeZone_ is *undefined*, then 1. Return ? CreateTemporalDate(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]], _calendar_). 1. If _offsetBehaviour_ is ~option~, then - 1. If IsTimeZoneOffsetString(_offsetString_) is *false*, throw a *RangeError* exception. - 1. Let _offsetNs_ be ParseTimeZoneOffsetString(_offsetString_). + 1. Let _offsetNs_ be ? ParseDateTimeUTCOffset(_offsetString_).[[OffsetNanoseconds]]. 1. Else, 1. Let _offsetNs_ be 0. 1. Let _epochNanoseconds_ be ? InterpretISODateTimeOffset(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]], _result_.[[Hour]], _result_.[[Minute]], _result_.[[Second]], _result_.[[Millisecond]], _result_.[[Microsecond]], _result_.[[Nanosecond]], _offsetBehaviour_, _offsetNs_, _timeZone_, *"compatible"*, *"reject"*, _matchBehaviour_). @@ -946,6 +945,7 @@

ISO 8601 grammar

  • Alphabetic designators may be in lower or upper case.
  • Period or comma may be used as the decimal separator.
  • A time zone offset of *"-00:00"* is allowed, and means the same thing as *"+00:00"*.
  • +
  • UTC offsets may have seconds and up to 9 sub-second fractional digits.
  • In a combined representation, combinations of date, time, and time zone offset with Basic (no `-` or `:` separators) and Extended (with `-` or `:` separators) formatting are allowed. (The date, time, and time zone offset must each be fully in Basic format or Extended format.) @@ -1098,14 +1098,25 @@

    ISO 8601 grammar

    TimeFraction : Fraction - TimeZoneUTCOffset : - UTCOffset - UTCDesignator + UTCOffsetWithSubMinuteComponents[Extended] : + Sign Hour[+Padded] TimeSeparator[?Extended] MinuteSecond TimeSeparator[?Extended] MinuteSecond Fraction? - TimeZoneUTCOffsetName[Extended] : + UTCOffsetMinutePrecision : Sign Hour[+Padded] - Sign Hour[+Padded] TimeSeparator[?Extended] MinuteSecond - Sign Hour[+Padded] TimeSeparator[?Extended] MinuteSecond TimeSeparator[?Extended] MinuteSecond Fraction? + Sign Hour[+Padded] TimeSeparator[+Extended] MinuteSecond + Sign Hour[+Padded] TimeSeparator[~Extended] MinuteSecond + + UTCOffsetSubMinutePrecision : + UTCOffsetMinutePrecision + UTCOffsetWithSubMinuteComponents[+Extended] + UTCOffsetWithSubMinuteComponents[~Extended] + + DateTimeUTCOffset : + UTCDesignator + UTCOffsetSubMinutePrecision + + TimeZoneUTCOffsetName : + UTCOffsetSubMinutePrecision TZLeadingChar : Alpha @@ -1145,8 +1156,7 @@

    ISO 8601 grammar

    TimeZoneIdentifier : TimeZoneIANAName - TimeZoneUTCOffsetName[+Extended] - TimeZoneUTCOffsetName[~Extended] + TimeZoneUTCOffsetName TimeZoneAnnotation : `[` AnnotationCriticalFlag? TimeZoneIdentifier `]` @@ -1186,21 +1196,21 @@

    ISO 8601 grammar

    TimeHour TimeMinute TimeSecond TimeFraction? TimeSpecWithOptionalOffsetNotAmbiguous : - TimeSpec TimeZoneUTCOffset? but not one of ValidMonthDay or DateSpecYearMonth + TimeSpec DateTimeUTCOffset? but not one of ValidMonthDay or DateSpecYearMonth DateTime : Date - Date DateTimeSeparator TimeSpec TimeZoneUTCOffset? + Date DateTimeSeparator TimeSpec DateTimeUTCOffset? AnnotatedTime : - TimeDesignator TimeSpec TimeZoneUTCOffset? TimeZoneAnnotation? Annotations? + TimeDesignator TimeSpec DateTimeUTCOffset? TimeZoneAnnotation? Annotations? TimeSpecWithOptionalOffsetNotAmbiguous TimeZoneAnnotation? Annotations? AnnotatedDateTime: DateTime TimeZoneAnnotation? Annotations? AnnotatedDateTimeTimeRequired : - Date DateTimeSeparator TimeSpec TimeZoneUTCOffset? TimeZoneAnnotation? Annotations? + Date DateTimeSeparator TimeSpec DateTimeUTCOffset? TimeZoneAnnotation? Annotations? AnnotatedYearMonth: DateSpecYearMonth TimeZoneAnnotation? Annotations? @@ -1281,7 +1291,7 @@

    ISO 8601 grammar

    Sign? DurationDesignator DurationTime TemporalInstantString : - Date DateTimeSeparator TimeSpec TimeZoneUTCOffset TimeZoneAnnotation? Annotations? + Date DateTimeSeparator TimeSpec DateTimeUTCOffset TimeZoneAnnotation? Annotations? TemporalDateTimeString : AnnotatedDateTime @@ -1408,10 +1418,9 @@

    1. Set _timeZoneResult_.[[Name]] to CodePointsToString(_name_). 1. If _parseResult_ contains a |UTCDesignator| Parse Node, then 1. Set _timeZoneResult_.[[Z]] to *true*. - 1. Else, - 1. If _parseResult_ contains a |UTCOffset| Parse Node, then - 1. Let _offset_ be the source text matched by the |UTCOffset| Parse Node contained within _parseResult_. - 1. Set _timeZoneResult_.[[OffsetString]] to CodePointsToString(_offset_). + 1. Else if _parseResult_ contains a |UTCOffsetSubMinutePrecision| Parse Node, then + 1. Let _offset_ be the source text matched by the |UTCOffsetSubMinutePrecision| Parse Node contained within _parseResult_. + 1. Set _timeZoneResult_.[[OffsetString]] to CodePointsToString(_offset_). 1. Let _calendar_ be *undefined*. 1. Let _calendarWasCritical_ be *false*. 1. For each |Annotation| Parse Node _annotation_ contained within _parseResult_, do @@ -1682,11 +1691,21 @@

    ParseTemporalTimeZoneString ( _timeZoneString_: a String, - ) + ): either a normal completion containing a Record containing information about the time zone, or a throw completion

    description
    -
    It parses the argument as either a time zone identifier or an ISO 8601 string and returns a Record representing information about the time zone.
    +
    + It parses the argument as either a time zone identifier or an ISO 8601 string. + The returned Record's fields are set as follows: +
      +
    • If _timeZoneString_ is either a named time zone identifier or offset time zone identifier, then [[Name]] is _timeZoneString_, while [[Z]] is *false* and [[OffsetString]] is *undefined*.
    • +
    • Otherwise, if _timeZoneString_ is an ISO 8601 string with a time zone annotation containing either a named time zone identifier or offset time zone identifier, then [[Name]] is the time zone identifier contained in the annotation, while [[Z]] is *false* and [[OffsetString]] is *undefined*.
    • +
    • Otherwise, if _timeZoneString_ is an ISO 8601 string using a *Z* offset designator, then [[Z]] is *true*, while [[Name]] and [[OffsetString]] are *undefined*.
    • +
    • Otherwise, if _timeZoneString_ is an ISO 8601 string using a numeric UTC offset, then [[OffsetString]] is _timeZoneString_, while [[Z]] is *false* and [[Name]] is *undefined*.
    • +
    • Otherwise, a *RangeError* is thrown.
    • +
    +
    1. Let _parseResult_ be ParseText(StringToCodePoints(_timeZoneString_), |TimeZoneIdentifier|). diff --git a/spec/instant.html b/spec/instant.html index 299f5547ed..8df73db85f 100644 --- a/spec/instant.html +++ b/spec/instant.html @@ -532,8 +532,7 @@

    ParseTemporalInstant ( _isoString_ )

    1. Let _offsetString_ be _result_.[[TimeZoneOffsetString]]. 1. Assert: _offsetString_ is not *undefined*. 1. Let _utc_ be GetUTCEpochNanoseconds(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]], _result_.[[Hour]], _result_.[[Minute]], _result_.[[Second]], _result_.[[Millisecond]], _result_.[[Microsecond]], _result_.[[Nanosecond]]). - 1. If IsTimeZoneOffsetString(_offsetString_) is *false*, throw a *RangeError* exception. - 1. Let _offsetNanoseconds_ be ParseTimeZoneOffsetString(_offsetString_). + 1. Let _offsetNanoseconds_ be ? ParseDateTimeUTCOffset(_offsetString_).[[OffsetNanoseconds]]. 1. Let _result_ be _utc_ - ℤ(_offsetNanoseconds_). 1. If ! IsValidEpochNanoseconds(_result_) is *false*, then 1. Throw a *RangeError* exception. @@ -658,7 +657,7 @@

    TemporalInstantToString ( _instant_, _timeZone_, _precision_ )

    1. Let _timeZoneString_ be *"Z"*. 1. Else, 1. Let _offsetNs_ be ? GetOffsetNanosecondsFor(_timeZone_, _instant_). - 1. Let _timeZoneString_ be ! FormatISOTimeZoneOffsetString(_offsetNs_). + 1. Let _timeZoneString_ be FormatDateTimeUTCOffsetRounded(_offsetNs_). 1. Return the string-concatenation of _dateTimeString_ and _timeZoneString_.
    diff --git a/spec/intl.html b/spec/intl.html index 53e0579911..c89fb1f4a5 100644 --- a/spec/intl.html +++ b/spec/intl.html @@ -2542,7 +2542,8 @@

    Temporal.ZonedDateTime.prototype.toLocaleString ( [ _locales_ [ , _options_ 1. Perform ? RequireInternalSlot(_zonedDateTime_, [[InitializedTemporalZonedDateTime]]). 1. Let _dateTimeFormat_ be ! OrdinaryCreateFromConstructor(%DateTimeFormat%, %DateTimeFormat.prototype%, « [[InitializedDateTimeFormat]], [[Locale]], [[Calendar]], [[NumberingSystem]], [[TimeZone]], [[Weekday]], [[Era]], [[Year]], [[Month]], [[Day]], [[DayPeriod]], [[Hour]], [[Minute]], [[Second]], [[FractionalSecondDigits]], [[TimeZoneName]], [[HourCycle]], [[Pattern]], [[BoundFormat]] »). 1. Let _timeZone_ be ? ToTemporalTimeZoneIdentifier(_zonedDateTime_.[[TimeZone]]). - 1. If IsTimeZoneOffsetString(_timeZone_) is *true*, throw a *RangeError* exception. + 1. Let _timeZoneParseResult_ be ? ParseTimeZoneIdentifier(_timeZone_). + 1. If _timeZoneParseResult_.[[OffsetNanoseconds]] is not ~empty~, throw a *RangeError* exception. 1. Let _timeZoneIdentifierRecord_ be GetAvailableNamedTimeZoneIdentifier(_timeZone_). 1. If _timeZoneIdentifierRecord_ is ~empty~, throw a *RangeError* exception. 1. Set _timeZone_ to _timeZoneIdentifierRecord_.[[PrimaryIdentifier]]. diff --git a/spec/mainadditions.html b/spec/mainadditions.html index dc71ca178f..86ac787720 100644 --- a/spec/mainadditions.html +++ b/spec/mainadditions.html @@ -218,7 +218,7 @@

    Time Zone Identifiers

    Time zones in ECMAScript are represented by time zone identifiers, which are Strings composed entirely of code units in the inclusive interval from 0x0000 to 0x007F0x0021 to 0x007E. - Time zones supported by an ECMAScript implementation may be available named time zones, represented by the [[Identifier]] field of the Time Zone Identifier Records returned by AvailableNamedTimeZoneIdentifiers, or offset time zones, represented by Strings for which IsTimeZoneOffsetString returns *true*. + Time zones supported by an ECMAScript implementation may be available named time zones, represented by the [[Identifier]] field of the Time Zone Identifier Records returned by AvailableNamedTimeZoneIdentifiers, or offset time zones, represented by Strings for which IsTimeZoneOffsetStringIsOffsetTimeZoneIdentifier returns *true*. Time zone identifiers are compared using ASCII-case-insensitive comparisons.

    @@ -271,6 +271,275 @@

    AvailableNamedTimeZoneIdentifiers ( ): a List of Time Zone Identifier Record 1. Return _result_. + + +

    SystemTimeZoneIdentifier ( ): a String

    +
    +
    description
    +
    + It returns a String representing the host environment's current time zone, which is either a String representing a UTC offset for which IsTimeZoneOffsetString returns *true*, or a primary time zone identifiera primary time zone identifier or an offset time zone identifier. +
    +
    + + + 1. If the implementation only supports the UTC time zone, return *"UTC"*. + 1. Let _systemTimeZoneString_ be the String representing the host environment's current time zone, either: a primary time zone identifier; or an offset time zone identifier. + 1. Return _systemTimeZoneString_. + + + +

    + To ensure the level of functionality that implementations commonly provide in the methods of the Date object, it is recommended that SystemTimeZoneIdentifier return an IANA time zone name corresponding to the host environment's time zone setting, if such a thing exists. + GetNamedTimeZoneEpochNanoseconds and GetNamedTimeZoneOffsetNanoseconds must reflect the local political rules for standard time and daylight saving time in that time zone, if such rules exist. +

    +

    For example, if the host environment is a browser on a system where the user has chosen US Eastern Time as their time zone, SystemTimeZoneIdentifier returns *"America/New_York"*.

    +
    +
    + + +

    Time Zone Offset String FormatFormats

    + + +

    + ECMAScript defines a string interchange format for UTC offsets, derived from ISO 8601. + The format is described by the following grammar. + The usage of Unicode code points in this grammar is listed in . +

    +
    + + +

    + ECMAScript defines string interchange formats for UTC offsets, derived from ISO 8601. +

    +

    + These formats are described by the ISO String grammar in . + The usage of Unicode code points in this grammar is listed in . +

    +
    + +

    [...]

    + + + The grammar in this section should be deleted; it is replaced by the ISO 8601 String grammar in . + +
    + + +

    + LocalTime ( + _t_: a finite time value, + ): an integral Number +

    +
    +
    description
    +
    + It converts _t_ from UTC to local time. + The local political rules for standard time and daylight saving time in effect at _t_ should be used to determine the result in the way specified in this section. +
    +
    + + 1. Let _systemTimeZoneIdentifier_ be SystemTimeZoneIdentifier(). + 1. Let _parseResult_ be ! ParseTimeZoneIdentifier(_systemTimeZoneIdentifier_). + 1. If IsTimeZoneOffsetString(_systemTimeZoneIdentifier_) is *true*_parseResult_.[[OffsetNanoseconds]] is not ~empty~, then + 1. Let _offsetNs_ be ParseTimeZoneOffsetString(_systemTimeZoneIdentifier_)_parseResult_.[[OffsetNanoseconds]]. + 1. Else, + 1. Let _offsetNs_ be GetNamedTimeZoneOffsetNanoseconds(_systemTimeZoneIdentifier_, ℤ(ℝ(_t_) × 106)). + 1. Let _offsetMs_ be truncate(_offsetNs_ / 106). + 1. Return _t_ + 𝔽(_offsetMs_). + + +

    If political rules for the local time _t_ are not available within the implementation, the result is _t_ because SystemTimeZoneIdentifier returns *"UTC"* and GetNamedTimeZoneOffsetNanoseconds returns 0.

    +
    + +

    It is required for time zone aware implementations (and recommended for all others) to use the time zone information of the IANA Time Zone Database https://www.iana.org/time-zones/.

    +
    + +

    Two different input time values _t_UTC are converted to the same local time tlocal at a negative time zone transition when there are repeated times (e.g. the daylight saving time ends or the time zone adjustment is decreased.).

    +

    LocalTime(UTC(_t_local)) is not necessarily always equal to _t_local. Correspondingly, UTC(LocalTime(_t_UTC)) is not necessarily always equal to _t_UTC.

    +
    +
    + + +

    + UTC ( + _t_: a Number, + ): a time value +

    +
    +
    description
    +
    + It converts _t_ from local time to a UTC time value. + The local political rules for standard time and daylight saving time in effect at _t_ should be used to determine the result in the way specified in this section. +
    +
    + + 1. Let _systemTimeZoneIdentifier_ be SystemTimeZoneIdentifier(). + 1. Let _parseResult_ be ! ParseTimeZoneIdentifier(_systemTimeZoneIdentifier_). + 1. If IsTimeZoneOffsetString(_systemTimeZoneIdentifier_) is *true*_parseResult_.[[OffsetNanoseconds]] is not ~empty~, then + 1. Let _offsetNs_ be ParseTimeZoneOffsetString(_systemTimeZoneIdentifier_)_parseResult_.[[OffsetNanoseconds]]. + 1. Else, + 1. Let _possibleInstants_ be GetNamedTimeZoneEpochNanoseconds(_systemTimeZoneIdentifier_, ℝ(YearFromTime(_t_)), ℝ(MonthFromTime(_t_)) + 1, ℝ(DateFromTime(_t_)), ℝ(HourFromTime(_t_)), ℝ(MinFromTime(_t_)), ℝ(SecFromTime(_t_)), ℝ(msFromTime(_t_)), 0, 0). + 1. NOTE: The following steps ensure that when _t_ represents local time repeating multiple times at a negative time zone transition (e.g. when the daylight saving time ends or the time zone offset is decreased due to a time zone rule change) or skipped local time at a positive time zone transition (e.g. when the daylight saving time starts or the time zone offset is increased due to a time zone rule change), _t_ is interpreted using the time zone offset before the transition. + 1. If _possibleInstants_ is not empty, then + 1. Let _disambiguatedInstant_ be _possibleInstants_[0]. + 1. Else, + 1. NOTE: _t_ represents a local time skipped at a positive time zone transition (e.g. due to daylight saving time starting or a time zone rule change increasing the UTC offset). + 1. [declared="tBefore"] Let _possibleInstantsBefore_ be GetNamedTimeZoneEpochNanoseconds(_systemTimeZoneIdentifier_, ℝ(YearFromTime(_tBefore_)), ℝ(MonthFromTime(_tBefore_)) + 1, ℝ(DateFromTime(_tBefore_)), ℝ(HourFromTime(_tBefore_)), ℝ(MinFromTime(_tBefore_)), ℝ(SecFromTime(_tBefore_)), ℝ(msFromTime(_tBefore_)), 0, 0), where _tBefore_ is the largest integral Number < _t_ for which _possibleInstantsBefore_ is not empty (i.e., _tBefore_ represents the last local time before the transition). + 1. Let _disambiguatedInstant_ be the last element of _possibleInstantsBefore_. + 1. Let _offsetNs_ be GetNamedTimeZoneOffsetNanoseconds(_systemTimeZoneIdentifier_, _disambiguatedInstant_). + 1. Let _offsetMs_ be truncate(_offsetNs_ / 106). + 1. Return _t_ - 𝔽(_offsetMs_). + +

    + Input _t_ is nominally a time value but may be any Number value. + The algorithm must not limit _t_ to the time value range, so that inputs corresponding with a boundary of the time value range can be supported regardless of local UTC offset. + For example, the maximum time value is 8.64 × 1015, corresponding with *"+275760-09-13T00:00:00Z"*. + In an environment where the local time zone offset is ahead of UTC by 1 hour at that instant, it is represented by the larger input of 8.64 × 1015 + 3.6 × 106, corresponding with *"+275760-09-13T01:00:00+01:00"*. +

    +

    If political rules for the local time _t_ are not available within the implementation, the result is _t_ because SystemTimeZoneIdentifier returns *"UTC"* and GetNamedTimeZoneOffsetNanoseconds returns 0.

    + +

    It is required for time zone aware implementations (and recommended for all others) to use the time zone information of the IANA Time Zone Database https://www.iana.org/time-zones/.

    +

    + 1:30 AM on 5 November 2017 in America/New_York is repeated twice (fall backward), but it must be interpreted as 1:30 AM UTC-04 instead of 1:30 AM UTC-05. + In UTC(TimeClip(MakeDate(MakeDay(2017, 10, 5), MakeTime(1, 30, 0, 0)))), the value of _offsetMs_ is -4 × msPerHour. +

    +

    + 2:30 AM on 12 March 2017 in America/New_York does not exist, but it must be interpreted as 2:30 AM UTC-05 (equivalent to 3:30 AM UTC-04). + In UTC(TimeClip(MakeDate(MakeDay(2017, 2, 12), MakeTime(2, 30, 0, 0)))), the value of _offsetMs_ is -5 × msPerHour. +

    +
    + +

    UTC(LocalTime(_t_UTC)) is not necessarily always equal to _t_UTC. Correspondingly, LocalTime(UTC(_t_local)) is not necessarily always equal to _t_local.

    +
    +
    + +

    [...]

    + + +

    + TimeZoneString ( + _tv_: an integral Number, + ): a String +

    +
    +
    + + 1. Let _systemTimeZoneIdentifier_ be SystemTimeZoneIdentifier(). + 1. Let _parseResult_ be ! ParseTimeZoneIdentifier(_systemTimeZoneIdentifier_). + 1. If IsTimeZoneOffsetString(_systemTimeZoneIdentifier_) is *true*_parseResult_.[[OffsetNanoseconds]] is not ~empty~, then + 1. Let _offsetNs_ be ParseTimeZoneOffsetString(_systemTimeZoneIdentifier_)_parseResult_.[[OffsetNanoseconds]]. + 1. Else, + 1. Let _offsetNs_ be GetNamedTimeZoneOffsetNanoseconds(_systemTimeZoneIdentifier_, ℤ(ℝ(_tv_) × 106)). + 1. Let _offset_ be 𝔽(truncate(_offsetNs_ / 106)). + 1. If _offset_ is *+0*𝔽 or _offset_ > *+0*𝔽, then + 1. Let _offsetSign_ be *"+"*. + 1. Let _absOffset_ be _offset_. + 1. Else, + 1. Let _offsetSign_ be *"-"*. + 1. Let _absOffset_ be -_offset_. + 1. Let _offsetMin_ be ToZeroPaddedDecimalString(ℝ(MinFromTime(_absOffset_)), 2). + 1. Let _offsetHour_ be ToZeroPaddedDecimalString(ℝ(HourFromTime(_absOffset_)), 2). + 1. Let _offsetString_ be FormatOffsetTimeZoneIdentifier(_offsetNs_, ~legacy~). + 1. Let _tzName_ be an implementation-defined string that is either the empty String or the string-concatenation of the code unit 0x0020 (SPACE), the code unit 0x0028 (LEFT PARENTHESIS), an implementation-defined timezone name, and the code unit 0x0029 (RIGHT PARENTHESIS). + 1. Return the string-concatenation of _offsetSign_, _offsetHour_, _offsetMin_, and _tzName_. + 1. Return the string-concatenation of _offsetString_ and _tzName_. + +
    + + + +

    + IsTimeZoneOffsetString ( + _offsetString_: a String, + ): a Boolean +

    +
    + +

    + IsOffsetTimeZoneIdentifier ( + _offsetString_: a String, + ): a Boolean +

    +
    +
    +
    description
    +
    The return value indicates whether _offsetString_ conforms to the grammar given by |UTCOffset||TimeZoneUTCOffsetName|.
    +
    + + 1. Let _parseResult_ be ParseText(StringToCodePoints(_offsetString_), |UTCOffset||TimeZoneUTCOffsetName|). + 1. If _parseResult_ is a List of errors, return *false*. + 1. Return *true*. + +
    + + + +

    + ParseTimeZoneOffsetString ( + _offsetString_: a String + ): either a normal completion containing a Record, or a throw completion +

    +
    + +

    + ParseDateTimeUTCOffset ( + _offsetString_: a String + ): either a normal completion containing a Record, or a throw completion +

    +
    +
    +
    description
    +
    + The return value is the UTC offset, as a number of nanoseconds, that corresponds to the String _offsetString_. + [[OffsetNanoseconds]] is the UTC offset, as a number of nanoseconds, that corresponds to _offsetString_. + [[HasSubMinutePrecision]] is *true* if _offsetString_ includes seconds or sub-second units, and *false* otherwise. + If _offsetString_ is invalid, a *RangeError* is thrown. +
    +
    + + 1. Let _parseResult_ be ParseText(StringToCodePoints(_offsetString_), |UTCOffset||UTCOffsetSubMinutePrecision|). + 1. Assert: _parseResult_ is not a List of errors. + 1. If _parseResult_ is a List of errors, throw a *RangeError* exception. + 1. Let _hasSubMinutePrecision_ be *false*. + 1. Assert: _parseResult_ contains a |TemporalSign| Parse Node. + 1. Let _parsedSign_ be the source text matched by the |TemporalSign| Parse Node contained within _parseResult_. + 1. If _parsedSign_ is the single code point U+002D (HYPHEN-MINUS) or U+2212 (MINUS SIGN), then + 1. Let _sign_ be -1. + 1. Else, + 1. Let _sign_ be 1. + 1. NOTE: Applications of StringToNumber below do not lose precision, since each of the parsed values is guaranteed to be a sufficiently short string of decimal digits. + 1. Assert: _parseResult_ contains an |Hour| Parse Node. + 1. Let _parsedHours_ be the source text matched by the |Hour| Parse Node contained within _parseResult_. + 1. Let _hours_ be ℝ(StringToNumber(CodePointsToString(_parsedHours_))). + 1. If _parseResult_ does not contain a |MinuteSecond| Parse Node, then + 1. Let _minutes_ be 0. + 1. Else, + 1. Let _parsedMinutes_ be the source text matched by the first |MinuteSecond| Parse Node contained within _parseResult_. + 1. Let _minutes_ be ℝ(StringToNumber(CodePointsToString(_parsedMinutes_))). + 1. If _parseResult_ does not contain two |MinuteSecond| Parse Nodes, then + 1. Let _seconds_ be 0. + 1. Let _nanoseconds_ be 0. + 1. Else, + 1. Set _hasSubMinutePrecision_ to *true*. + 1. Let _parsedSeconds_ be the source text matched by the second |MinuteSecond| Parse Node contained within _parseResult_. + 1. Let _seconds_ be ℝ(StringToNumber(CodePointsToString(_parsedSeconds_))). + 1. If _parseResult_ contains a |TemporalDecimalFraction| Parse Node, then + 1. Let _parsedFraction_ be the source text matched by the |TemporalDecimalFraction| Parse Node contained within _parseResult_. + 1. Let _fraction_ be the string-concatenation of CodePointsToString(_parsedFraction_) and *"000000000"*. + 1. Let _nanosecondsString_ be the substring of _fraction_ from 1 to 10. + 1. Let _nanoseconds_ be ℝ(StringToNumber(_nanosecondsString_)). + 1. If _parseResult_ does not contain a |TemporalDecimalFraction| Parse Node, then + 1. Let _nanoseconds_ be 0. + 1. Else, + 1. Let _parsedFraction_ be the source text matched by the |TemporalDecimalFraction| Parse Node contained within _parseResult_. + 1. Let _fraction_ be the string-concatenation of CodePointsToString(_parsedFraction_) and *"000000000"*. + 1. Let _nanosecondsString_ be the substring of _fraction_ from 1 to 10. + 1. Let _nanoseconds_ be ℝ(StringToNumber(_nanosecondsString_)). + 1. Return _sign_ × (((_hours_ × 60 + _minutes_) × 60 + _seconds_) × 109 + _nanoseconds_). + 1. Let _offsetNanoseconds_ be _sign_ × (((_hours_ × 60 + _minutes_) × 60 + _seconds_) × 109 + _nanoseconds_). + 1. Return the Record { [[OffsetNanoseconds]]: _offsetNanoseconds_, [[HasSubMinutePrecision]]: _hasSubMinutePrecision_ }. + +
    diff --git a/spec/timezone.html b/spec/timezone.html index 5ad6a4e1a3..3e2ebb77b4 100644 --- a/spec/timezone.html +++ b/spec/timezone.html @@ -32,7 +32,10 @@

    Temporal.TimeZone ( _identifier_ )

    1. If NewTarget is *undefined*, then 1. Throw a *TypeError* exception. 1. Set _identifier_ to ? ToString(_identifier_). - 1. If IsTimeZoneOffsetString(_identifier_) is *false*, then + 1. Let _parseResult_ be ? ParseTimeZoneIdentifier(_identifier_). + 1. If _parseResult_.[[OffsetNanoseconds]] is not ~empty~, then + 1. Set _identifier_ to FormatOffsetTimeZoneIdentifier(_parseResult_.[[OffsetNanoseconds]], ~separated~). + 1. Else, 1. Let _timeZoneIdentifierRecord_ be GetAvailableNamedTimeZoneIdentifier(_identifier_). 1. If _timeZoneIdentifierRecord_ is ~empty~, throw a *RangeError* exception. 1. Set _identifier_ to _timeZoneIdentifierRecord_.[[PrimaryIdentifier]]. @@ -98,7 +101,7 @@

    get Temporal.TimeZone.prototype.id

    1. Let _timeZone_ be the *this* value. 1. Perform ? RequireInternalSlot(_timeZone_, [[InitializedTemporalTimeZone]]). - 1. If _timeZone_.[[OffsetNanoseconds]] is not ~empty~, return FormatTimeZoneOffsetString(_timeZone_.[[OffsetNanoseconds]]). + 1. If _timeZone_.[[OffsetNanoseconds]] is not ~empty~, return FormatOffsetTimeZoneIdentifier(_timeZone_.[[OffsetNanoseconds]], ~separated~). 1. Return _timeZone_.[[Identifier]].
    @@ -222,7 +225,7 @@

    Temporal.TimeZone.prototype.toString ( )

    1. Let _timeZone_ be the *this* value. 1. Perform ? RequireInternalSlot(_timeZone_, [[InitializedTemporalTimeZone]]). - 1. If _timeZone_.[[OffsetNanoseconds]] is not ~empty~, return FormatTimeZoneOffsetString(_timeZone_.[[OffsetNanoseconds]]). + 1. If _timeZone_.[[OffsetNanoseconds]] is not ~empty~, return FormatOffsetTimeZoneIdentifier(_timeZone_.[[OffsetNanoseconds]], ~separated~). 1. Return _timeZone_.[[Identifier]]. @@ -235,7 +238,7 @@

    Temporal.TimeZone.prototype.toJSON ( )

    1. Let _timeZone_ be the *this* value. 1. Perform ? RequireInternalSlot(_timeZone_, [[InitializedTemporalTimeZone]]). - 1. If _timeZone_.[[OffsetNanoseconds]] is not ~empty~, return FormatTimeZoneOffsetString(_timeZone_.[[OffsetNanoseconds]]). + 1. If _timeZone_.[[OffsetNanoseconds]] is not ~empty~, return FormatOffsetTimeZoneIdentifier(_timeZone_.[[OffsetNanoseconds]], ~separated~). 1. Return _timeZone_.[[Identifier]]. @@ -281,7 +284,7 @@

    Properties of Temporal.TimeZone Instances

    An integer for nanoseconds representing the constant offset of this time zone relative to UTC, or ~empty~ if the instance represents a named time zone. - If not ~empty~, this value must be greater than −8.64 × 1013 nanoseconds (−24 hours) and smaller than +8.64 × 1013 nanoseconds (+24 hours). + If not ~empty~, this value must be in the interval from −8.64 × 1013 (exclusive) to +8.64 × 1013 (exclusive) (i.e., strictly less than 24 hours in magnitude). @@ -319,10 +322,13 @@

    1. If _newTarget_ is not present, set _newTarget_ to %Temporal.TimeZone%. 1. Let _object_ be ? OrdinaryCreateFromConstructor(_newTarget_, *"%Temporal.TimeZone.prototype%"*, « [[InitializedTemporalTimeZone]], [[Identifier]], [[OffsetNanoseconds]] »). - 1. If IsTimeZoneOffsetString(_identifier_) is *true*, then + 1. Assert: _identifier_ is an available named time zone identifier or an offset time zone identifier. + 1. Let _parseResult_ be ! ParseTimeZoneIdentifier(_identifier_). + 1. If _parseResult_.[[OffsetNanoseconds]] is not ~empty~, then 1. Set _object_.[[Identifier]] to ~empty~. - 1. Set _object_.[[OffsetNanoseconds]] to ParseTimeZoneOffsetString(_identifier_). + 1. Set _object_.[[OffsetNanoseconds]] to _parseResult_.[[OffsetNanoseconds]]. 1. Else, + 1. Assert: _parseResult_.[[Name]] is not ~empty~. 1. Assert: GetAvailableNamedTimeZoneIdentifier(_identifier_).[[PrimaryIdentifier]] is _identifier_. 1. Set _object_.[[Identifier]] to _identifier_. 1. Set _object_.[[OffsetNanoseconds]] to ~empty~. @@ -336,8 +342,8 @@

    Although the [[Identifier]] internal slot is a String in this specification, implementations may choose to store named time zone identifiers it in any other form (for example as an enumeration or index into a List of identifier strings) as long as the String can be regenerated when needed.

    - Similar flexibility exists for the storage of the [[OffsetNanoseconds]] internal slot, which can be interchangeably represented as a 6-byte integer or as a String value that may be as long as 19 characters. - ParseTimeZoneOffsetString and FormatTimeZoneOffsetString may be used to losslessly convert one representation to the other. + Similar flexibility exists for the storage of the [[OffsetNanoseconds]] internal slot, which can be interchangeably represented as a 6-byte signed integer or as a String value that may be as long as 19 characters. + ParseTimeZoneIdentifier and FormatOffsetTimeZoneIdentifier may be used to losslessly convert one representation to the other. Implementations are free to store either or both representations.

    @@ -464,19 +470,34 @@

    - -

    FormatTimeZoneOffsetString ( _offsetNanoseconds_ )

    + +

    + FormatOffsetTimeZoneIdentifier ( + _offsetNanoseconds_: an integer, + _style_: ~legacy~ or ~separated~ + ): a String +

    +
    +
    description
    +
    + It formats a time zone offset, in nanoseconds, into a UTC offset string. + If _style_ is ~legacy~, then the output will be formatted like ±HHMM. + If _style_ is ~separated~, then the output will be formatted like ±HH:MM if _offsetNanoseconds_ represents an integer number of minutes, like ±HH:MM:SS if _offsetNanoseconds_ represents an integer number of seconds, and otherwise like ±HH:MM:SS.fff where "fff" is a sequence of at least 1 and at most 9 fractional seconds digits with no trailing zeroes. +
    +
    - 1. Assert: _offsetNanoseconds_ is an integer. 1. If _offsetNanoseconds_ ≥ 0, let _sign_ be the code unit 0x002B (PLUS SIGN); otherwise, let _sign_ be the code unit 0x002D (HYPHEN-MINUS). 1. Set _offsetNanoseconds_ to abs(_offsetNanoseconds_). - 1. Let _nanoseconds_ be _offsetNanoseconds_ modulo 109. - 1. Let _seconds_ be floor(_offsetNanoseconds_ / 109) modulo 60. - 1. Let _minutes_ be floor(_offsetNanoseconds_ / (6 × 1010)) modulo 60. - 1. Let _hours_ be floor(_offsetNanoseconds_ / (3.6 × 1012)). + 1. Let _hours_ be floor(_offsetNanoseconds_ / (3.6 × 1012)). 1. Let _h_ be ToZeroPaddedDecimalString(_hours_, 2). + 1. Let _minutes_ be floor(_offsetNanoseconds_ / (6 × 1010)) modulo 60. 1. Let _m_ be ToZeroPaddedDecimalString(_minutes_, 2). + 1. If _style_ is ~legacy~, then + 1. Assert _offsetNanoseconds_ modulo (6 × 1010) = 0. + 1. Return the string-concatenation of _sign_, _h_, and _m_. + 1. Let _seconds_ be floor(_offsetNanoseconds_ / 109) modulo 60. 1. Let _s_ be ToZeroPaddedDecimalString(_seconds_, 2). + 1. Let _nanoseconds_ be _offsetNanoseconds_ modulo 109. 1. If _nanoseconds_ ≠ 0, then 1. Let _fraction_ be ToZeroPaddedDecimalString(_nanoseconds_, 9). 1. Set _fraction_ to the longest possible substring of _fraction_ starting at position 0 and not ending with the code unit 0x0030 (DIGIT ZERO). @@ -489,21 +510,15 @@

    FormatTimeZoneOffsetString ( _offsetNanoseconds_ )

    - -

    FormatISOTimeZoneOffsetString ( _offsetNanoseconds_ )

    -

    - The abstract operation FormatISOTimeZoneOffsetString is similar to FormatTimeZoneOffsetString but rounds the offset to the nearest minute boundary, in order to produce a ±HH:MM format offset string for use in ISO 8601 strings. -

    + +

    FormatDateTimeUTCOffsetRounded ( _offsetNanoseconds_ )

    +
    +
    description
    +
    It rounds _offsetNanoseconds_ to the nearest minute boundary and formats the rounded value into a ±HH:MM format, to support available named time zones that may have sub-minute offsets.
    +
    - 1. Assert: _offsetNanoseconds_ is an integer. - 1. Set _offsetNanoseconds_ to RoundNumberToIncrement(_offsetNanoseconds_, 60 × 109, *"halfExpand"*). - 1. If _offsetNanoseconds_ ≥ 0, let _sign_ be *"+"*; otherwise, let _sign_ be *"-"*. - 1. Set _offsetNanoseconds_ to abs(_offsetNanoseconds_). - 1. Let _minutes_ be _offsetNanoseconds_ / (60 × 109) modulo 60. - 1. Let _hours_ be floor(_offsetNanoseconds_ / (3600 × 109)). - 1. Let _h_ be ToZeroPaddedDecimalString(_hours_, 2). - 1. Let _m_ be ToZeroPaddedDecimalString(_minutes_, 2). - 1. Return the string-concatenation of _sign_, _h_, the code unit 0x003A (COLON), and _m_. + 1. Set _offsetNanoseconds_ to RoundNumberToIncrement(_offsetNanoseconds_, 6 × 1010, *"halfExpand"*). + 1. Return FormatOffsetTimeZoneIdentifier(_offsetNanoseconds_, ~separated~).
    @@ -529,23 +544,6 @@

    - -

    - CanonicalizeTimeZoneOffsetString ( - _offsetString_: a String - ): a String -

    -
    -
    description
    -
    It converts _offsetString_, which must be a valid time zone offset string, into its canonical form.
    -
    - - 1. Assert: IsTimeZoneOffsetString(_offsetString_) is *true*. - 1. Let _offsetNanoseconds_ be ParseTimeZoneOffsetString(_offsetString_). - 1. Return ! FormatTimeZoneOffsetString(_offsetNanoseconds_). - -
    -

    ToTemporalTimeZoneSlotValue ( @@ -566,12 +564,15 @@

    1. Let _parseResult_ be ? ParseTemporalTimeZoneString(_identifier_). 1. If _parseResult_.[[Name]] is not *undefined*, then 1. Let _name_ be _parseResult_.[[Name]]. - 1. If IsTimeZoneOffsetString(_name_) is *true*, return CanonicalizeTimeZoneOffsetString(_name_). + 1. Let _offsetNanoseconds_ be ? ParseTimeZoneIdentifier(_name_).[[OffsetNanoseconds]]. + 1. If _offsetNanoseconds_ is not ~empty~, return FormatOffsetTimeZoneIdentifier(_offsetNanoseconds_, ~separated~). 1. Let _timeZoneIdentifierRecord_ be GetAvailableNamedTimeZoneIdentifier(_name_). 1. If _timeZoneIdentifierRecord_ is ~empty~, throw a *RangeError* exception. 1. Return _timeZoneIdentifierRecord_.[[PrimaryIdentifier]]. 1. If _parseResult_.[[Z]] is *true*, return *"UTC"*. - 1. Return CanonicalizeTimeZoneOffsetString(_parseResult_.[[OffsetString]]). + 1. Let _offsetParseResult_ be ! ParseDateTimeUTCOffset(_parseResult_.[[OffsetString]]). + 1. If _offsetParseResult_.[[HasSubMinutePrecision]] is *true*, throw a *RangeError* exception. + 1. Return FormatOffsetTimeZoneIdentifier(_offsetParseResult_.[[OffsetNanoseconds]], ~separated~). @@ -587,7 +588,7 @@

    1. If _timeZoneSlotValue_ is a String, then - 1. Assert: Either IsTimeZoneOffsetString(_timeZoneSlotValue_) is *true*, or GetAvailableNamedTimeZoneIdentifier(_timeZoneSlotValue_) is not ~empty~. + 1. Assert: Either IsOffsetTimeZoneIdentifier(_timeZoneSlotValue_) is *true*, or GetAvailableNamedTimeZoneIdentifier(_timeZoneSlotValue_) is not ~empty~. 1. Return _timeZoneSlotValue_. 1. Let _identifier_ be ? Get(_timeZoneSlotValue_, *"id"*). 1. If _identifier_ is not a String, throw a *TypeError* exception. @@ -656,7 +657,7 @@

    1. Let _offsetNanoseconds_ be ? GetOffsetNanosecondsFor(_timeZone_, _instant_). - 1. Return ! FormatTimeZoneOffsetString(_offsetNanoseconds_). + 1. Return FormatOffsetTimeZoneIdentifier(_offsetNanoseconds_, ~separated~). @@ -810,5 +811,33 @@

    1. Return *false*. + + +

    + ParseTimeZoneIdentifier ( + _identifier_: a String + ): either a normal completion containing a Record containing [[Name]] and [[OffsetNanoseconds]] fields, or a throw completion +

    +
    +
    description
    +
    + If _identifier_ is a named time zone identifier, [[Name]] will be _identifier_ and [[OffsetNanoseconds]] will be ~empty~. + If _identifier_ is an offset time zone identifier, [[Name]] will be ~empty~ and [[OffsetNanoseconds]] will be a signed integer. + Otherwise, a *RangeError* will be thrown. +
    +
    + + 1. Let _parseResult_ be ParseText(StringToCodePoints(_identifier_), |TimeZoneIdentifier|). + 1. If _parseResult_ is a List of errors, throw a *RangeError* exception. + 1. If _parseResult_ contains a |TimeZoneIANAName| Parse Node, then + 1. Let _name_ be the source text matched by the |TimeZoneIANAName| Parse Node contained within _parseResult_. + 1. Return the Record { [[Name]]: _name_, [[OffsetNanoseconds]]: ~empty~ }. + 1. Else, + 1. Assert: _parseResult_ contains a |TimeZoneUTCOffsetName| Parse Node. + 1. Let _offsetString_ be the source text matched by the |TimeZoneUTCOffsetName| Parse Node contained within _parseResult_. + 1. Let _offsetNanoseconds_ be ! ParseDateTimeUTCOffset(_offsetString_). + 1. Return the Record { [[Name]]: ~empty~, [[OffsetNanoseconds]]: _offsetNanoseconds_ }. + +
    diff --git a/spec/zoneddatetime.html b/spec/zoneddatetime.html index 06d0f2b8ac..f442e0662c 100644 --- a/spec/zoneddatetime.html +++ b/spec/zoneddatetime.html @@ -598,8 +598,7 @@

    Temporal.ZonedDateTime.prototype.with ( _temporalZonedDateTimeLike_ [ , _opt 1. Let _dateTimeResult_ be ? InterpretTemporalDateTimeFields(_calendar_, _fields_, _options_). 1. Let _offsetString_ be ! Get(_fields_, *"offset"*). 1. Assert: Type(_offsetString_) is String. - 1. If IsTimeZoneOffsetString(_offsetString_) is *false*, throw a *RangeError* exception. - 1. Let _offsetNanoseconds_ be ParseTimeZoneOffsetString(_offsetString_). + 1. Let _offsetNanoseconds_ be ? ParseDateTimeUTCOffset(_offsetString_).[[OffsetNanoseconds]]. 1. Let _timeZone_ be _zonedDateTime_.[[TimeZone]]. 1. Let _epochNanoseconds_ be ? InterpretISODateTimeOffset(_dateTimeResult_.[[Year]], _dateTimeResult_.[[Month]], _dateTimeResult_.[[Day]], _dateTimeResult_.[[Hour]], _dateTimeResult_.[[Minute]], _dateTimeResult_.[[Second]], _dateTimeResult_.[[Millisecond]], _dateTimeResult_.[[Microsecond]], _dateTimeResult_.[[Nanosecond]], ~option~, _offsetNanoseconds_, _timeZone_, _disambiguation_, _offset_, ~match exactly~). 1. Return ! CreateTemporalZonedDateTime(_epochNanoseconds_, _timeZone_, _calendar_). @@ -1189,8 +1188,7 @@

    1. Perform ? ToTemporalOverflow(_options_). 1. Let _offsetNanoseconds_ be 0. 1. If _offsetBehaviour_ is ~option~, then - 1. If IsTimeZoneOffsetString(_offsetString_) is *false*, throw a *RangeError* exception. - 1. Set _offsetNanoseconds_ to ParseTimeZoneOffsetString(_offsetString_). + 1. Set _offsetNanoseconds_ to ? ParseDateTimeUTCOffset(_offsetString_).[[OffsetNanoseconds]]. 1. Let _epochNanoseconds_ be ? InterpretISODateTimeOffset(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]], _result_.[[Hour]], _result_.[[Minute]], _result_.[[Second]], _result_.[[Millisecond]], _result_.[[Microsecond]], _result_.[[Nanosecond]], _offsetBehaviour_, _offsetNanoseconds_, _timeZone_, _disambiguation_, _offsetOption_, _matchBehaviour_). 1. Return ! CreateTemporalZonedDateTime(_epochNanoseconds_, _timeZone_, _calendar_). @@ -1252,7 +1250,7 @@

    1. Let _offsetString_ be the empty String. 1. Else, 1. Let _offsetNs_ be ? GetOffsetNanosecondsFor(_timeZone_, _instant_). - 1. Let _offsetString_ be ! FormatISOTimeZoneOffsetString(_offsetNs_). + 1. Let _offsetString_ be FormatDateTimeUTCOffsetRounded(_offsetNs_). 1. If _showTimeZone_ is *"never"*, then 1. Let _timeZoneString_ be the empty String. 1. Else,