Stage 2 Draft / September 25, 2023

Time Zone Canonicalization proposal

Time Zone Canonicalization proposal

This proposal aims to:

This specification consists of two parts:

To provide feedback on this proposal, please visit the proposal repository at https://github.com/tc39/proposal-canonical-tz.

1 Amendments to the ECMAScript® 2024 Language Specification

Editor's Note

This section lists amendments which must be made to ECMA-262, the ECMAScript® 2024 Language Specification.

This text is based on https://github.com/tc39/proposal-temporal/blob/main/spec/timezone.html, which specifies the timezone-related changes to ECMA-262 made by the Temporal proposal. Text to be added is marked like this, and text to be deleted is marked like this.

The changes below are limited to the %Temporal.TimeZone% built-in object and the abstract operations related to that object.

1.1 Temporal.TimeZone Objects

1.1.1 Temporal.TimeZone ( identifier )

This function performs the following steps when called:

  1. If NewTarget is undefined, then
    1. Throw a TypeError exception.
  2. Set identifier to ? ToString(identifier).
  3. Let parseResult be ? ParseTimeZoneIdentifier(identifier).
  4. If parseResult.[[OffsetNanoseconds]] is not empty, then
    1. Set identifier to FormatOffsetTimeZoneIdentifier(parseResult.[[OffsetNanoseconds]], separated).
  5. Else,
    1. Let timeZoneIdentifierRecord be GetAvailableNamedTimeZoneIdentifier(identifier).
    2. If timeZoneIdentifierRecord is empty, throw a RangeError exception.
    3. Set identifier to timeZoneIdentifierRecord.[[PrimaryIdentifierIdentifier]].
  6. Return ? CreateTemporalTimeZone(identifier, NewTarget).

1.1.2 Temporal.TimeZone.prototype.equals ( timeZoneLike )

This method performs the following steps when called:

  1. Let timeZone be the this value.
  2. Perform ? RequireInternalSlot(timeZone, [[InitializedTemporalTimeZone]]).
  3. Let other be ? ToTemporalTimeZoneSlotValue(timeZoneLike).
  4. Return ? TimeZoneEquals(timeZone, other).

1.1.3 Abstract operations

1.1.3.1 CreateTemporalTimeZone ( identifier [ , newTarget ] )

The abstract operation CreateTemporalTimeZone takes argument identifier (a String) and optional argument newTarget (a constructor) and returns either a normal completion containing a Temporal.TimeZone, or an abrupt completion. It creates a new Temporal.TimeZone instance and fills the internal slots with valid values. It performs the following steps when called:

  1. If newTarget is not present, set newTarget to %Temporal.TimeZone%.
  2. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.TimeZone.prototype%", « [[InitializedTemporalTimeZone]], [[Identifier]], [[OffsetNanoseconds]] »).
  3. Assert: identifier is an available named time zone identifier or an offset time zone identifier.
  4. Let parseResult be ! ParseTimeZoneIdentifier(identifier).
  5. If parseResult.[[OffsetNanoseconds]] is not empty, then
    1. Set object.[[Identifier]] to empty.
    2. Set object.[[OffsetNanoseconds]] to parseResult.[[OffsetNanoseconds]].
  6. Else,
    1. Assert: parseResult.[[Name]] is not empty.
    2. Assert: GetAvailableNamedTimeZoneIdentifier(identifier).[[PrimaryIdentifierIdentifier]] is identifier.
    3. Set object.[[Identifier]] to identifier.
    4. Set object.[[OffsetNanoseconds]] to empty.
  7. Return object.

1.1.3.2 ToTemporalTimeZoneSlotValue ( temporalTimeZoneLike )

