Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Temporal.TimeZone docs #316

Merged
merged 7 commits into from
Jan 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ See [Temporal.Absolute Documentation](./absolute.md) for more detailed documenta

### `Temporal.TimeZone`

A `Temporal.TimeZone` represents an IANA Timezone, a specific UTF-Offset or UTC itself. Because of this `Temporal.TimeZone` can be used to convert between `Temporal.Absolute` and `Temporal.DateTime` as well as finding out the offset at a specific `Temporal.Absolute`.
A `Temporal.TimeZone` represents an IANA time zone, a specific UTC offset or UTC itself. Because of this `Temporal.TimeZone` can be used to convert between `Temporal.Absolute` and `Temporal.DateTime` as well as finding out the offset at a specific `Temporal.Absolute`.

`Temporal.TimeZone` is also an iterable that give access to the IANA-Timezones supported by the system from the [IANA time zone database](https://www.iana.org/time-zones) (also listed [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)).
`Temporal.TimeZone` is also an iterable that gives access to the IANA time zones supported by the system from the [IANA time zone database](https://www.iana.org/time-zones) (also listed [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)).

See [Temporal.TimeZone Documentation](./timezone.md) for more detailed documentation.

### `Temporal.DateTime`

A `Temporal.DateTime` represents a calendar date and wall-clock time. That means it does not carry timezone information. However it can be converted to a `Temporal.Absolute` using a `Temporal.TimeZone`.
A `Temporal.DateTime` represents a calendar date and wall-clock time. That means it does not carry time zone information. However it can be converted to a `Temporal.Absolute` using a `Temporal.TimeZone`.

This can also be converted to object containing only partial information such as `Temporal.Date` and `Temporal.Time`.

Expand Down Expand Up @@ -82,7 +82,7 @@ See [Temporal.Duration Documentation](./duration.md) for more detailed documenta
### `Temporal` functions

* `Temporal.getAbsolute()` - get the current system absolute time
* `Temporal.getTimeZone()` - get the current system timezone
* `Temporal.getTimeZone()` - get the current system time zone
* `Temporal.getDateTime()` - get the current system date/time
* `Temporal.getTime()` - get the current system time
* `Temporal.getDate()` - get the current system date
Expand Down
4 changes: 2 additions & 2 deletions docs/absolute.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# `Temporal.Absolute`

An absolute point in time.
No timezone or offset information is present. As such `Absolute`s have no concept of days, months or even hours. For convenience of interoperability it uses _nanoseconds since the unix-epoch_
No time zone or offset information is present. As such `Absolute`s have no concept of days, months or even hours. For convenience of interoperability it uses _nanoseconds since the unix-epoch_

## new Temporal.Absolute(epochNanoSeconds : bigint) : Temporal.Absolute

Expand Down Expand Up @@ -29,7 +29,7 @@ Returns the numeric value of the specified date as the number of nanoseconds sin

## absolute.inTimeZone(timeZone: Temporal.TimeZone | string) : Temporal.DateTime

Returns the time-zone offset as a `DateTime` object for the current timezone.
Returns the time zone offset as a `DateTime` object for the current time zone.

## absolute.plus(duration: Temporal.Duration | string | object) : Temporal.Absolute

Expand Down
2 changes: 1 addition & 1 deletion docs/calendar-draft.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ Temporal.Date.prototype.toString = function() {
}
```

For objects with time components (such as Temporal.DateTime), the calendar would be appended to the end of the string. It would be distinguisable from the timezone because it does not contain a slash. For example: `2019-12-06T16:23+00:50[America/NewYork][hebrew]`. @gibson042 points out that this could be probematic: "There are many aliases without / ... [including] #156. And it gets worse with author-defined time zone and calendar names."
For objects with time components (such as Temporal.DateTime), the calendar would be appended to the end of the string. It would be distinguisable from the time zone because it does not contain a slash. For example: `2019-12-06T16:23+00:50[America/NewYork][hebrew]`. @gibson042 points out that this could be probematic: "There are many aliases without / ... [including] #156. And it gets worse with author-defined time zone and calendar names."

Alternatively, we may consider changing the syntax to add `c=` for calendars and `z=` for zones. `2019-12-06T16:23+00:50[z=America/NewYork][c=hebrew]`

Expand Down
198 changes: 196 additions & 2 deletions docs/timezone.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,213 @@
# `Temporal.TimeZone`

A representation of a time-zone giving access to information about offsets & dst-changes as well as allowing for conversions.
A `Temporal.TimeZone` is a representation of a time zone: either an [IANA time zone](https://www.iana.org/time-zones), including information about the time zone such as the offset between the local time and UTC at a particular time, and daylight saving time (DST) changes; or simply a particular UTC offset with no DST.

## new Temporal.TimeZone(timeZone: string) : Temporal.TimeZone
Since `Temporal.Absolute` and `Temporal.DateTime` do not contain any time zone information, a `Temporal.TimeZone` object is required to convert between the two.

Finally, the `Temporal.TimeZone` object itself provides access to a list of the time zones in the IANA time zone database.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pipobscure is this in the spec, or is it just a helper functionality in the polyfill?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a stub in the spec (https://tc39.es/proposal-temporal/#sec-temporal.timezone.prototype-@@iterator) but it does look like it's intentionally there. It was also mentioned elsewhere in the stub documentation before I expanded it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does anyone know the motivation for this feature? @pipobscure


## new Temporal.TimeZone(timeZoneIdentifier: string) : Temporal.TimeZone

**Parameters:**
- `timeZoneIdentifier` (string): A description of the time zone; either its IANA name, or a UTC offset.

**Returns:** a new `Temporal.TimeZone` object.

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 `+100` will be understood to mean `+01:00`, and capitalization will be corrected.
If no time zone can be determined from `timeZoneIdentifier`, then a `RangeError` is thrown.

Usage examples:
```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('+645');
/* WRONG */ tz = new Temporal.TimeZone('local'); // not a time zone, throws
```

### Difference between IANA time zones and UTC offsets

The returned time zone object behaves slightly differently depending on whether an IANA time zone name (e.g. `Europe/Berlin`) is given, or a UTC offset (e.g. `+01:00`).
IANA time zones may have DST transitions, and UTC offsets do not.
For example:

```javascript
tz1 = new Temporal.TimeZone('-08:00');
tz2 = new Temporal.TimeZone('America/Vancouver');
now = Temporal.now.absolute();
tz1.getTransitions(now).next().done; // => true
tz2.getTransitions(now).next().done; // => false
```

## timeZone.name : string

The `name` 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.

## timeZone.getOffsetFor(absolute: Temporal.Absolute) : string

**Parameters:**
- `absolute` (`Temporal.Absolute`): The time for which to compute the time zone's UTC offset.

**Returns**: a string indicating the UTC offset at the given time.

Since the UTC offset can change throughout the year in time zones that employ DST, this method queries the UTC offset at a particular time.

Note that only `Temporal.TimeZone` objects constructed from an IANA time zone name may have DST transitions; those constructed from a UTC offset do not.
If `timeZone` is a UTC offset time zone, the return value of this method is effectively the same as `timeZone.name`.

Example usage:
```javascript
// Getting the UTC offset for a time zone at a particular time
timestamp = new Temporal.Absolute(1553993100000000000n);
tz = new Temporal.TimeZone('Europe/Berlin');
tz.getOffsetFor(timestamp); // => +01:00

// TimeZone with a fixed UTC offset
tz = new Temporal.TimeZone('-08:00');
tz.getOffsetFor(timestamp); // => -08:00
// UTC is always 0 offset
tz = new Temporal.TimeZone('UTC');
tz.getOffsetFor(timestamp); // => +00:00
```

## timeZone.getDateTimeFor(absolute: Temporal.Absolute) : Temporal.DateTime

**Parameters:**
- `absolute` (`Temporal.Absolute`): An absolute time to convert.

**Returns:** A `Temporal.DateTime` object indicating the calendar date and wall-clock time in `timeZone` at the absolute time indicated by `absolute`.

This method is one way to convert a `Temporal.Absolute` to a `Temporal.DateTime`.

Example usage:

```javascript
// Converting a specific absolute time to a calendar date / wall-clock time
timestamp = new Temporal.Absolute(1553993100000000000n);
tz = new Temporal.TimeZone('Europe/Berlin');
tz.getDateTimeFor(timestamp); // => 2019-03-31T01:45

// What time was the Unix Epoch (timestamp 0) in Bell Labs (Murray Hill, New Jersey, USA)?
epoch = new Temporal.Absolute(0n);
tz = new Temporal.TimeZone('America/New_York');
tz.getDateTimeFor(epoch); // => 1969-12-31T19:00
```

## timeZone.getAbsoluteFor(dateTime: Temporal.DateTime, disambiguation: 'earlier' | 'later' | 'reject' = 'earlier') : Temporal.Absolute

**Parameters:**
- `dateTime` (`Temporal.DateTime`): A calendar date and wall-clock time to convert.
- `disambiguation` (optional `string`): How to disambiguate if the date and time given by `dateTime` does not exist in the time zone, or exists more than once.
Allowed values are `earlier`, `later`, and `reject`.
The default is `earlier`.

**Returns:** A `Temporal.Absolute` object indicating the absolute time in `timeZone` at the time of the calendar date and wall-clock time from `dateTime`.

This method is one way to convert a `Temporal.DateTime` to a `Temporal.Absolute`.

### Resolving ambiguity

> This explanation was adapted from the [moment-timezone documentation](https://github.com/moment/momentjs.com/blob/master/docs/moment-timezone/01-using-timezones/02-parsing-ambiguous-inputs.md).

This conversion is not necessarily one-to-one.
Due to DST time changes, there is a possibility that a wall-clock time either does not exist, or has existed twice.
The `disambiguation` argument controls what absolute time to return in the case of ambiguity:
- `earlier` (the default): The earlier of the two possible absolute times will be returned.
- `later`: The later of the two possible absolute times will be returned.
- `reject`: A `RangeError` will be thrown.

When entering DST, clocks move forward an hour.
In reality, it is not time that is moving, it is the offset moving.
Moving the offset forward gives the illusion that an hour has disappeared.
As the clock ticks, you can see it move from 1:58 to 1:59 to 3:00.
ptomato marked this conversation as resolved.
Show resolved Hide resolved
It is easier to see what is actually happening when you include the offset.

```
1:58 +1
1:59 +1
3:00 +2
3:01 +2
```

The result is that any time between 1:59:59 and 3:00:00 never actually happened.
In `earlier` mode, the absolute time will skip backwards by the amount of the DST gap (usually 1 hour).
In `later` mode, the absolute time will skip forwards by the amount of the DST gap.

```javascript
tz = new Temporal.TimeZone('Europe/Berlin');
dt = new Temporal.DateTime(2019, 3, 31, 2, 45);
tz.getAbsoluteFor(dt, 'earlier'); // => 2019-03-31T00:45Z
tz.getAbsoluteFor(dt, 'later'); // => 2019-03-31T01:45Z
tz.getAbsoluteFor(dt, 'reject'); // throws
```

In this example, the wall-clock time 2:45 doesn't exist, so it is treated as either 1:45 +01:00 or 3:45 +02:00.
ptomato marked this conversation as resolved.
Show resolved Hide resolved

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.
In `later` mode, the absolute time will be the later instance of the duplicated time.

```javascript
tz = new Temporal.TimeZone('America/Sao_Paulo');
dt = new Temporal.DateTime(2019, 2, 16, 23, 45);
tz.getAbsoluteFor(dt, 'earlier'); // => 2019-02-17T01:45Z
tz.getAbsoluteFor(dt, 'later'); // => 2019-02-17T02:45Z
tz.getAbsoluteFor(dt, 'reject'); // throws
```

In this example, the wall-clock time 23:45 exists twice.

> *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.
ptomato marked this conversation as resolved.
Show resolved Hide resolved

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

**Parameters:**
- `startingPoint` (`Temporal.Absolute`): Time after which to start calculating DST transitions.

**Returns:** An iterable yielding `Temporal.Absolute` objects indicating subsequent DST transitions in the given time zone.

This method is used to calculate future DST transitions for the time zone, starting at `startingPoint`.

Note that if the time zone was constructed from a UTC offset, there will be no DST transitions.
In that case, the iterable will be empty.

Example usage:

```javascript
// How long until the next DST change from now, in the current location?
tz = Temporal.now.timeZone();
now = Temporal.now.absolute();
[nextTransition] = tz.getTransitions(now);
duration = nextTransition.difference(now);
duration.toLocaleString(); // output will vary
```

## timeZone.toString() : string

**Returns:** The string given by `timeZone.name`.

This method overrides `Object.prototype.toString()` and provides the time zone's `name` property as a human-readable description.

## Temporal.TimeZone: iterator<Temporal.TimeZone>

The `Temporal.TimeZone` object is itself iterable, and can be used to iterate through all of the IANA time zones supported by the implementation.

Example usage:

```javascript
// List all supported IANA time zones
for (let zone of Temporal.TimeZone) console.log(zone.name);
// example output:
// Africa/Abidjan
// Africa/Accra
// Africa/Addis_Ababa
// ...and many more
```
6 changes: 3 additions & 3 deletions polyfill/lib/duration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,17 @@ export class Duration {
[years, months, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds] = arr;
break;
case 'balance':
let tdays;
let deltaDays;
({
days: tdays,
deltaDays,
ptomato marked this conversation as resolved.
Show resolved Hide resolved
hour: hours,
minute: minutes,
second: seconds,
millisecond: milliseconds,
microsecond: microseconds,
nanosecond: nanoseconds
} = ES.BalanceTime(hours, minutes, seconds, milliseconds, microseconds, nanoseconds));
days += tdays;
days += deltaDays;
break;
default:
throw new TypeError('disambiguation should be either reject, constrain or balance');
Expand Down
9 changes: 5 additions & 4 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {
NANOSECONDS
} from './slots.mjs';

const DAYMILLIS = 3600000;
const DAYMILLIS = 86400000;
ptomato marked this conversation as resolved.
Show resolved Hide resolved

const INTRINSICS = {
'%Temporal.DateTime%': TemporalDateTime,
Expand Down Expand Up @@ -899,9 +899,10 @@ const OFFSET = new RegExp(`^${REGEX.offset.source}$`);
function parseOffsetString(string) {
const match = OFFSET.exec(String(string));
if (!match) return null;
const hours = +match[1];
const minutes = +match[2];
return (hours * 60 + minutes) * 60 * 1000;
const sign = match[1] === '-' ? -1 : +1;
const hours = +match[2];
const minutes = +match[3];
return sign * (hours * 60 + minutes) * 60 * 1000;
}
function makeOffsetString(offsetMilliSeconds) {
let offsetSeconds = Math.round(offsetMilliSeconds / 1000);
Expand Down
2 changes: 1 addition & 1 deletion polyfill/lib/regex.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ export const monthday = new RegExp(
'$'
);

export const offset = /([+-][01]?[0-9]):?([0-5][0-9])/;
export const offset = /([+-])([01]?[0-9]):?([0-5][0-9])/;
export const duration = /P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?/;
12 changes: 7 additions & 5 deletions polyfill/lib/timezone.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { IDENTIFIER, EPOCHNANOSECONDS, CreateSlots, GetSlot, SetSlot } from './s
import { ZONES } from './zones.mjs';
import { timezone as STRING } from './regex.mjs';

import bigInt from 'big-integer';
ptomato marked this conversation as resolved.
Show resolved Hide resolved

export class TimeZone {
constructor(timeZoneIndentifier) {
CreateSlots(this);
Expand Down Expand Up @@ -80,7 +82,7 @@ export class TimeZone {

if (!~['earlier', 'later'].indexOf(disambiguation)) throw new RangeError(`no such absolute found`);

const { ms: utcms } = ES.GetEpochFromParts(
const utcns = ES.GetEpochFromParts(
ptomato marked this conversation as resolved.
Show resolved Hide resolved
year,
month,
day,
Expand All @@ -91,10 +93,10 @@ export class TimeZone {
microsecond,
nanosecond
);
const before = ES.GetTimeZoneOffsetMilliseconds(utcms - 86400000, GetSlot(this, IDENTIFIER));
const after = ES.GetTimeZoneOffsetMilliseconds(utcms + 86400000, GetSlot(this, IDENTIFIER));
const diff = ES.ToToDuration({
milliseconds: after - before
const before = ES.GetTimeZoneOffsetNanoseconds(utcns.minus(bigInt(86400 * 1e9)), GetSlot(this, IDENTIFIER));
const after = ES.GetTimeZoneOffsetNanoseconds(utcns.plus(bigInt(86400 * 1e9)), GetSlot(this, IDENTIFIER));
const diff = ES.ToDuration({
nanoseconds: after.minus(before)
});
switch (disambiguation) {
case 'earlier':
Expand Down
6 changes: 4 additions & 2 deletions polyfill/test/duration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ describe('Duration', () => {
throws(() => new Duration(-1, -1, -1, -1, -1, -1, -1, -1, -1, 'reject'), RangeError));
it('negative values invert when "constrain"', () =>
equal(`${new Duration(-1, -1, -1, -1, -1, -1, -1, -1, -1, 'constrain')}`, 'P1Y1M1DT1H1M1.001001001S'));
it('excessive values balance when "balance"', () =>
equal(`${new Duration(0, 0, 0, 0, 0, 0, 0, 0, 1000, 'balance')}`, 'PT0.000001S'));
it('excessive values balance when "balance"', () => {
equal(`${new Duration(0, 0, 0, 0, 0, 0, 0, 0, 1000, 'balance')}`, 'PT0.000001S');
equal(`${new Duration(0, 0, 0, 0, 0, 2 * 86400, 0, 0, 0, 'balance')}`, 'P2D');
});
it('throw when bad disambiguation', () =>
throws(() => new Duration(0, 0, 0, 0, 0, 0, 0, 0, 0, 'xyz'), TypeError));
});
Expand Down
Loading