Skip to content

Commit

Permalink
Editorial: Redefine time zone handling in legacy Date in terms of Def…
Browse files Browse the repository at this point in the history
…aultTimeZone

The intention of this change is to ensure that it is impossible for
Temporal to support a different set of time zones than legacy Date.

Technically, legacy Date doesn't "support" any time zones — its data model
is the same as Temporal.Instant — but calls like new Date(y, m, d, ...)
interpret the given date and time as a local time in the system's current
time zone. We need to ensure that this vaguely defined "current time zone"
is conceptually the same time zone that Temporal.Now.timeZone() returns.

Legacy Date's conversion to and from local times is handled by the
LocalTZA abstract operation, which is implementation-defined. The idea
here is to push the "implementation-definedness" from LocalTZA on to other
abstract operations that are also used by Temporal.

We remove LocalTZA everywhere it is used, and instead redefine the UTC,
LocalTime, and TimeZoneString abstract operations to get the current time
zone from the operation DefaultTimeZone, which is where Temporal gets it
as well.

We also rewrite these operations to perform the same steps as Temporal
when it does the same conversions, only without any observable calls to
methods on the Temporal.TimeZone object, and without a disambiguation
parameter. (The algorithm is the same as what happens when the
disambiguation is set to "compatible", which is the default for reasons
of interoperability with legacy Date.)

This pushes the "implementation-definedness" from LocalTZA into
GetIANATimeZoneOffsetNanoseconds, GetIANATimeZoneEpochValue, and
DefaultTimeZone.

LocalTZA had a lot of explanatory text, which I've tried to move to other
sections where it makes sense to do so.

(LocalTZA was weird anyway, it performed totally different functions
depending on the value of the _isUTC_ parameter — as you can see from the
rewritten versions of LocalTime and UTC.)

This should affect absolutely nothing for implementations. It's just a
more formal guarantee of what was already stipulated.

Closes: #519
  • Loading branch information
ptomato committed May 4, 2022
1 parent 62059b9 commit b15281a
Show file tree
Hide file tree
Showing 2 changed files with 207 additions and 20 deletions.
193 changes: 182 additions & 11 deletions spec/mainadditions.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!doctype html>
<meta charset="utf8">

<emu-clause id="sec-temporal-legacy-date-objects">
<emu-clause id="sec-temporal-ecma262-amendments">
<h1>Amendments to the ECMAScript® 2023 Language Specification</h1>

<emu-note type="editor">
Expand Down Expand Up @@ -45,19 +45,190 @@ <h1>Mathematical Operations</h1>
<p>[...]</p>
</emu-clause>

<emu-clause id="sec-temporal-properties-of-the-legacy-date-prototype-object">
<h1><a href="https://tc39.es/ecma262/#sec-properties-of-the-date-prototype-object">Properties of the Date Prototype Object</a></h1>
<emu-clause id="sec-temporal-legacy-date-objects">
<h1>Date Objects</h1>

<ins class="block">
<emu-clause id="sec-date.prototype.totemporalinstant">
<h1>Date.prototype.toTemporalInstant ( )</h1>
<p>The following steps are performed:</p>
<emu-clause id="sec-temporal-overview-of-legacy-date-objects-and-definitions-of-abstract-operations">
<h1>Overview of Date Objects and Definitions of Abstract Operations</h1>
<p>The following abstract operations operate on time values (defined in <emu-xref href="#sec-time-values-and-time-range"></emu-xref>). Note that, in every case, if any argument to one of these functions is *NaN*, the result will be *NaN*.</p>