The abstract operation ToTemporalTimeZoneSlotValue takes argument temporalTimeZoneLike (an ECMAScript value) and returns either a normal completion containing either a String or an Object, or a throw completion. It attempts to derive a value from temporalTimeZoneLike that is suitable for storing in a Temporal.ZonedDateTime's [[TimeZone]] internal slot, and returns that value if found or throws an exception if not. It performs the following steps when called:

  1. If Type(temporalTimeZoneLike) is Object, then
    1. If temporalTimeZoneLike has an [[InitializedTemporalZonedDateTime]] internal slot, then
      1. Return temporalTimeZoneLike.[[TimeZone]].
    2. If ? ObjectImplementsTemporalTimeZoneProtocol(temporalTimeZoneLike) is false, throw a TypeError exception.
    3. Return temporalTimeZoneLike.
  2. Let identifier be ? ToString(temporalTimeZoneLike).
  3. Let parseResult be ? ParseTemporalTimeZoneString(identifier).
  4. If parseResult.[[Name]] is not undefined, then
    1. Let name be parseResult.[[Name]].
    2. Let offsetNanoseconds be ? ParseTimeZoneIdentifier(name).[[OffsetNanoseconds]].
    3. If offsetNanoseconds is not empty, return FormatOffsetTimeZoneIdentifier(offsetNanoseconds, separated).
    4. Let timeZoneIdentifierRecord be GetAvailableNamedTimeZoneIdentifier(name).
    5. If timeZoneIdentifierRecord is empty, throw a RangeError exception.
    6. Return timeZoneIdentifierRecord.[[PrimaryIdentifierIdentifier]].
  5. If parseResult.[[Z]] is true, return "UTC".
  6. Let offsetParseResult be ! ParseDateTimeUTCOffset(parseResult.[[OffsetString]]).
  7. If offsetParseResult.[[HasSubMinutePrecision]] is true, throw a RangeError exception.
  8. Return FormatOffsetTimeZoneIdentifier(offsetParseResult.[[OffsetNanoseconds]], separated).

1.1.3.3 TimeZoneEquals ( one, two )

The abstract operation TimeZoneEquals takes arguments one (a String or Object) and two (a String or Object) and returns either a normal completion containing either true or false, or a throw completion. It returns true if its arguments represent time zones using the same identifierthe same time zones, either because their identifiers are equal or because those identifiers resolve to the same primary time zone identifier or UTC offset. It performs the following steps when called:

  1. If one and two are the same Object value, return true.
  2. Let timeZoneOne be ? ToTemporalTimeZoneIdentifier(one).
  3. Let timeZoneTwo be ? ToTemporalTimeZoneIdentifier(two).
  4. If timeZoneOne is timeZoneTwo, return true.
  5. Let offsetNanosecondsOne be ? ParseTimeZoneIdentifier(timeZoneOne).[[OffsetNanoseconds]].
  6. Let offsetNanosecondsTwo be ? ParseTimeZoneIdentifier(timeZoneTwo).[[OffsetNanoseconds]].
  7. If offsetNanosecondsOne is empty and offsetNanosecondsTwo is empty, then
    1. Let recordOne be GetAvailableNamedTimeZoneIdentifier(timeZoneOne).
    2. Let recordTwo be GetAvailableNamedTimeZoneIdentifier(timeZoneTwo).
    3. If recordOne is not empty and recordTwo is not empty and recordOne.[[PrimaryIdentifier]] is recordTwo.[[PrimaryIdentifier]], return true.
  8. Else,
    1. If offsetNanosecondsOne is not empty and offsetNanosecondsTwo is not empty and offsetNanosecondsOne = offsetNanosecondsTwo, return true.
  9. Return false.

2 Amendments to the ECMAScript® 2024 Internationalization API Specification

Editor's Note

This section lists amendments which must be made to ECMA-402, the ECMAScript® 2024 Internationalization API Specification.

This text is based on https://github.com/tc39/proposal-temporal/blob/main/spec/intl.html, which specifies the changes to ECMA-402 made by the Temporal proposal. Text to be added is marked like this, and text to be deleted is marked like this.

2.1 Use of the IANA Time Zone Database

Editor's Note

This proposal adds a note to the Temporal proposal's Use of the IANA Time Zone Database section, which replaces the current Time Zone Names section in ECMA-402.

