From a20ca5319ecb8c4e1adc4827e372c6b36c8767a4 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 19 Jul 2023 00:17:27 -0700 Subject: [PATCH 1/4] Normative: Merge proposal-canonical-tz Stage 3 --- spec/intl.html | 22 ++++++++++++++++++++-- spec/timezone.html | 26 +++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/spec/intl.html b/spec/intl.html index 9185d79586..89a7e3f571 100644 --- a/spec/intl.html +++ b/spec/intl.html @@ -66,6 +66,24 @@

Use of the IANA Time Zone Database

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.

+ + +

+ 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. @@ -327,7 +345,7 @@

InitializeDateTimeFormat ( _dateTimeFormat_, _locales_, _options_ [ , _ 1. Let _timeZoneIdentifierRecord_ be GetAvailableNamedTimeZoneIdentifier(_timeZone_). 1. If the result of IsValidTimeZoneName(_timeZone_) is *false*_timeZoneIdentifierRecord_ is ~empty~, then 1. Throw a *RangeError* exception. - 1. Set _timeZone_ to CanonicalizeTimeZoneName(_timeZone_)_timeZoneIdentifierRecord_.[[PrimaryIdentifier]]. + 1. Set _timeZone_ to CanonicalizeTimeZoneName(_timeZone_)_timeZoneIdentifierRecord_.[[Identifier]]. 1. Set _dateTimeFormat_.[[TimeZone]] to _timeZone_. 1. Let _formatOptions_ be a new Record. 1. Set _formatOptions_.[[hourCycle]] to _hc_. @@ -2546,7 +2564,7 @@

Temporal.ZonedDateTime.prototype.toLocaleString ( [ _locales_ [ , _options_ 1. If _timeZoneParseResult_.[[OffsetMinutes]] 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]]. + 1. Set _timeZone_ to _timeZoneIdentifierRecord_.[[Identifier]]. 1. Perform ? InitializeDateTimeFormat(_dateTimeFormat_, _locales_, _options_, _timeZone_). 1. Let _calendar_ be ? ToTemporalCalendarIdentifier(_zonedDateTime_.[[Calendar]]). 1. If _calendar_ is not *"iso8601"* and not equal to _dateTimeFormat_.[[Calendar]], then diff --git a/spec/timezone.html b/spec/timezone.html index feabfd98e2..42d30b8148 100644 --- a/spec/timezone.html +++ b/spec/timezone.html @@ -38,7 +38,7 @@

Temporal.TimeZone ( _identifier_ )

1. Else, 1. Let _timeZoneIdentifierRecord_ be GetAvailableNamedTimeZoneIdentifier(_identifier_). 1. If _timeZoneIdentifierRecord_ is ~empty~, throw a *RangeError* exception. - 1. Set _identifier_ to _timeZoneIdentifierRecord_.[[PrimaryIdentifier]]. + 1. Set _identifier_ to _timeZoneIdentifierRecord_.[[Identifier]]. 1. Return ? CreateTemporalTimeZone(_identifier_, NewTarget). @@ -106,6 +106,18 @@

get Temporal.TimeZone.prototype.id

+ +

Temporal.TimeZone.prototype.equals ( _other_ )

+

+ This method performs the following steps when called: +

+ + 1. Let _timeZone_ be the *this* value. + 1. Perform ? RequireInternalSlot(_timeZone_, [[InitializedTemporalTimeZone]]). + 1. Return ? TimeZoneEquals(_timeZone_, _other_). + +
+

Temporal.TimeZone.prototype.getOffsetNanosecondsFor ( _instant_ )

@@ -329,7 +341,7 @@

1. Set _object_.[[OffsetMinutes]] to _parseResult_.[[OffsetMinutes]]. 1. Else, 1. Assert: _parseResult_.[[Name]] is not ~empty~. - 1. Assert: GetAvailableNamedTimeZoneIdentifier(_identifier_).[[PrimaryIdentifier]] is _identifier_. + 1. Assert: GetAvailableNamedTimeZoneIdentifier(_identifier_).[[Identifier]] is _identifier_. 1. Set _object_.[[Identifier]] to _identifier_. 1. Set _object_.[[OffsetMinutes]] to ~empty~. 1. Return _object_. @@ -559,7 +571,7 @@

1. If _offsetMinutes_ is not ~empty~, return FormatOffsetTimeZoneIdentifier(_offsetMinutes_). 1. Let _timeZoneIdentifierRecord_ be GetAvailableNamedTimeZoneIdentifier(_name_). 1. If _timeZoneIdentifierRecord_ is ~empty~, throw a *RangeError* exception. - 1. Return _timeZoneIdentifierRecord_.[[PrimaryIdentifier]]. + 1. Return _timeZoneIdentifierRecord_.[[Identifier]]. 1. If _parseResult_.[[Z]] is *true*, return *"UTC"*. 1. Let _offsetParseResult_ be ! ParseDateTimeUTCOffset(_parseResult_.[[OffsetString]]). 1. If _offsetParseResult_.[[HasSubMinutePrecision]] is *true*, throw a *RangeError* exception. @@ -809,6 +821,14 @@

1. Let _timeZoneOne_ be ? ToTemporalTimeZoneIdentifier(_one_). 1. Let _timeZoneTwo_ be ? ToTemporalTimeZoneIdentifier(_two_). 1. If _timeZoneOne_ is _timeZoneTwo_, return *true*. + 1. Let _offsetMinutesOne_ be ? ParseTimeZoneIdentifier(_timeZoneOne_).[[OffsetMinutes]]. + 1. Let _offsetMinutesTwo_ be ? ParseTimeZoneIdentifier(_timeZoneTwo_).[[OffsetMinutes]]. + 1. If _offsetMinutesOne_ is ~empty~ and _offsetMinutesTwo_ is ~empty~, then + 1. Let _recordOne_ be GetAvailableNamedTimeZoneIdentifier(_timeZoneOne_). + 1. Let _recordTwo_ be GetAvailableNamedTimeZoneIdentifier(_timeZoneTwo_). + 1. If _recordOne_ is not ~empty~ and _recordTwo_ is not ~empty~ and _recordOne_.[[PrimaryIdentifier]] is _recordTwo_.[[PrimaryIdentifier]], return *true*. + 1. Else, + 1. If _offsetMinutesOne_ is not ~empty~ and _offsetMinutesTwo_ is not ~empty~ and _offsetMinutesOne_ = _offsetMinutesTwo_, return *true*. 1. Return *false*. From 8ddb901db7453238819cbd62231b622723291e3e Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 19 Jul 2023 00:07:29 -0700 Subject: [PATCH 2/4] Docs for proposal-canonical-tz Stage 3 --- docs/ambiguity.md | 2 +- docs/strings.md | 4 +- docs/timezone.md | 154 +++++++++++++++++++++++++++++++++--------- docs/zoneddatetime.md | 38 +++++------ 4 files changed, 145 insertions(+), 53 deletions(-) diff --git a/docs/ambiguity.md b/docs/ambiguity.md index 88cfe6e233..a7d2f2288b 100644 --- a/docs/ambiguity.md +++ b/docs/ambiguity.md @@ -44,7 +44,7 @@ Temporal uses the [**IANA Time Zone Database**](https://en.wikipedia.org/wiki/Tz In some time zones, temporary offset changes happen twice each year due to **Daylight Saving Time (DST)** starting in the Spring and ending each Fall. Offsets can also change permanently due to political changes, e.g. a country switching time zones. -The TZ database is updated several times per year in response to political changes around the world. +The IANA Time Zone Database is updated several times per year in response to political changes around the world. Each update contains changes to time zone definitions. These changes usually affect only future date/time values, but occasionally fixes are made to past ranges too, for example when new historical sources are discovered about early-20th century timekeeping. diff --git a/docs/strings.md b/docs/strings.md index a9df7c0f47..44112394b5 100644 --- a/docs/strings.md +++ b/docs/strings.md @@ -234,8 +234,8 @@ To determine the `Temporal` class that should be used to parse a string, it's im Other than the few use cases detailed in the [`Temporal.PlainDateTime` documentation](plaindatetime.html), most of the time it's better to use a different type. - `Temporal.Duration` represents a period of time. Its data model is a number of years, months, days, hours, minutes, seconds, milliseconds, microseconds, and nanoseconds. -- `Temporal.TimeZone` represents an IANA time zone like `Asia/Tokyo` or (rarely) an offset time zone like `+06:00`. - Its data model is the canonical ID of the time zone, e.g. `"Asia/Tokyo"` or `"+06:00"`. +- `Temporal.TimeZone` represents a time zone in the [IANA time zone database](https://www.iana.org/time-zones), or (rarely) a numeric offset time zone. + Its data model is the identifier of the time zone, like `"Asia/Tokyo"` or `"+06:00"`. - `Temporal.Calendar` represents a calendar like Hebrew, Chinese, or the default ISO 8601 calendar. Its data model is the ID of the calendar, e.g. `"iso8601"` or `"hebrew"`. diff --git a/docs/timezone.md b/docs/timezone.md index 16bcdbc765..f68317847d 100644 --- a/docs/timezone.md +++ b/docs/timezone.md @@ -28,7 +28,7 @@ The other, more difficult, way to create a custom time zone is to create a plain The object must have at least `getOffsetNanosecondsFor()` and `getPossibleInstantsFor()` methods, and an `id` property. Any object with those three methods will return the correct output from any Temporal property or method. However, most other code will assume that custom time zones act like built-in `Temporal.TimeZone` objects. -To interoperate with libraries or other code that you didn't write, then you should implement all the other `Temporal.TimeZone` members as well: `toString()`, `toJSON()`, `getOffsetStringFor()`, `getPlainDateTimeFor()`, `getInstantFor()`, `getNextTransition()`, `getPreviousTransition()`, and `toJSON()`. +To interoperate with libraries or other code that you didn't write, then you should implement all the other `Temporal.TimeZone` members as well: `toString()`, `toJSON()`, `equals()`, `getOffsetStringFor()`, `getPlainDateTimeFor()`, `getInstantFor()`, `getNextTransition()`, `getPreviousTransition()`. The identifier of a custom time zone must consist of one or more components separated by slashes (`/`), as described in the [tzdata documentation](https://htmlpreview.github.io/?https://github.com/eggert/tz/blob/master/theory.html#naming). Each component must consist of between one and 14 characters. @@ -48,8 +48,8 @@ Valid characters are ASCII letters, `.`, `-`, and `_`. For a list of IANA time zone names, see the current version of the [IANA time zone database](https://www.iana.org/time-zones). A convenient list is also available [on Wikipedia](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), although it might not reflect the latest official status. -The string `timeZoneIdentifier` is canonicalized before being used to determine the time zone. -For example, values like `+01` will be understood to mean `+01:00`, and capitalization will be corrected. +The string `timeZoneIdentifier` is normalized before being used to determine the time zone. +For example, capitalization will be corrected to match the IANA Time Zone Database, and offsets like `+01` or `+0100` will be converted to normal form like `+01:00`. If no time zone can be determined from `timeZoneIdentifier`, then a `RangeError` is thrown. Use this constructor directly if you have a string that is known to be a correct time zone identifier. @@ -57,15 +57,23 @@ If you have an ISO 8601 date-time string, `Temporal.TimeZone.from()` is probably Example usage: + ```javascript -tz = new Temporal.TimeZone('UTC'); -tz = new Temporal.TimeZone('Africa/Cairo'); -tz = new Temporal.TimeZone('america/VANCOUVER'); -tz = new Temporal.TimeZone('Asia/Katmandu'); // alias of Asia/Kathmandu -tz = new Temporal.TimeZone('-04:00'); -tz = new Temporal.TimeZone('+0645'); -/* WRONG */ tz = new Temporal.TimeZone('local'); // => throws, not a time zone +new Temporal.TimeZone('UTC'); // => UTC +new Temporal.TimeZone('Etc/UTC'); // => Etc/UTC (Links are not followed) +new Temporal.TimeZone('Africa/Cairo'); // => Africa/Cairo +new Temporal.TimeZone('aSiA/TOKYO'); // => Asia/Tokyo (capitalization is normalized) +new Temporal.TimeZone('Asia/Kolkata'); // => Asia/Kolkata +new Temporal.TimeZone('Asia/Calcutta'); // => Asia/Calcutta (Links are not followed) +new Temporal.TimeZone('-04:00'); // => -04:00 +new Temporal.TimeZone('-0400'); // => -04:00 (offset formats are normalized) +new Temporal.TimeZone('-04'); // => -04:00 (offset formats are normalized) + +/* WRONG */ new Temporal.TimeZone('hi'); // => throws, not a time zone identifier +/* WRONG */ new Temporal.TimeZone('2020-01-13T16:31:00.06-08:00[America/Vancouver]'); + // => throws, use from() to parse time zones from ISO 8601 strings ``` + #### Difference between IANA time zones and numeric UTC offsets @@ -95,50 +103,134 @@ This static method creates a new time zone from another value. If the value is another `Temporal.TimeZone` object, or object implementing the time zone protocol, the same object is returned. If the value is another Temporal object that carries a time zone or an object with a `timeZone` property, such as `Temporal.ZonedDateTime`, the object's time zone is returned. -Any other value is converted to a string, which is expected to be either: +Any other value is required to be a string in one of the following formats: -- a string that is accepted by `new Temporal.TimeZone()`; or -- a string in the ISO 8601 format including a time zone offset part. - -Note that the ISO 8601 string can optionally be extended with an IANA time zone name in square brackets appended to it. +- A time zone identifier accepted by `new Temporal.TimeZone()`. +- A string like `2020-01-01[Asia/Tokyo]` or `2020-01-01T00:00+09:00[Asia/Tokyo]` in ISO 8601 format with a time zone identifier suffix in square brackets. + When a time zone identifier suffix is present, any UTC offset outside the brackets will be ignored. +- An ISO 8601 string like `2020-01-01T00:00+09:00` that includes a numeric time zone offset. +- An ISO 8601 string like `2020-01-01T00:00Z` that uses the Z offset designator. + Such strings will result in a `Temporal.TimeZone` object with the identifier `"UTC"`. This function is often more convenient to use than `new Temporal.TimeZone()` because it handles a wider range of input. Usage examples: + ```javascript // IANA time zone names and UTC offsets -tz = Temporal.TimeZone.from('UTC'); -tz = Temporal.TimeZone.from('Africa/Cairo'); -tz = Temporal.TimeZone.from('america/VANCOUVER'); -tz = Temporal.TimeZone.from('Asia/Katmandu'); // alias of Asia/Kathmandu -tz = Temporal.TimeZone.from('-04:00'); -tz = Temporal.TimeZone.from('+0645'); - -// ISO 8601 string with time zone offset part -tz = Temporal.TimeZone.from('2020-01-14T00:31:00.065858086Z'); -tz = Temporal.TimeZone.from('2020-01-13T16:31:00.065858086-08:00'); -tz = Temporal.TimeZone.from('2020-01-13T16:31:00.065858086-08:00[America/Vancouver]'); +Temporal.TimeZone.from('UTC'); // => UTC +Temporal.TimeZone.from('Etc/UTC'); // => Etc/UTC (Links are not followed) +Temporal.TimeZone.from('Africa/Cairo'); // => Africa/Cairo +Temporal.TimeZone.from('aSiA/TOKYO'); // => Asia/Tokyo (capitalization is normalized) +Temporal.TimeZone.from('Asia/Kolkata'); // => Asia/Kolkata +Temporal.TimeZone.from('Asia/Calcutta'); // => Asia/Calcutta (Links are not followed) +Temporal.TimeZone.from('-04:00'); // => -04:00 +Temporal.TimeZone.from('-0400'); // => -04:00 (offset formats are normalized) +Temporal.TimeZone.from('-04'); // => -04:00 (offset formats are normalized) + +// ISO 8601 string with bracketed time zone identifier +Temporal.TimeZone.from('2020-01-13T16:31:00.06+09:00[Asia/Tokyo]'); // => Asia/Tokyo +Temporal.TimeZone.from('2020-01-14T00:31:00.06Z[Asia/Tokyo]'); // => Asia/Tokyo +Temporal.TimeZone.from('2020-01-13T16:31:00.06+09:00[+09:00]'); // => +09:00 + +// ISO 8601 string with only a time zone offset part +Temporal.TimeZone.from('2020-01-14T00:31:00.065858086Z'); // => UTC +Temporal.TimeZone.from('2020-01-13T16:31:00.065858086-08:00'); // => -08:00 // Existing TimeZone object -tz2 = Temporal.TimeZone.from(tz); +Temporal.TimeZone.from(Temporal.TimeZone.from('Asia/Tokyo')); // => Asia/Tokyo -/* WRONG */ tz = Temporal.TimeZone.from('local'); // => throws, not a time zone -/* WRONG */ tz = Temporal.TimeZone.from('2020-01-14T00:31:00'); // => throws, ISO 8601 string without time zone offset part -/* WRONG */ tz = Temporal.TimeZone.from('-08:00[America/Vancouver]'); // => throws, ISO 8601 string without date-time part +/* WRONG */ tz = Temporal.TimeZone.from('local'); // => throws, not a time zone +/* WRONG */ tz = Temporal.TimeZone.from('2020-01-14T00:31'); // => throws, no time zone +/* WRONG */ tz = Temporal.TimeZone.from('-08:00[Asia/Aden]'); // => throws, no date/time ``` + ## Properties ### timeZone.**id** : string The `id` property gives an unambiguous identifier for the time zone. -Effectively, this is the canonicalized version of whatever `timeZoneIdentifier` was passed as a parameter to the constructor. +This is the normalized version of whatever `timeZoneIdentifier` was passed as a parameter to the constructor. When subclassing `Temporal.TimeZone`, this property must be overridden to provide an identifier for the custom time zone. ## Methods +### timeZone.**equals**(_other_: Temporal.TimeZone | object | string) : boolean + +**Parameters:** + +- `other` (`Temporal.TimeZone` object, object implementing the `Temporal.TimeZone` protocol, or a string time zone identifier): Another time zone to compare. + +**Returns:** `true` if `timeZone` and `other` are equal, or `false` if not. + +Compares two time zones for equality. +Equality is determined by the following algorithm: + +- If `timeZone === other`, then the time zones are equal. +- Otherwise, `timeZone.id` is compared to `other` (or `other.id` if `other` is an object). + If any of the following conditions are true, then the time zones are equal: + - Both string identifiers are Zone or Link names in the [IANA Time Zone Database](https://www.iana.org/time-zones), and they resolve to the same Zone name. + This resolution is case-insensitive. + - Both string identifiers are custom time zone identifiers that are equal according to `===`. + This comparison is case-sensitive and does not normalize Unicode characters. + - Both identifiers are numeric offset time zone identifiers like "+05:30", and they represent the same offset. +- Otherwise, the time zones are not equal. + +Time zones that resolve to different Zones in the IANA Time Zone Database are not equal, even if those Zones use the same offsets. +Similarly, a numeric-offset identifier is never equal to a named time zone in the IANA Time Zone Database, even if they represent the same offsets. + +Although there may be slight variation between implementations, ECMAScript implementations generally build the IANA Time Zone Database using build options that guarantee at least one Zone for every ISO 3166-1 Alpha-2 country code. +This behavior differs from the default build options of the IANA Time Zone Database where a Zone may span multiple countries that have shared the same UTC offsets and transitions since 1970, for example Europe/Oslo, Europe/Stockholm, Europe/Copenhagen, and Europe/Berlin. +To avoid conflating different countries' time zones that may vary in the future, these default build options are discouraged, and in practice ECMAScript implementations do not use them. + +Example usage: + +```javascript +kolkata = Temporal.TimeZone.from('Asia/Kolkata'); +kolkata.id; // => "Asia/Kolkata" +calcutta = Temporal.TimeZone.from('Asia/Calcutta'); +calcutta.id; // => "Asia/Calcutta" +kolkata.equals(calcutta); // => true +kolkata.equals('Asia/Calcutta'); // => true +kolkata.equals('Asia/Colombo'); // => false + +// IANA Time Zone Database identifiers are case insensitive +kolkata.equals('asia/calcutta'); // => true + +// Offset time zones are never equal to named time zones +kolkata.equals('+05:30'); // => false +zeroOffset = Temporal.TimeZone.from('+00:00'); +zeroOffset.equals('UTC'); // false + +// For offset time zones, any valid format is accepted +zeroOffset.equals('+00:00'); // => true +zeroOffset.equals('+0000'); // => true +zeroOffset.equals('+00'); // => true + +// Custom time zone identifiers are compared case-sensitively +class Custom1 extends Temporal.TimeZone { + constructor() { + super('UTC'); + } + get id() { + return 'Moon/Cheese'; + } +} +class Custom2 extends Temporal.TimeZone { + constructor() { + super('UTC'); + } + get id() { + return 'Moon/CHEESE'; + } +} +new Custom1().equals(new Custom1()); // => true +new Custom1().equals(new Custom2()); // => false +``` + ### timeZone.**getOffsetNanosecondsFor**(_instant_: Temporal.Instant | string) : number **Parameters:** diff --git a/docs/zoneddatetime.md b/docs/zoneddatetime.md index 84237016d9..e244740f3f 100644 --- a/docs/zoneddatetime.md +++ b/docs/zoneddatetime.md @@ -421,14 +421,16 @@ If `zonedDateTime` was created with a custom time zone object, this gives the `i By storing its time zone, `Temporal.ZonedDateTime` is able to use that time zone when deriving other values, e.g. to automatically perform DST adjustment when adding or subtracting time. -If a non-canonical time zone ID is used, it will be normalized by `Temporal` into its canonical name listed in the [IANA time zone database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). - Usually, the time zone ID will be an IANA time zone ID. However, in unusual cases, a time zone can also be created from a time zone offset string like `+05:30`. Offset time zones function just like IANA time zones except that their offset can never change due to DST or political changes. This can be problematic for many use cases because by using an offset time zone you lose the ability to safely derive past or future dates because, even in time zones without DST, offsets sometimes change for political reasons (e.g. countries change their time zone). Therefore, using an IANA time zone is recommended wherever possible. +Time zone identifiers are normalized before being used to determine the time zone. +For example, capitalization will be corrected to match the [IANA time zone database](https://www.iana.org/time-zones), and offsets like `+01` or `+0100` will be converted to `+01:00`. +Link names in the IANA Time Zone Database are not resolved to Zone names. + In very rare cases, you may choose to use `UTC` as your time zone ID. This is generally not advised because no humans actually live in the UTC time zone; it's just for computers. Also, UTC has no DST and always has a zero offset, which means that any action you'd take with `Temporal.ZonedDateTime` would return identical results to the same action on `Temporal.PlainDateTime` or `Temporal.Instant`. @@ -447,22 +449,20 @@ Usage example: zdt = Temporal.ZonedDateTime.from('1995-12-07T03:24-08:00[America/Los_Angeles]'); `Time zone is: ${zdt.timeZoneId}`; // => 'Time zone is: America/Los_Angeles' -zdt.withTimeZone('Asia/Singapore').timeZoneId; - // => Asia/Singapore -zdt.withTimeZone('Asia/Chongqing').timeZoneId; - // => Asia/Shanghai - // (time zone IDs are normalized, e.g. Asia/Chongqing -> Asia/Shanghai) +zdt.withTimeZone('Asia/Kolkata').timeZoneId; + // => Asia/Kolkata +zdt.withTimeZone('Asia/Calcutta').timeZoneId; + // => Asia/Calcutta (does not follow links in the IANA Time Zone Database) + +zdt.withTimeZone('europe/paris').timeZoneId; + // => Europe/Paris (normalized to match IANA Time Zone Database capitalization) + zdt.withTimeZone('+05:00').timeZoneId; // => +05:00 zdt.withTimeZone('+05').timeZoneId; - // => +05:00 - // (normalized to canonical form) -zdt.withTimeZone('utc').timeZoneId; - // => UTC - // (normalized to canonical form which is uppercase) -zdt.withTimeZone('GMT').timeZoneId; - // => UTC - // (normalized to canonical form) + // => +05:00 (normalized to ±HH:MM) +zdt.withTimeZone('+0500').timeZoneId; + // => +05:00 (normalized to ±HH:MM) ``` @@ -1072,8 +1072,8 @@ To calculate the difference between calendar dates only, use `.toPlainDate().unt To calculate the difference between clock times only, use `.toPlainTime().until(other.toPlainTime())`. If the other `Temporal.ZonedDateTime` is in a different time zone, then the same days can be different lengths in each time zone, e.g. if only one of them observes DST. -Therefore, a `RangeError` will be thrown if `largestUnit` is `'day'` or larger and the two instances' time zones have different `id` fields. -To work around this limitation, transform one of the instances to the other's time zone using `.withTimeZone(other.timeZone)` and then calculate the same-timezone difference. +Therefore, a `RangeError` will be thrown if `largestUnit` is `'day'` or larger and the two instances' time zones are not equal, using the same equality algorithm as `Temporal.TimeZone.prototype.equals`. +To work around this same-time-zone requirement, transform one of the instances to the other's time zone using `.withTimeZone(other.timeZone)` and then calculate the same-timezone difference. Because of the complexity and ambiguity involved in cross-timezone calculations involving days or larger units, `'hour'` is the default for `largestUnit`. Take care when using milliseconds, microseconds, or nanoseconds as the largest unit. @@ -1380,8 +1380,8 @@ console.log(str); // { // "id": 311, // "name": "FictionalConf 2018", -// "openingZonedDateTime": "2018-07-06T10:00+05:30[Asia/Calcutta]", -// "closingZonedDateTime": "2018-07-08T18:15+05:30[Asia/Calcutta]" +// "openingZonedDateTime": "2018-07-06T10:00+05:30[Asia/Kolkata]", +// "closingZonedDateTime": "2018-07-08T18:15+05:30[Asia/Kolkata]" // } // To rebuild from the string: From bd23ac59afe0e3c8591ceb275c386060db1f8e19 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 19 Jul 2023 00:49:01 -0700 Subject: [PATCH 3/4] Polyfill: Implement proposal-canonical-tz Stage 3 --- polyfill/index.d.ts | 1 + polyfill/lib/ecmascript.mjs | 20 ++++++++++++++++++-- polyfill/lib/intl.mjs | 2 +- polyfill/lib/timezone.mjs | 7 ++++++- polyfill/lib/zoneddatetime.mjs | 2 +- 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/polyfill/index.d.ts b/polyfill/index.d.ts index 784677c9ce..b11758ea38 100644 --- a/polyfill/index.d.ts +++ b/polyfill/index.d.ts @@ -1138,6 +1138,7 @@ export namespace Temporal { static from(timeZone: TimeZoneLike): Temporal.TimeZone | TimeZoneProtocol; constructor(timeZoneIdentifier: string); readonly id: string; + equals(timeZone: TimeZoneLike): boolean; getOffsetNanosecondsFor(instant: Temporal.Instant | string): number; getOffsetStringFor(instant: Temporal.Instant | string): string; getPlainDateTimeFor(instant: Temporal.Instant | string, calendar?: CalendarLike): Temporal.PlainDateTime; diff --git a/polyfill/lib/ecmascript.mjs b/polyfill/lib/ecmascript.mjs index ded5447f85..e9f24c4463 100644 --- a/polyfill/lib/ecmascript.mjs +++ b/polyfill/lib/ecmascript.mjs @@ -2132,7 +2132,7 @@ export function ToTemporalTimeZoneSlotValue(temporalTimeZoneLike) { const record = GetAvailableNamedTimeZoneIdentifier(tzName); if (!record) throw new RangeError(`Unrecognized time zone ${tzName}`); - return record.primaryIdentifier; + return record.identifier; } if (z) return 'UTC'; // if !tzName && !z then offset must be present @@ -2160,7 +2160,23 @@ export function TimeZoneEquals(one, two) { if (one === two) return true; const tz1 = ToTemporalTimeZoneIdentifier(one); const tz2 = ToTemporalTimeZoneIdentifier(two); - return tz1 === tz2; + if (tz1 === tz2) return true; + const offsetMinutes1 = ParseTimeZoneIdentifier(tz1).offsetMinutes; + const offsetMinutes2 = ParseTimeZoneIdentifier(tz2).offsetMinutes; + if (offsetMinutes1 === undefined && offsetMinutes2 === undefined) { + // Calling GetAvailableNamedTimeZoneIdentifier is costly, so (unlike the + // spec) the polyfill will early-return if one of them isn't recognized. Try + // the second ID first because it's more likely to be unknown, because it + // can come from the argument of TimeZone.p.equals as opposed to the first + // ID which comes from the receiver. + const idRecord2 = GetAvailableNamedTimeZoneIdentifier(tz2); + if (!idRecord2) return false; + const idRecord1 = GetAvailableNamedTimeZoneIdentifier(tz1); + if (!idRecord1) return false; + return idRecord1.primaryIdentifier === idRecord2.primaryIdentifier; + } else { + return offsetMinutes1 === offsetMinutes2; + } } export function TemporalDateTimeToDate(dateTime) { diff --git a/polyfill/lib/intl.mjs b/polyfill/lib/intl.mjs index 9d78ee4301..d86269a5a9 100644 --- a/polyfill/lib/intl.mjs +++ b/polyfill/lib/intl.mjs @@ -139,7 +139,7 @@ Object.defineProperty(DateTimeFormat, 'prototype', { function resolvedOptions() { const resolved = this[ORIGINAL].resolvedOptions(); - resolved.timeZone = this[TZ_CANONICAL]; + resolved.timeZone = this[TZ_ORIGINAL]; return resolved; } diff --git a/polyfill/lib/timezone.mjs b/polyfill/lib/timezone.mjs index 83e40d30eb..86ea38c448 100644 --- a/polyfill/lib/timezone.mjs +++ b/polyfill/lib/timezone.mjs @@ -28,7 +28,7 @@ export class TimeZone { } else { const record = ES.GetAvailableNamedTimeZoneIdentifier(stringIdentifier); if (!record) throw new RangeError(`Invalid time zone identifier: ${stringIdentifier}`); - stringIdentifier = record.primaryIdentifier; + stringIdentifier = record.identifier; } CreateSlots(this); SetSlot(this, TIMEZONE_ID, stringIdentifier); @@ -46,6 +46,11 @@ export class TimeZone { if (!ES.IsTemporalTimeZone(this)) throw new TypeError('invalid receiver'); return GetSlot(this, TIMEZONE_ID); } + equals(other) { + if (!ES.IsTemporalTimeZone(this)) throw new TypeError('invalid receiver'); + const timeZoneSlotValue = ES.ToTemporalTimeZoneSlotValue(other); + return ES.TimeZoneEquals(this, timeZoneSlotValue); + } getOffsetNanosecondsFor(instant) { if (!ES.IsTemporalTimeZone(this)) throw new TypeError('invalid receiver'); instant = ES.ToTemporalInstant(instant); diff --git a/polyfill/lib/zoneddatetime.mjs b/polyfill/lib/zoneddatetime.mjs index cbd4f8c8ce..ed1a41437d 100644 --- a/polyfill/lib/zoneddatetime.mjs +++ b/polyfill/lib/zoneddatetime.mjs @@ -478,7 +478,7 @@ export class ZonedDateTime { } else { const record = ES.GetAvailableNamedTimeZoneIdentifier(timeZoneIdentifier); if (!record) throw new RangeError(`toLocaleString formats built-in time zones, not ${timeZoneIdentifier}`); - optionsCopy.timeZone = record.primaryIdentifier; + optionsCopy.timeZone = record.identifier; } const formatter = new DateTimeFormat(locales, optionsCopy); From 1930b00fc1812b633a3bf7d4d296b836efdc4406 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Wed, 19 Jul 2023 00:52:53 -0700 Subject: [PATCH 4/4] Update Test262 for proposal-canonical-tz Stage 3 --- polyfill/test262 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polyfill/test262 b/polyfill/test262 index 6f146e6f30..29dde1ce0e 160000 --- a/polyfill/test262 +++ b/polyfill/test262 @@ -1 +1 @@ -Subproject commit 6f146e6f30390ac87d2b6b0198639d8c2ebbdf91 +Subproject commit 29dde1ce0e97a8bd6423c4397b9d3b51df0a1d8e