<del class="block">
<emu-clause id="sec-local-time-zone-adjustment" type="implementation-defined abstract operation">
<h1>
LocalTZA (
_t_: a Number,
_isUTC_: a Boolean,
): an integral Number
</h1>
<dl class="header">
<dt>description</dt>
<dd>Its return value represents the local time zone adjustment, or offset, in milliseconds. 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.</dd>
</dl>
<p>When _isUTC_ is true, <emu-eqn>LocalTZA( _t_<sub>UTC</sub>, true )</emu-eqn> should return the offset of the local time zone from UTC measured in milliseconds at time represented by time value <emu-eqn>_t_<sub>UTC</sub></emu-eqn>. When the result is added to <emu-eqn>_t_<sub>UTC</sub></emu-eqn>, it should yield the corresponding Number <emu-eqn>_t_<sub>local</sub></emu-eqn>.</p>
<p>When _isUTC_ is false, <emu-eqn>LocalTZA( _t_<sub>local</sub>, false )</emu-eqn> should return the offset of the local time zone from UTC measured in milliseconds at local time represented by Number <emu-eqn>_t_<sub>local</sub></emu-eqn>. When the result is subtracted from <emu-eqn>_t_<sub>local</sub></emu-eqn>, it should yield the corresponding time value <emu-eqn>_t_<sub>UTC</sub></emu-eqn>.</p>
<p>Input _t_ is nominally a time value but may be any Number value. This can occur when _isUTC_ is false and _t_<sub>local</sub> represents a time value that is already offset outside of the time value range at the range boundaries. The algorithm must not limit _t_<sub>local</sub> to the time value range, so that such inputs are supported.</p>
<p>When <emu-eqn>_t_<sub>local</sub></emu-eqn> 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 transitions (e.g. when the daylight saving time starts or the time zone offset is increased due to a time zone rule change), <emu-eqn>_t_<sub>local</sub></emu-eqn> must be interpreted using the time zone offset before the transition.</p>
<p>If an implementation does not support a conversion described above or if political rules for time _t_ are not available within the implementation, the result must be *+0*<sub>𝔽</sub>.</p>
<emu-note>
<p>It is recommended that implementations use the time zone information of the IANA Time Zone Database <a href="https://www.iana.org/time-zones/">https://www.iana.org/time-zones/</a>.</p>
<p>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. LocalTZA(TimeClip(MakeDate(MakeDay(2017, 10, 5), MakeTime(1, 30, 0, 0))), false) is <emu-eqn>-4 &times; msPerHour</emu-eqn>.</p>
<p>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). LocalTZA(TimeClip(MakeDate(MakeDay(2017, 2, 12), MakeTime(2, 30, 0, 0))), false) is <emu-eqn>-5 &times; msPerHour</emu-eqn>.</p>
<p>Local time zone offset values may be positive <i>or</i> negative.</p>
</emu-note>
</emu-clause>
</del>

<emu-clause id="sec-localtime" type="abstract operation">
<h1>
LocalTime (
_t_: a time value,
): a Number
</h1>
<dl class="header">
<dt>description</dt>
<dd>
It converts _t_ from UTC to local time.
<ins>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.</ins>
</dd>
</dl>
<emu-alg>
1. <ins>Let _localTimeZone_ be ! CreateTemporalTimeZone(DefaultTimeZone()).</ins>
1. <ins>If _localTimeZone_.[[OffsetNanoseconds]] is not *undefined*, then</ins>
1. <ins>Let _offsetNs_ be _localTimeZone_.[[OffsetNanoseconds]].</ins>
1. <ins>Else,</ins>
1. <ins>Let _offsetNs_ be GetIANATimeZoneOffsetNanoseconds(ℤ(_t_) &times; 10<sup>6</sup>, _localTimeZone_.[[Identifier]]).</ins>
1. <ins>Let _offsetMs_ be RoundTowardsZero(_offsetNs_ / 10<sup>6</sup>).</ins>
1. Return _t_ + <del>LocalTZA(_t_, *true*)</del><ins>𝔽(_offsetMs_)</ins>.
</emu-alg>
<p><ins>
If political rules for the local time _t_ are not available within the implementation, the result is equal to _t_ because DefaultTimeZone returns *"UTC"* and GetIANATimeZoneOffsetNanoseconds returns 0.
</ins></p>
<emu-note>
<p>Two different input time values <emu-eqn>_t_<sub>UTC</sub></emu-eqn> are converted to the same local time <emu-eqn>t<sub>local</sub></emu-eqn> 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.).</p>
<p><emu-eqn>LocalTime(UTC(_t_<sub>local</sub>))</emu-eqn> is not necessarily always equal to <emu-eqn>_t_<sub>local</sub></emu-eqn>. Correspondingly, <emu-eqn>UTC(LocalTime(_t_<sub>UTC</sub>))</emu-eqn> is not necessarily always equal to <emu-eqn>_t_<sub>UTC</sub></emu-eqn>.</p>
</emu-note>
</emu-clause>