One paragraph of existing text above and below the inserted note is included for context.

[...]

The IANA Time Zone Database is typically updated between five and ten times per year. These updates may add new Zone or Link names, may change Zones to Links, and may change the UTC offsets and transitions associated with any Zone. ECMAScript implementations are recommended to include updates to the IANA Time Zone Database as soon as possible. Such prompt action ensures that ECMAScript programs can accurately perform time-zone-sensitive calculations and can use newly-added available named time zone identifiers supplied by external input or the host environment.

Note

Although the IANA Time Zone Database maintainers strive for stability, in rare cases (averaging less than once per year) a Zone may be replaced by a new Zone. For example, in 2022 "Europe/Kiev" was deprecated to a Link resolving to a new "Europe/Kyiv" Zone.

To reduce disruption from renaming changes, ECMAScript implementations are encouraged to initially add the new Zone as a non-primary time zone identifier that resolves to the current primary identifier. Then, after a waiting period, implementations are recommended to promote the new Zone to a primary time zone identifier while simultaneously demoting the deprecated name to non-primary. The recommended waiting period is two years after the IANA Time Zone Database release containing the changes. This delay allows other systems, that ECMAScript programs may interact with, to be updated to recognize the new Zone.

A waiting period should only apply when a new Zone is added to replace an existing Zone. If an existing Zone and Link are swapped, then no waiting period is necessary.

If implementations revise time zone information during the lifetime of an agent, then which identifiers are supported, the primary time zone identifier associated with any identifier, and the UTC offsets and transitions associated with any Zone, must be consistent with results previously observed by that agent. Due to the complexity of supporting this requirement, it is recommended that implementations maintain a fully consistent copy of the IANA Time Zone Database for the lifetime of each agent.

[...]

2.2 Temporal.ZonedDateTime.prototype.toLocaleString ( [ locales [ , options ] ] )

This definition supersedes the definition provided in Temporal.ZonedDateTime.prototype.toLocaleString.

This method performs the following steps when called:

  1. Let zonedDateTime be the this value.
  2. Perform ? RequireInternalSlot(zonedDateTime, [[InitializedTemporalZonedDateTime]]).
  3. 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]] »).
  4. Let timeZone be ? ToTemporalTimeZoneIdentifier(zonedDateTime.[[TimeZone]]).
  5. If IsOffsetTimeZoneIdentifier(timeZone) is true, throw a RangeError exception.
  6. Let timeZoneIdentifierRecord be GetAvailableNamedTimeZoneIdentifier(timeZone).
  7. If timeZoneIdentifierRecord is empty, throw a RangeError exception.
  8. Set timeZone to timeZoneIdentifierRecord.[[PrimaryIdentifierIdentifier]].
  9. Perform ? InitializeDateTimeFormat(dateTimeFormat, locales, options, timeZone).
  10. Let calendar be ? ToTemporalCalendarIdentifier(zonedDateTime.[[Calendar]]).
  11. If calendar is not "iso8601" and not equal to dateTimeFormat.[[Calendar]], then
    1. Throw a RangeError exception.
  12. Let instant be ! CreateTemporalInstant(zonedDateTime.[[Nanoseconds]]).
  13. Return ? FormatDateTime(dateTimeFormat, instant).

2.3 InitializeDateTimeFormat ( dateTimeFormat, locales, options [ , toLocaleStringTimeZone ] )

Editor's Note

This text is based on the revisions to InitializeDateTimeFormat made by the Temporal proposal, not the current text in ECMA-402.

Only one line is changed and is marked like this. The rest of the text has been included for context, but it can be ignored.

The abstract operation InitializeDateTimeFormat accepts the arguments dateTimeFormat (which must be an object), locales, and options. It initializes dateTimeFormat as a DateTimeFormat object. If an additional toLocaleStringTimeZone argument is provided (which, if present, must be a canonical time zone name string), the time zone will be overridden and some adjustments will be made to the defaults in order to implement the behaviour of Temporal.ZonedDateTime.prototype.toLocaleString. This abstract operation functions as follows:

