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 6, 2020
1 parent 5cbc449 commit 8bfd98d
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 0 deletions.
160 changes: 160 additions & 0 deletions docs/timezone-draft.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# 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.
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 perform attenuation, an implementation would monkeypatch the `Temporal.TimeZone.idToTimeZone()` function which performs the built-in mapping, and change the list of allowed time zones that `Temporal.TimeZone` iterates through.
Or it could just replace the `Temporal.TimeZone` class 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))
return TemporalTimeZone_idToTimeZone(id);
return null;
}
Temporal.TimeZone[Symbol.iterator] = function* () { return null; }

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

Temporal.TimeZone = FrozenTimeZoneImplementation;
```

## 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. */
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 should (must?) implement all of the non-static methods in the interface.
> **FIXME:** This means we have to remove any checks for the _[[InitializedTemporalTimeZone]]_ slot in any APIs outside of `Temporal.TimeZone`.
> **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, hours = 0, minutes = 0, seconds = 0, milliseconds = 0, microseconds = 0, nanoseconds = 0) {
const hourString = `${hours}`.padStart(2, '0');
const minuteString = `${minutes}`.padStart(2, '0');
const name = `${sign < 0 ? '-' : '+'}${hourString}:${minuteString}`;
super(name);
this._offsetNs = sign *
MakeTime(hours, minutes, seconds, milliseconds, microseconds, nanoseconds);
}

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
}
}
```
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 8bfd98d

Please sign in to comment.