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 6, 2022
1 parent cd66806 commit 6304dc2
Show file tree
Hide file tree
Showing 2 changed files with 229 additions and 24 deletions.
217 changes: 203 additions & 14 deletions spec/mainadditions.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
<!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">
<p>
This section lists amendments which must be made to <a href="https://tc39.es/ecma262/">ECMA-262, the ECMAScript® 2023 Language Specification</a>, other than the addition of the new sections specifying the Temporal object and everything related to it.
Text to be added is marked <ins>like this</ins>, and text to be deleted is marked <del>like this</del>.
</p>
<p>
This text is based on top of the ECMA-262 spec text from commit <a href="https://github.com/tc39/ecma262/commit/fb5e39af09b926f36538bc62b87f5b7b5370f9e7">fb5e39af09b926f36538bc62b87f5b7b5370f9e7</a>.
</p>
</emu-note>

<emu-clause id="sec-literals-numeric-literals">
Expand Down Expand Up @@ -45,19 +48,205 @@ <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>

<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 id="sec-temporal-legacy-date-objects">
<h1>Date Objects</h1>

<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>
<del class="block">
<emu-alg>
1. Return _t_ + LocalTZA(_t_, *true*).
</emu-alg>
</del>
<ins class="block">
<emu-alg>
1. Let _localTimeZone_ be ! CreateTemporalTimeZone(DefaultTimeZone()).
1. If _localTimeZone_.[[OffsetNanoseconds]] is not *undefined*, then
1. Let _offsetNs_ be _localTimeZone_.[[OffsetNanoseconds]].
1. Else,
1. Let _offsetNs_ be GetIANATimeZoneOffsetNanoseconds(ℤ(ℝ(_t_) &times; 10<sup>6</sup>), _localTimeZone_.[[Identifier]]).
1. Let _offsetMs_ be RoundTowardsZero(_offsetNs_ / 10<sup>6</sup>).
1. Return _t_ + 𝔽(_offsetMs_).
</emu-alg>
</ins>
<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>
<del class="block">
<emu-alg>
1. Return _t_ - LocalTZA(_t_, *false*).
</emu-alg>
</del>
<ins class="block">
<emu-alg>
1. Let _localTimeZone_ be ! CreateTemporalTimeZone(DefaultTimeZone()).
1. If _localTimeZone_.[[OffsetNanoseconds]] is not *undefined*, then
1. Let _offsetNs_ be -_localTimeZone_.[[OffsetNanoseconds]].
1. Else,
1. Let _year_ be ℝ(YearFromTime(_t_)).
1. Let _month_ be ℝ(MonthFromTime(_t_)) + 1.
1. Let _day_ be ℝ(DateFromTime(_t_)).
1. Let _hour_ be ℝ(HourFromTime(_t_)).
1. Let _minute_ be ℝ(MinFromTime(_t_)).
1. Let _second_ be ℝ(SecFromTime(_t_)).
1. Let _millisecond_ be ℝ(msFromTime(_t_)).
1. Let _possibleInstants_ be GetIANATimeZoneEpochValue(_localTimeZone_.[[Identifier]], _year_, _month_, _day_, _hour_, _minute_, _second_, _millisecond_, 0, 0).
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. Let _epochNs_ be GetEpochFromISOParts(_year_, _month_, _day_, _hour_, _minute_, _second_, _millisecond_, 0, 0).
1. Let _dayBefore_ be _epochNs_ - ℤ(nsPerDay).
1. Let _dayAfter_ be _epochNs_ + ℤ(nsPerDay).
1. Let _offsetBefore_ be GetIANATimeZoneOffsetNanoseconds(_dayBefore_, _localTimeZone_.[[Identifier]]).
1. Let _offsetAfter_ be GetIANATimeZoneOffsetNanoseconds(_dayAfter_, _localTimeZone_.[[Identifier]]).
1. Let _nanoseconds_ be _offsetAfter_ - _offsetBefore_.
1. Let _balanceResult_ be ! BalanceISODateTime(_year_, _month_, _day_, _hour_, _minute_, _second_, _millisecond_, 0, _nanoseconds_).
1. Set _possibleInstants_ to GetIANATimeZoneEpochValue(_localTimeZone_.[[Identifier]], _balanceResult_.[[Year]], _balanceResult_.[[Month]], _balanceResult_.[[Day]], _balanceResult_.[[Hour]], _balanceResult_.[[Minute]], _balanceResult_.[[Second]], _balanceResult_.[[Millisecond]], _balanceResult_.[[Microsecond]], _balanceResult_.[[Nanosecond]]).
1. Assert: _possibleInstants_ is not empty.
1. Let _disambiguatedInstant_ be the last element of _possibleInstants_.
1. Let _offsetNs_ be GetIANATimeZoneOffsetNanoseconds(_disambiguatedInstant_ &times; ℤ(10<sup>6</sup>), _localTimeZone_.[[Identifier]]).
1. Let _offsetMs_ be RoundTowardsZero(_offsetNs_ / 10<sup>6</sup>).
1. Return _t_ - 𝔽(_offsetMs_).
</emu-alg>
</ins>
<p><ins>
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 &times; 10<sup>15</sup>, 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 &times; 10<sup>15</sup> + 3.6 &times; 10<sup>6</sup>, corresponding with *"+275760-09-13T01:00:00+01:00"*.
</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>
</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>

<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>
Loading

0 comments on commit 6304dc2

Please sign in to comment.