The following algorithm refers to the type nonterminal from UTS 35's Unicode Locale Identifier grammar.

  1. Let requestedLocales be ? CanonicalizeLocaleList(locales).
  2. Let opt be a new Record.
  3. Let matcher be ? GetOption(options, "localeMatcher", "string", « "lookup", "best fit" », "best fit").
  4. Set opt.[[localeMatcher]] to matcher.
  5. Let calendar be ? GetOption(options, "calendar", "string", undefined, undefined).
  6. If calendar is not undefined, then
    1. If calendar does not match the Unicode Locale Identifier type nonterminal, throw a RangeError exception.
  7. Set opt.[[ca]] to calendar.
  8. Let numberingSystem be ? GetOption(options, "numberingSystem", "string", undefined, undefined).
  9. If numberingSystem is not undefined, then
    1. If numberingSystem does not match the Unicode Locale Identifier type nonterminal, throw a RangeError exception.
  10. Set opt.[[nu]] to numberingSystem.
  11. Let hour12 be ? GetOption(options, "hour12", "boolean", undefined, undefined).
  12. Let hourCycle be ? GetOption(options, "hourCycle", "string", « "h11", "h12", "h23", "h24" », undefined).
  13. If hour12 is not undefined, then
    1. Set hourCycle to null.
  14. Set opt.[[hc]] to hourCycle.
  15. Let localeData be %DateTimeFormat%.[[LocaleData]].
  16. Let r be ResolveLocale(%DateTimeFormat%.[[AvailableLocales]], requestedLocales, opt, %DateTimeFormat%.[[RelevantExtensionKeys]], localeData).
  17. Set dateTimeFormat.[[Locale]] to r.[[locale]].
  18. Let resolvedCalendar be r.[[ca]].
  19. Set dateTimeFormat.[[Calendar]] to resolvedCalendar.
  20. Set dateTimeFormat.[[NumberingSystem]] to r.[[nu]].
  21. Let dataLocale be r.[[dataLocale]].
  22. Let dataLocaleData be localeData.[[<dataLocale>]].
  23. Let hcDefault be dataLocaleData.[[hourCycle]].
  24. If hour12 is true, then
    1. If hcDefault is "h11" or "h23", let hc be "h11". Otherwise, let hc be "h12".
  25. Else if hour12 is false, then
    1. If hcDefault is "h11" or "h23", let hc be "h23". Otherwise, let hc be "h24".
  26. Else,
    1. Assert: hour12 is undefined.
    2. Let hc be r.[[hc]].
    3. If hc is null, set hc to hcDefault.
  27. Let timeZone be ? Get(options, "timeZone").
  28. If timeZone is undefined, then
    1. If toLocaleStringTimeZone is present, then
      1. Set timeZone to toLocaleStringTimeZone.
    2. Else,
      1. Set timeZone to SystemTimeZoneIdentifier().
  29. Else,
    1. If toLocaleStringTimeZone is present, throw a TypeError exception.
    2. Set timeZone to ? ToString(timeZone).
    3. Let timeZoneIdentifierRecord be GetAvailableNamedTimeZoneIdentifier(timeZone).
    4. If timeZoneIdentifierRecord is empty, then
      1. Throw a RangeError exception.
    5. Set timeZone to timeZoneIdentifierRecord.[[PrimaryIdentifierIdentifier]].
  30. Set dateTimeFormat.[[TimeZone]] to timeZone.
  31. Let formatOptions be a new Record.
  32. Set formatOptions.[[hourCycle]] to hc.
  33. Let hasExplicitFormatComponents be false.
  34. For each row of Table 6, except the header row, in table order, do
    1. Let prop be the name given in the Property column of the row.
    2. If prop is "fractionalSecondDigits", then
      1. Let value be ? GetNumberOption(options, "fractionalSecondDigits", 1, 3, undefined).
    3. Else,
      1. Let values be a List whose elements are the strings given in the Values column of the row.
      2. Let value be ? GetOption(options, prop, "string", values, undefined).
    4. Set formatOptions.[[<prop>]] to value.
    5. If value is not undefined, then
      1. Set hasExplicitFormatComponents to true.
  35. Let matcher be ? GetOption(options, "formatMatcher", "string", « "basic", "best fit" », "best fit").
  36. Let dateStyle be ? GetOption(options, "dateStyle", "string", « "full", "long", "medium", "short" », undefined).
  37. Set dateTimeFormat.[[DateStyle]] to dateStyle.
  38. Let timeStyle be ? GetOption(options, "timeStyle", "string", « "full", "long", "medium", "short" », undefined).
  39. Set dateTimeFormat.[[TimeStyle]] to timeStyle.
  40. Let expandedOptions be a copy of formatOptions.
  41. Let needDefaults be true.
  42. For each element field of « "weekday", "year", "month", "day", "hour", "minute", "second" » in List order, do
    1. If expandedOptions.[[<field>]] is not undefined, then
      1. Set needDefaults to false.
  43. If needDefaults is true, then
    1. For each element field of « "year", "month", "day", "hour", "minute", "second" » in List order, do
      1. Set expandedOptions.[[<field>]] to "numeric".
  44. Let bestFormat be GetDateTimeFormatPattern(dateStyle, timeStyle, matcher, expandedOptions, dataLocaleData, hc, resolvedCalendar, hasExplicitFormatComponents).
  45. Set dateTimeFormat.[[Pattern]] to bestFormat.[[pattern]].
  46. Set dateTimeFormat.[[RangePatterns]] to bestFormat.[[rangePatterns]].
  47. For each row in Supported fields for Temporal patterns, except the header row, in table order, do
    1. Let limitedOptions be a new Record.
    2. Let needDefaults be true.
    3. Let fields be the list of fields in the Supported fields column of the row.
    4. For each field field of formatOptions, do
      1. If field is in fields, then
        1. Set needDefaults to false.
        2. Set limitedOptions.[[<field>]] to formatOptions.[[<field>]].
    5. If needDefaults is true, then
      1. Let defaultFields be the list of fields in the Default fields column of the row.
      2. If the Pattern column of the row is [[TemporalInstantPattern]], and toLocaleStringTimeZone is present, append [[timeZoneName]] to defaultFields.
      3. For each element field of defaultFields, do
        1. If field is [[timeZoneName]], then
          1. Let defaultValue be "short".
        2. Else,
          1. Let defaultValue be "numeric".
        3. Set limitedOptions.[[<field>]] to defaultValue.
    6. Let bestFormat be GetDateTimeFormatPattern(dateStyle, timeStyle, matcher, limitedOptions, dataLocaleData, hc, resolvedCalendar, hasExplicitFormatComponents).
    7. If bestFormat does not have any fields that are in fields, then
      1. Set bestFormat to null.
    8. Set dateTimeFormat's internal slot whose name is the Pattern column of the row to bestFormat.
  48. Return dateTimeFormat.

A Copyright & Software License

Copyright Notice

© 2023 Justin Grant, Richard Gibson

Software License

All Software contained in this document ("Software") is protected by copyright and is being made available under the "BSD License", included below. This Software may be subject to third party rights (rights from parties other than Ecma International), including patent rights, and no licenses under such third party rights are granted under this license even if the third party concerned is a member of Ecma International. SEE THE ECMA CODE OF CONDUCT IN PATENT MATTERS AVAILABLE AT https://ecma-international.org/memento/codeofconduct.htm FOR INFORMATION REGARDING THE LICENSING OF PATENT CLAIMS THAT ARE REQUIRED TO IMPLEMENT ECMA INTERNATIONAL STANDARDS.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  3. Neither the name of the authors nor Ecma International may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE ECMA INTERNATIONAL "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ECMA INTERNATIONAL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.