Skip to content

Commit

Permalink
Add TimeZone.getPossibleAbsolutesFor
Browse files Browse the repository at this point in the history
This method returns an array of 0, 1, or 2 Temporal.Absolute objects
that may correspond to a particular Temporal.DateTime in a particular
Temporal.TimeZone.
  • Loading branch information
ptomato committed May 20, 2020
1 parent 811bc82 commit 0ed6991
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 2 deletions.
13 changes: 12 additions & 1 deletion docs/ambiguity.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ tz.getAbsoluteFor(dt, { disambiguation: 'earlier' }).inTimeZone(tz); // => 2019
tz.getAbsoluteFor(dt, { disambiguation: 'later' }).inTimeZone(tz); // => 2019-03-31T03:45
```

Using [`Temporal.TimeZone.prototype.getPossibleAbsolutesFor`](./timezone.html#getPossibleAbsolutesFor) we can show that the wall-clock time doesn't exist:

```javascript
tz.getPossibleAbsolutesFor(dt); // => []
```

Likewise, at the end of DST, clocks move backward an hour.
In this case, the illusion is that an hour repeats itself.
In `earlier` mode, the absolute time will be the earlier instance of the duplicated wall-clock time.
Expand All @@ -56,6 +62,11 @@ dt.inTimeZone(tz, { disambiguation: 'later' }); // => 2019-02-17T02:45Z
dt.inTimeZone(tz, { disambiguation: 'reject' }); // throws
```

In this example, the wall-clock time 23:45 exists twice.
In this example, the wall-clock time 23:45 exists twice, which can also be verified with `getPossibleAbsolutesFor`:

```javascript
tz.getPossibleAbsolutesFor(dt) // => [2019-02-17T01:45Z, 2019-02-17T02:45Z]
```

> *Compatibility Note*: The built-in behaviour of the Moment Timezone and Luxon libraries is to give the same result as `earlier` when turning the clock back, and `later` when setting the clock forward.
This disambiguation behaviour isn't available by default in Temporal, but you can implement it yourself using `getPossibleAbsolutesFor`.
15 changes: 15 additions & 0 deletions docs/timezone.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,21 @@ For usage examples and a more complete explanation of how this disambiguation wo

If the result is outside the range that `Temporal.Absolute` can represent, then a `RangeError` will be thrown, no matter the value of `disambiguation`.

### timeZone.**getPossibleAbsolutesFor**(_dateTime_: Temporal.DateTime) : array<Temporal.Absolute>

**Parameters:**
- `dateTime` (`Temporal.DateTime`): A calendar date and wall-clock time to convert.

**Returns:** An array of 0, 1, or 2 `Temporal.Absolute` objects.

This method returns an array of all the possible absolute times that could correspond to the calendar date and wall-clock time indicated by `dateTime`.

Normally there is only one possible absolute time corresponding to a wall-clock time, but around a daylight saving change, a wall-clock time may not exist, or the same wall-clock time may exist twice in a row.
See [Resolving ambiguity](./ambiguity.md) for usage examples and a more complete explanation.

Usually you won't have to use this method; `Temporal.TimeZone.prototype.getAbsoluteFor()` will be more convenient for most use cases.
This method is useful for implementing a custom time zone or custom disambiguation behaviour.

### timeZone.**getTransitions**(_startingPoint_: Temporal.Absolute) : iterator<Temporal.Absolute>

