Skip to content

Commit

Permalink
Draft of custom time zone API
Browse files Browse the repository at this point in the history
  • Loading branch information
ptomato committed Apr 10, 2020
1 parent 76a6acc commit ae419d6
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 0 deletions.
175 changes: 175 additions & 0 deletions docs/timezone-draft.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# Draft design of custom time zone API

## Why?

Currently, `Temporal.TimeZone` objects can only be obtained by passing an identifier to the constructor or to `Temporal.TimeZone.from()`.
These identifiers can be:

- an offset string such as `-07:00`, in which case a time zone object is returned with a constant offset, with no daylight saving changes ever;
- an IANA time zone name such as `America/New_York`, in which case a time zone object is returned that will give a different offset depending on the date that it is queried with, which attempts to be historically accurate (within reason) as to the actual time that clocks showed there.

No other identifiers than the ones deemed valid by one of the above two rules (`±HH:MM` or [IsValidTimeZoneName](https://tc39.es/ecma402/#sec-isvalidtimezonename)), are currently permitted.

Custom time zones are a desirable feature because implementations may want to prevent leaking information about what historical revision of the time zone data is present on the host system, or control what information about the host system's current time zone is exposed.
See [issue #273](/~https://github.com/tc39/proposal-temporal/issues/273).

API users may also want to perform time zone calculations with older (or newer!) versions of the time zone data than what is present in their implementation.

## Usage of a custom time zone

Like built-in time zones, custom time zones have an `[[Identifier]]`.
In these examples we assume a custom time zone class `MarsTime` with the identifier `Mars/Olympus_Mons`.
(See the following section for how such a time zone object would be obtained.)

When parsing an ISO 8601 string, the only places the time zone identifier is taken into account are `Temporal.Absolute.from()` and `Temporal.TimeZone.from()`.

These functions gain an `options` parameter with an `idToTimeZone` option, whose value is a function returning the appropriate time zone object for the `id` passed in, or `null` to indicate that the time zone doesn't exist.

If the option is not given, `Temporal.TimeZone.idToTimeZone()` is called.
This is a new static method of `Temporal.TimeZone`.

> **FIXME:** Note that there is an issue open regarding this behaviour for calendars: [#294](/~https://github.com/tc39/proposal-temporal/issues/294)
```javascript
function idToTimeZone(id) {
if (id === 'Mars/Olympus_Mons')
return new MarsTime();
return Temporal.TimeZone.idToTimeZone(id);
}

Temporal.Absolute.from('2020-04-01T18:16+00:00[Mars/Olympus_Mons]', { idToTimeZone })
// returns the Absolute corresponding to 2020-04-01T18:16Z,
// assuming that MarsTime instances have an offset of +00:00 at that instant

Temporal.TimeZone.from('2020-04-01T18:16+00:00[Mars/Olympus_Mons]', { idToTimeZone })
// returns a new MarsTime instance
```

> **FIXME:** Passing a custom time zone's identifier to the built-in `Temporal.TimeZone` constructor currently doesn't work: `new Temporal.TimeZone('Mars/Olympus_Mons')` throws.
> However, this must change, because implementations would need to call `super(id)` to set the _[[Identifier]]_ and _[[InitializedTemporalTimeZone]]_ internal slots.
> Maybe we need to make `Temporal.TimeZone` not directly constructable?
In order to lock down any leakage of information about the host system's time zone database, one would monkeypatch the `Temporal.TimeZone.idToTimeZone()` function which performs the built-in mapping, change the list of allowed time zones that `Temporal.TimeZone` iterates through, and replace `Temporal.now.timeZone()` to avoid exposing the current time zone.
Or just replace the `Temporal.TimeZone` class and `Temporal.now` object altogether:

```javascript
// For example, to allow only offset time zones:

TemporalTimeZone_idToTimeZone = Temporal.TimeZone.idToTimeZone;
Temporal.TimeZone.idToTimeZone = function (id) {
if (/^[+-]\d{2}:?\d{2}$/.test(id) || id === 'UTC')
return TemporalTimeZone_idToTimeZone(id);
return null;
}
Temporal.TimeZone[Symbol.iterator] = function* () { return null; }
Temporal.now.timeZone = function () { return Temporal.TimeZone.from('UTC'); }

// or, to replace the built-in implementation altogether:

Temporal.TimeZone = LockedDownTimeZoneImplementation;
Temporal.now = lockedDownNowObject;
```

## Implementation of a custom time zone

### TimeZone interface

```javascript
class Temporal.TimeZone {
/** Sets the [[Identifier]] internal slot to @id, and creates the
* [[InitializedTemporalTimeZone]] internal slot. A subclassed custom
* time zone must chain up to this constructor. */
constructor(id : string) : Temporal.TimeZone;

// Methods that a subclassed custom time zone must implement

/** Given an absolute instant returns this time zone's corresponding
* UTC offset, in nanoseconds (signed). */
getOffsetAtInstant(absolute : Temporal.Absolute) : number;

/** Given the calendar/wall-clock time, returns an array of 0, 1, or
* 2 absolute instants that are possible points on the timeline
* corresponding to it. In getAbsoluteFor(), one of these will be
* selected, depending on the disambiguation option. */
possibleInstants(dateTime : Temporal.DateTime) : array<Temporal.Absolute>;

/** Returns an iterator of all following offset transitions, starting
* from @startingPoint. */
*getTransitions(startingPoint : Temporal.Absolute) : iterator<Temporal.Absolute>;

// API methods that a subclassed custom time zone doesn't need to touch

get name() : string;
getDateTimeFor(absolute : Temporal.Absolute) : Temporal.DateTime;
getAbsoluteFor(
dateTime : Temporal.DateTime,
options?: object
) : Temporal.Absolute;
getOffsetFor(absolute : Temporal.Absolute) : string;
toString() : string;
toJSON() : string;

static from(item : any, options?: object) : Temporal.TimeZone;
static idToTimeZone(id: string) : Temporal.TimeZone;
static [Symbol.iterator]() : iterator<Temporal.TimeZone>;
}
```
All the methods that custom time zones inherit from `Temporal.TimeZone` are implemented in terms of `getOffsetAtInstant()`, `possibleInstants()`, and the value of the _[[Identifier]]_ internal slot.
For example, `getOffsetFor()` and `getDateTimeFor()` call `getOffsetAtInstant()`, and `getAbsoluteFor()` calls both.
> **FIXME:** These names are not very good.
> Help is welcome in determining the color of this bike shed.
Alternatively, a custom time zone doesn't have to be a subclass of `Temporal.TimeZone`.
In this case, it can be a plain object, which must implement `getOffsetAtInstant()`, `possibleInstants()`, and the `name` property.
> **FIXME:** This means we have to remove any checks for the _[[InitializedTemporalTimeZone]]_ slot in all APIs, so that plain objects can use them with e.g. `Temporal.TimeZone.prototype.getOffsetFor.call(plainObject, absolute)`.
> **FIXME:** `Temporal.TimeZone` is supposed to be an iterable through all time zones known to the implementation, but what do we do about custom time zones there?
## Show Me The Code
Here's what it could look like to implement the built-in offset-based time zones as custom time zones.
The `MakeDate`, `MakeDay`, and `MakeTime` functions are as in the [ECMA-262 specification](https://tc39.es/ecma262/#sec-overview-of-date-objects-and-definitions-of-abstract-operations), except that instead of milliseconds, `MakeDate` and `MakeDay` return BigInt-typed nanoseconds, and `MakeTime` returns Number-typed nanoseconds.
(This example leaves out all the type checking, range checking, and error handling, just to show the bare bones.)
```javascript
class OffsetTimeZone extends Temporal.TimeZone {
constructor(sign = 1, h = 0, min = 0, s = 0, ms = 0, µs = 0, ns = 0) {
const offsetNs = MakeTime(h, min, s, ms, µs, ns);
if (sign === -1 && offsetNs === 0) sign = 1; // "-00:00" is "+00:00"
const hourString = `${h}`.padStart(2, '0');
const minuteString = `${min}`.padStart(2, '0');
const name = `${sign < 0 ? '-' : '+'}${hourString}:${minuteString}`;
super(name);
this._offsetNs = sign * offsetNs;
}

getOffsetAtInstant(/* absolute */) {
return this._offsetNs; // offset is always the same
}

possibleInstants(dateTime) {
const iso = dateTime.getISOFields();
const epochNs = MakeDate(
MakeDay(iso.year, iso.month, iso.day),
MakeTime(iso.hour, iso.minute, iso.second, iso.millisecond, iso.microsecond, iso.nanosecond)
);
return [Temporal.Absolute(epochNs + BigInt(this._offsetNs))];
}

*getTransitions() {
return null; // no transitions ever
}

static idToTimeZone(id) {
const match = /^([+-])(\d{2}):?(\d{2})$/.exec(id);
if (match) {
const [, sign, hours, minutes] = result;
return new OffsetTimeZone(sign === '-' ? -1 : 1, +hours, +minutes, 0, 0, 0, 0);
}
return Temporal.TimeZone.idToTimeZone(id);
}
}
```
3 changes: 3 additions & 0 deletions docs/timezone.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Since `Temporal.Absolute` and `Temporal.DateTime` do not contain any time zone i

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

An API for creating custom time zones is under discussion.
See [Custom Time Zone Draft](./timezone-draft.md) for more information.

## Constructor

### **new Temporal.TimeZone**(_timeZoneIdentifier_: string) : Temporal.TimeZone
Expand Down

0 comments on commit ae419d6

Please sign in to comment.