<emu-clause id="sec-utc-t" type="abstract operation">
<h1>
UTC (
_t_: a Number,
): a time value
</h1>
<dl class="header">
<dt>description</dt>
<dd>
It converts _t_ from local time to a UTC time value.
<ins>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.</ins>
</dd>
</dl>
<emu-alg>
1. Let _t_ be ? thisTimeValue(*this* value).
1. Let _ns_ be ? NumberToBigInt(_t_) &times; 10<sup>6</sup>.
1. Return ! CreateTemporalInstant(_ns_).
1. <ins>Let _localTimeZone_ be ! CreateTemporalTimeZone(DefaultTimeZone()).</ins>
1. <ins>If _localTimeZone_.[[OffsetNanoseconds]] is not *undefined*, then</ins>
1. <ins>Let _offsetNs_ be -_localTimeZone_.[[OffsetNanoseconds]].</ins>
1. <ins>Else,</ins>
1. <ins>Let _year_ be ℝ(YearFromTime(_t_)).</ins>
1. <ins>Let _month_ be ℝ(MonthFromTime(_t_)) + 1.</ins>
1. <ins>Let _day_ be ℝ(DateFromTime(_t_)).</ins>
1. <ins>Let _hour_ be ℝ(HourFromTime(_t_)).</ins>
1. <ins>Let _minute_ be ℝ(MinFromTime(_t_)).</ins>
1. <ins>Let _second_ be ℝ(SecFromTime(_t_)).</ins>
1. <ins>Let _millisecond_ be ℝ(msFromTime(_t_)).</ins>
1. <ins>Let _possibleInstants_ be GetIANATimeZoneEpochValue(_localTimeZone_.[[Identifier]], _year_, _month_, _day_, _hour_, _minute_, _second_, _millisecond_, 0, 0).</ins>
1. <ins>If _possibleInstants_ is not empty, then</ins>
1. <ins>Let _disambiguatedInstant_ be _possibleInstants_[0].</ins>
1. <ins>Else,</ins>
1. <ins>Let _epochNs_ be GetEpochFromISOParts(_year_, _month_, _day_, _hour_, _minute_, _second_, _millisecond_, 0, 0).</ins>
1. <ins>Let _dayBefore_ be _epochNs_ - *8.64 &times; 10<sup>13</sup>*<sub>ℤ</sub>.</ins>
1. <ins>Let _dayAfter_ be _epochNs_ + *8.64 &times; 10<sup>13</sup>*<sub>ℤ</sub>.</ins>
1. <ins>Let _offsetBefore_ be GetIANATimeZoneOffsetNanoseconds(_dayBefore_, _localTimeZone_.[[Identifier]]).</ins>
1. <ins>Let _offsetAfter_ be GetIANATimeZoneOffsetNanoseconds(_dayAfter_, _localTimeZone_.[[Identifier]]).</ins>
1. <ins>Let _nanoseconds_ be _offsetAfter_ - _offsetBefore_.</ins>
1. <ins>Let _balanceResult_ be ! BalanceISODateTime(_year_, _month_, _day_, _hour_, _minute_, _second_, _millisecond_, 0, _nanoseconds_).</ins>
1. <ins>Set _possibleInstants_ to GetIANATimeZoneEpochValue(_localTimeZone_.[[Identifier]], _balanceResult_.[[Year]], _balanceResult_.[[Month]], _balanceResult_.[[Day]], _balanceResult_.[[Hour]], _balanceResult_.[[Minute]], _balanceResult_.[[Second]], _balanceResult_.[[Millisecond]], _balanceResult_.[[Microsecond]], _balanceResult_.[[Nanosecond]]).</ins>
1. <ins>Assert: _possibleInstants_ is not empty.</ins>
1. <ins>Let _disambiguatedInstant_ be the last element of _possibleInstants_.</ins>
1. <ins>Let _offsetNs_ be GetIANATimeZoneOffsetNanoseconds(_disambiguatedInstant_ &times; *10<sup>6</sup>*<sub>ℤ</sub>, _localTimeZone_.[[Identifier]]).</ins>
1. <ins>Let _offsetMs_ be RoundTowardsZero(_offsetNs_ / 10<sup>6</sup>).</ins>
1. Return _t_ - <del>LocalTZA(_t_, *false*)</del><ins>𝔽(_offsetMs_)</ins>.
</emu-alg>
<p><ins>
Input _t_ is nominally a time value but may be any Number value.
This can occur when _t_ represents a local time that is already offset outside of the time value range at the range boundaries.
The local time represented by _t_ would still be within the range accepted by ISODateTimeWithinLimits.
The algorithm must not limit _t_ to the time value range, so that such inputs are supported.
</ins></p>
<p><ins>
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 transitions (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.
</ins></p>
<p><ins>
If political rules for the local time _t_ are not available within the implementation, the result is equal to _t_ because DefaultTimeZone returns *"UTC"* and GetIANATimeZoneOffsetNanoseconds returns 0.
</ins></p>
<emu-note>
<p><ins>
It is recommended that implementations use the time zone information of the IANA Time Zone Database <a href="https://www.iana.org/time-zones/">https://www.iana.org/time-zones/</a>.
</ins></p>
<p><ins>
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 <emu-eqn>-4 &times; msPerHour</emu-eqn>.
</ins></p>
<p><ins>
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 <emu-eqn>-5 &times; msPerHour</emu-eqn>.
</ins></p>
</emu-note>
<emu-note>
<p><emu-eqn>UTC(LocalTime(_t_<sub>UTC</sub>))</emu-eqn> is not necessarily always equal to <emu-eqn>_t_<sub>UTC</sub></emu-eqn>. Correspondingly, <emu-eqn>LocalTime(UTC(_t_<sub>local</sub>))</emu-eqn> is not necessarily always equal to <emu-eqn>_t_<sub>local</sub></emu-eqn>.</p>
</emu-note>
</emu-clause>
</ins>
</emu-clause>

<emu-clause id="sec-temporal-properties-of-the-legacy-date-prototype-object">
<h1><a href="https://tc39.es/ecma262/#sec-properties-of-the-date-prototype-object">Properties of the Date Prototype Object</a></h1>

<emu-clause id="sec-date.prototype.tostring">
<h1>Date.prototype.toString ( )</h1>

<emu-clause id="sec-timezoneestring" type="abstract operation">
<h1>
TimeZoneString (
_tv_: a Number, but not *NaN*,
): a String
</h1>
<dl class="header">
</dl>
<emu-alg>
1. <ins>Let _localTimeZone_ be ! CreateTemporalTimeZone(DefaultTimeZone()).</ins>
1. <ins>If _localTimeZone_.[[OffsetNanoseconds]] is not *undefined*, then</ins>
1. <ins>Let _offsetNs_ be _localTimeZone_.[[OffsetNanoseconds]].</ins>
1. <ins>Else,</ins>
1. <ins>Let _offsetNs_ be GetIANATimeZoneOffsetNanoseconds(ℤ(_t_) &times; 10<sup>6</sup>, _localTimeZone_.[[Identifier]]).</ins>
1. Let _offset_ be <del>LocalTZA(_tv_, *true*)</del><ins>𝔽(RoundTowardsZero(_offsetNs_ / 10<sup>6</sup>))</ins>.
1. If _offset_ is *+0*<sub>𝔽</sub> or _offset_ &gt; *+0*<sub>𝔽</sub>, 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 _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_.
</emu-alg>
</emu-clause>
</emu-clause>

<ins class="block">
<emu-clause id="sec-date.prototype.totemporalinstant">
<h1>Date.prototype.toTemporalInstant ( )</h1>
<p>The following steps are performed:</p>
<emu-alg>
1. Let _t_ be ? thisTimeValue(*this* value).
1. Let _ns_ be ? NumberToBigInt(_t_) &times; 10<sup>6</sup>.
1. Return ! CreateTemporalInstant(_ns_).
</emu-alg>
</emu-clause>
</ins>
</emu-clause>
</emu-clause>
</emu-clause>
34 changes: 25 additions & 9 deletions spec/timezone.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,6 @@ <h1>Temporal.TimeZone Objects</h1>
<emu-clause id="sec-time-zone-names">
<h1>Time Zone Names</h1>

<emu-note type="editor">
<p>
The normative intention of this section is that an implementation must support UTC and a local time zone, which may also be UTC, or a named time zone like *"Europe/London"*, or an offset time zone like *"+01:00"*; and that the local time zone must in any case be consistent with the time zone offsets that can be determined using `Date.prototype.getTimezoneOffset()`, `Date.prototype.toString()`, and `Date.prototype.toTimeString()`.
The exact formal definition of "local time zone" is still to be determined.
</p>
</emu-note>

<p>
An ECMAScript implementation must support a number of built-in time zones.
At a minimum, implementations must support a built-in time zone named *"UTC"*.
Expand Down Expand Up @@ -74,7 +67,7 @@ <h1>
An ECMAScript implementation that includes the ECMA-402 Internationalization API must implement the CanonicalizeTimeZoneName abstract operation as specified in the ECMA-402 specification.
</p>
<p>
The minimum implementation of CanonicalizeTimeZoneName for ECMAScript implementations that do not include the ECMA-402 API, supporting only the *"UTC"* time zone, performs the following steps when called:
The minimum implementation of CanonicalizeTimeZoneName for ECMAScript implementations that do not include local political rules for any time zones, performs the following steps when called:
</p>

<emu-alg>
Expand All @@ -96,12 +89,21 @@ <h1>
An ECMAScript implementation that includes the ECMA-402 Internationalization API must implement the DefaultTimeZone abstract operation as specified in the ECMA-402 specification.
</p>
<p>
The minimum implementation of DefaultTimeZone for ECMAScript implementations that do not include the ECMA-402 API, supporting only the *"UTC"* time zone, performs the following steps when called:
The minimum implementation of DefaultTimeZone for ECMAScript implementations that do not include local political rules for any time zones, performs the following steps when called:
</p>

<emu-alg>
1. Return *"UTC"*.
</emu-alg>

<emu-note>
<p>
To ensure the level of functionality that implementations commonly provide in the methods of the Date object, DefaultTimeZone must return an IANA time zone name corresponding to the host environment's time zone setting, if such a thing exists. GetIANATimeZoneEpochValue and GetIANATimeZoneOffsetNanoseconds must reflect the local political rules for standard time and daylight saving time in that time zone.
</p>
<p>
For example, if the host environment is a browser on a system where the user has chosen US Eastern Time as their time zone, DefaultTimeZone returns *"America/New_York"*.
</p>
</emu-note>
</emu-clause>

<emu-clause id="sec-availabletimezones" type="abstract operation">
Expand Down Expand Up @@ -510,6 +512,17 @@ <h1>
1. Let _epochNanoseconds_ be GetEpochFromISOParts(_year_, _month_, _day_, _hour_, _minute_, _second_, _millisecond_, _microsecond_, _nanosecond_).
1. Return « _epochNanoseconds ».
</emu-alg>
<emu-note>
<p>
It is recommended that implementations use the time zone information of the IANA Time Zone Database <a href="https://www.iana.org/time-zones/">https://www.iana.org/time-zones/</a>.
</p>
<p>
1:30 AM on 5 November 2017 in America/New_York is repeated twice (fall backward), so GetIANATimeZoneEpochValue(*"America/New_York"*, 2017, 11, 5, 1, 30, 0, 0, 0, 0) would return a List of length 2.
</p>
<p>
2:30 AM on 12 March 2017 in America/New_York does not exist, so GetIANATimeZoneEpochValue(*"America/New_York"*, 2017, 3, 12, 2, 30, 0, 0, 0, 0) would return an empty List.
</p>
</emu-note>
</emu-clause>

<emu-clause id="sec-temporalgetianatimezoneoffsetnanoseconds" type="implementation-defined abstract operation">
Expand All @@ -534,6 +547,9 @@ <h1>
1. Assert: _timeZoneIdentifier_ is *"UTC"*.
1. Return 0.
</emu-alg>
<emu-note>
<p>Time zone offset values may be positive <i>or</i> negative.</p>
</emu-note>
</emu-clause>

<emu-clause id="sec-temporal-getianatimezonenexttransition" type="implementation-defined abstract operation">
Expand Down

0 comments on commit b15281a

Please sign in to comment.