**Parameters:**
Expand Down
19 changes: 19 additions & 0 deletions polyfill/lib/timezone.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,25 @@ export class TimeZone {
}
}
}
getPossibleAbsolutesFor(dateTime) {
if (!ES.IsTemporalTimeZone(this)) throw new TypeError('invalid receiver');
if (!ES.IsTemporalDateTime(dateTime)) throw new TypeError('invalid DateTime object');
const Absolute = GetIntrinsic('%Temporal.Absolute%');
const { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = dateTime;
const possibleEpochNs = ES.GetTimeZoneEpochValue(
GetSlot(this, IDENTIFIER),
year,
month,
day,
hour,
minute,
second,
millisecond,
microsecond,
nanosecond
);
return possibleEpochNs.map((ns) => new Absolute(ns));
}
getTransitions(startingPoint) {
if (!ES.IsTemporalTimeZone(this)) throw new TypeError('invalid receiver');
if (!ES.IsTemporalAbsolute(startingPoint)) throw new TypeError('invalid Absolute object');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (C) 2020 Igalia, S.L. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.

/*---
esid: sec-temporal.timezone.prototype.getpossibleabsolutesfor
---*/

const timeZone = Temporal.TimeZone.from("UTC");
assert.throws(TypeError, () => timeZone.getPossibleAbsolutesFor(undefined), "undefined");
assert.throws(TypeError, () => timeZone.getPossibleAbsolutesFor(null), "null");
assert.throws(TypeError, () => timeZone.getPossibleAbsolutesFor(true), "boolean");
assert.throws(TypeError, () => timeZone.getPossibleAbsolutesFor("2020-01-02T12:34:56Z"), "string");
assert.throws(TypeError, () => timeZone.getPossibleAbsolutesFor(Symbol()), "Symbol");
assert.throws(TypeError, () => timeZone.getPossibleAbsolutesFor(5), "number");
assert.throws(TypeError, () => timeZone.getPossibleAbsolutesFor(5n), "bigint");
assert.throws(TypeError, () => timeZone.getPossibleAbsolutesFor({}), "plain object");
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (C) 2020 Igalia, S.L. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.

const getPossibleAbsolutesFor = Temporal.TimeZone.prototype.getPossibleAbsolutesFor;

assert.sameValue(typeof getPossibleAbsolutesFor, "function");

assert.throws(TypeError, () => getPossibleAbsolutesFor.call(undefined), "undefined");
assert.throws(TypeError, () => getPossibleAbsolutesFor.call(null), "null");
assert.throws(TypeError, () => getPossibleAbsolutesFor.call(true), "true");
assert.throws(TypeError, () => getPossibleAbsolutesFor.call(""), "empty string");
assert.throws(TypeError, () => getPossibleAbsolutesFor.call(Symbol()), "symbol");
assert.throws(TypeError, () => getPossibleAbsolutesFor.call(1), "1");
assert.throws(TypeError, () => getPossibleAbsolutesFor.call({}), "plain object");
assert.throws(TypeError, () => getPossibleAbsolutesFor.call(Temporal.TimeZone), "Temporal.TimeZone");
assert.throws(TypeError, () => getPossibleAbsolutesFor.call(Temporal.TimeZone.prototype), "Temporal.TimeZone.prototype");
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (C) 2020 Igalia, S.L. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.

/*---
includes: [propertyHelper.js]
---*/

const { TimeZone } = Temporal;
assert.sameValue(
typeof TimeZone.prototype.getPossibleAbsolutesFor,
"function",
"`typeof TimeZone.prototype.getPossibleAbsolutesFor` is `function`"
);

verifyProperty(TimeZone.prototype, "getPossibleAbsolutesFor", {
writable: true,
enumerable: false,
configurable: true,
});
27 changes: 26 additions & 1 deletion polyfill/test/timezone.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Pretty from '@pipobscure/demitasse-pretty';
const { reporter } = Pretty;

import { strict as assert } from 'assert';
const { equal, notEqual, throws } = assert;
const { deepEqual, equal, notEqual, throws } = assert;

import * as Temporal from 'tc39-temporal';

Expand All @@ -30,6 +30,8 @@ describe('TimeZone', () => {
equal(typeof Temporal.TimeZone.prototype.getDateTimeFor, 'function'));
it('Temporal.TimeZone.prototype has getAbsoluteFor', () =>
equal(typeof Temporal.TimeZone.prototype.getAbsoluteFor, 'function'));
it('Temporal.TimeZone.prototype has getPossibleAbsolutesFor', () =>
equal(typeof Temporal.TimeZone.prototype.getPossibleAbsolutesFor, 'function'));
it('Temporal.TimeZone.prototype has getTransitions', () =>
equal(typeof Temporal.TimeZone.prototype.getTransitions, 'function'));
it('Temporal.TimeZone.prototype has toString', () =>
Expand Down Expand Up @@ -230,6 +232,29 @@ describe('TimeZone', () => {
);
});
});
describe('getPossibleAbsolutesFor', () => {
it('with constant offset', () => {
const zone = Temporal.TimeZone.from('+03:30');
const dt = Temporal.DateTime.from('2019-02-16T23:45');
deepEqual(
zone.getPossibleAbsolutesFor(dt).map((a) => `${a}`),
['2019-02-16T20:15Z']
);
});
it('with clock moving forward', () => {
const zone = Temporal.TimeZone.from('Europe/Berlin');
const dt = Temporal.DateTime.from('2019-03-31T02:45');
deepEqual(zone.getPossibleAbsolutesFor(dt), []);
});
it('with clock moving backward', () => {
const zone = Temporal.TimeZone.from('America/Sao_Paulo');
const dt = Temporal.DateTime.from('2019-02-16T23:45');
deepEqual(
zone.getPossibleAbsolutesFor(dt).map((a) => `${a}`),
['2019-02-17T01:45Z', '2019-02-17T02:45Z']
);
});
});
describe('getTransitions works as expected', () => {
it('should not have bug #510', () => {
// See https://github.com/tc39/proposal-temporal/issues/510 for more.
Expand Down
14 changes: 14 additions & 0 deletions spec/timezone.html
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,20 @@ <h1>Temporal.TimeZone.prototype.getAbsoluteFor ( _dateTime_ [ , _options_ ] )</h
</emu-alg>
</emu-clause>

<emu-clause id="sec-temporal.timezone.prototype.getpossibleabsolutesfor">
<h1>Temporal.TimeZone.prototype.getPossibleAbsolutesFor ( _dateTime_ )</h1>
<p>
The `getPossibleAbsolutesFor` method takes one argument _dateTime_.
The following steps are taken:
</p>
<emu-alg>
1. Let _timeZone_ be the *this* value.
1. Perform ? RequireInternalSlot(_timeZone_, [[InitializedTemporalTimezone]]).
1. Perform ? RequireInternalSlot(_dateTime_, [[InitializedTemporalDateTime]]).
1. <mark>TODO</mark>
</emu-alg>
</emu-clause>

<emu-clause id="sec-temporal.timezone.prototype.gettransitions">
<h1>Temporal.TimeZone.prototype.getTransitions ( _startingPoint_ )</h1>
<p>
Expand Down

0 comments on commit 0ed6991

Please sign in to comment.