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

Time zone and calendar protocol best practices #1043

Merged
merged 3 commits into from
Oct 26, 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
24 changes: 14 additions & 10 deletions docs/calendar.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,20 @@ date.withCalendar(calendar).add({ months: 1 })

### Custom calendars

For specialized applications where you need to do calculations in a calendar system that is not supported by Intl, you can also implement your own `Temporal.Calendar` object.
To do this, create a class inheriting from `Temporal.Calendar`, call `super()` in the constructor with a calendar identifier, and implement all the methods.
Any subclass of `Temporal.Calendar` will be accepted in Temporal APIs where a built-in `Temporal.Calendar` would work.
For specialized applications where you need to do calculations in a calendar system that is not supported by Intl, you can implement a custom calendar.
There are two ways to do this.

The identifier of a custom calendar must consist of one or more components of between 3 and 8 ASCII alphanumeric characters each, separated by dashes, as described in [Unicode Technical Standard 35](https://unicode.org/reports/tr35/tr35.html#Unicode_locale_identifier).
The recommended way is to create a class inheriting from `Temporal.Calendar`, call `super()` in the constructor with a calendar identifier string, and implement all the members except `id`, `toString()`, and `fields()`, which are optional.
If you don't implement the optional members, then the base class's default implementations will be used.

### Protocol
The other, more difficult, way to create a custom calendar is to create a plain object implementing the `Temporal.Calendar` protocol, without subclassing.
The object must implement all of the `Temporal.Calendar` methods except for `fields()`.
Any object with the required methods will return the correct output from any Temporal property or method.
However, most other code will assume that custom calendars act like built-in `Temporal.Calendar` objects.
To interoperate with libraries or other code that you didn't write, then you should implement the `id` property and the `fields()` method as well.
Your object must not have a `calendar` property, so that it can be distinguished in `Temporal.Calendar.from()` from other Temporal objects that have a calendar.

It's also possible for a plain object to be a custom calendar, without subclassing.
The object must implement the `Temporal.Calendar` methods and have an `id` property.
It must not have a `calendar` property, so that it can be distinguished in `Temporal.Calendar.from()` from other Temporal objects that have a calendar.
It is possible to pass such an object into any Temporal API that would normally take a built-in `Temporal.Calendar`.
The identifier of a custom calendar must consist of one or more components of between 3 and 8 ASCII alphanumeric characters each, separated by dashes, as described in [Unicode Technical Standard 35](https://unicode.org/reports/tr35/tr35.html#Unicode_locale_identifier).

## Constructor

Expand Down Expand Up @@ -121,6 +123,8 @@ cal = Temporal.Calendar.from({id: 'mycalendar'});
The `id` property gives an unambiguous identifier for the calendar.
Effectively, this is whatever `calendarIdentifier` was passed as a parameter to the constructor.

When subclassing `Temporal.Calendar`, this property doesn't need to be overridden because the default implementation gives the result of calling `toString()`.

## Methods

### calendar.**year**(_date_: Temporal.Date) : number
Expand Down Expand Up @@ -307,7 +311,7 @@ It is called indirectly when using the `from()` static methods and `with()` meth
Custom calendars should override this method if they require more fields with which to denote the date than the standard `era`, `year`, `month`, and `day`.
The input array contains the field names that are necessary for a particular operation (for example, `'month'` and `'day'` for `Temporal.MonthDay.prototype.with()`), and the method should make a copy of the array and add whichever extra fields are necessary.

The default implementation of this method returns a copy of `fields`.
When subclassing `Temporal.Calendar`, this method doesn't need to be overridden, unless your calendar requires extra fields, because the default implementation returns a copy of `fields`.

Usage example:

Expand Down
40 changes: 30 additions & 10 deletions docs/timezone.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,24 @@ Since `Temporal.Instant` and `Temporal.DateTime` do not contain any time zone in

## Custom time zones

For specialized applications where you need to do calculations in a time zone that is not supported by `Intl`, you can also implement your own `Temporal.TimeZone` object.
To do this, create a class inheriting from `Temporal.TimeZone`, call `super()` in the constructor with a time zone identifier, and implement the methods `getOffsetNanosecondsFor()`, `getPossibleInstantsFor()`, `getNextTransition()`, and `getPreviousTransition()`.
Any subclass of `Temporal.TimeZone` will be accepted in Temporal APIs where a built-in `Temporal.TimeZone` would work.
For specialized applications where you need to do calculations in a time zone that is not built in, you can implement a custom time zone.
There are two ways to do this.

The recommended way is to create a class inheriting from `Temporal.TimeZone`, call `super()` in the constructor with a time zone identifier string, and implement the methods `getOffsetNanosecondsFor()` and `getPossibleInstantsFor()`, and optionally `getNextTransition()` and `getPreviousTransition()`.
You don't need to implement any other methods because other properties and methods will use the base class's default implementations.

The other, more difficult, way to create a custom time zone is to create a plain object implementing the `Temporal.TimeZone` protocol, without subclassing.
The object must have at least `getOffsetNanosecondsFor()`, `getPossibleInstantsFor()`, and `toString()` methods.
Any object with those three methods will return the correct output from any Temporal property or method.
However, most other code will assume that custom time zones act like built-in `Temporal.TimeZone` objects.
To interoperate with libraries or other code that you didn't write, then you should implement all the other `Temporal.TimeZone` members as well: `id`, `getOffsetStringFor()`, `getDateTimeFor()`, `getZonedDateTimeFor()`, `getInstantFor()`, `getNextTransition()`, `getPreviousTransition()`, and `toJSON()`.
Your object must not have a `timeZone` property, so that it can be distinguished in `Temporal.TimeZone.from()` from other Temporal objects that have a time zone.

The identifier of a custom time zone must consist of one or more components separated by slashes (`/`), as described in the [tzdata documentation](https://htmlpreview.github.io/?https://github.com/eggert/tz/blob/master/theory.html#naming).
Each component must consist of between one and 14 characters.
Valid characters are ASCII letters, `.`, `-`, and `_`.
`-` may not appear as the first character of a component, and a component may not be a single dot `.` or two dots `..`.

### Protocol

It's also possible for a plain object to be a custom time zone, without subclassing.
The object must have `getOffsetNanosecondsFor()`, `getPossibleInstantsFor()`, and `toString()` methods.
It must not have a `timeZone` property, so that it can be distinguished in `Temporal.TimeZone.from()` from other Temporal objects that have a time zone.
It is possible to pass such an object into any Temporal API that would normally take a built-in `Temporal.TimeZone`.

## Constructor

### **new Temporal.TimeZone**(_timeZoneIdentifier_: string) : Temporal.TimeZone
Expand Down Expand Up @@ -127,6 +129,8 @@ tz2 = Temporal.TimeZone.from(tz);
The `id` 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.

When subclassing `Temporal.TimeZone`, this property doesn't need to be overridden because the default implementation gives the result of calling `toString()`.

## Methods

### timeZone.**getOffsetNanosecondsFor**(_instant_: Temporal.Instant | string) : number
Expand Down Expand Up @@ -179,6 +183,8 @@ If `timeZone` is a UTC offset time zone, the return value of this method is effe

If `instant` is not a `Temporal.Instant` object, then it will be converted to one as if it were passed to `Temporal.Instant.from()`.

When subclassing `Temporal.TimeZone`, this method doesn't need to be overridden because the default implementation creates an offset string using the result of calling `timeZone.getOffsetNanosecondsFor()`.

Example usage:

```javascript
Expand Down Expand Up @@ -206,6 +212,8 @@ This method is one way to convert a `Temporal.Instant` to a `Temporal.ZonedDateT

If `instant` is not a `Temporal.Instant` object, then it will be converted to one as if it were passed to `Temporal.Instant.from()`.

When subclassing `Temporal.TimeZone`, this method doesn't need to be overridden because the default implementation gives a result equivalent to calling `new Temporal.ZonedDateTime(instant.epochNanoseconds, timeZone, calendar)`.

Example usage:

```javascript
Expand Down Expand Up @@ -234,6 +242,8 @@ This method is one way to convert a `Temporal.Instant` to a `Temporal.DateTime`.

If `instant` is not a `Temporal.Instant` object, then it will be converted to one as if it were passed to `Temporal.Instant.from()`.

When subclassing `Temporal.TimeZone`, this method doesn't need to be overridden because the default implementation creates a `Temporal.DateTime` from `instant` using a UTC offset which is the result of calling `timeZone.getOffsetNanosecondsFor()`.

Example usage:

```javascript
Expand Down Expand Up @@ -283,6 +293,8 @@ For usage examples and a more complete explanation of how this disambiguation wo

If the result is earlier or later than the range that `Temporal.Instant` can represent (approximately half a million years centered on the [Unix epoch](https://en.wikipedia.org/wiki/Unix_time)), then a `RangeError` will be thrown, no matter the value of `disambiguation`.

When subclassing `Temporal.TimeZone`, this method doesn't need to be overridden because the default implementation calls `timeZone.getPossibleInstantsFor()`, and if there is more than one possible instant, uses `disambiguation` to pick which one to return.

### timeZone.**getPossibleInstantsFor**(_dateTime_: Temporal.DateTime | object | string) : array<Temporal.Instant>

**Parameters:**
Expand Down Expand Up @@ -316,6 +328,9 @@ In that case, this method will return `null`.

If `instant` is not a `Temporal.Instant` object, then it will be converted to one as if it were passed to `Temporal.Instant.from()`.

When subclassing `Temporal.TimeZone`, this method should be overridden if the time zone changes offsets.
Single-offset time zones can use the default implementation which returns `null`.

Example usage:

```javascript
Expand All @@ -342,6 +357,9 @@ In that case, this method will return `null`.

If `instant` is not a `Temporal.Instant` object, then it will be converted to one as if it were passed to `Temporal.Instant.from()`.

When subclassing `Temporal.TimeZone`, this method should be overridden if the time zone changes offsets.
Single-offset time zones can use the default implementation which returns `null`.

Example usage:

```javascript
Expand Down Expand Up @@ -370,6 +388,8 @@ The reverse operation, recovering a `Temporal.TimeZone` object from a string, is
If you need to rebuild a `Temporal.TimeZone` object from a JSON string, then you need to know the names of the keys that should be interpreted as `Temporal.TimeZone`s.
In that case you can build a custom "reviver" function for your use case.

When subclassing `Temporal.TimeZone`, this method doesn't need to be overridden because the default implementation returns the result of calling `timeZone.toString()`.

Example usage:

```js
Expand Down
5 changes: 3 additions & 2 deletions polyfill/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ export namespace Temporal {
}

export interface CalendarProtocol {
id: string;
id?: string;
calendar?: never;
year(date: Temporal.Date): number;
month(date: Temporal.Date): number;
Expand Down Expand Up @@ -485,6 +485,7 @@ export namespace Temporal {
>
): Temporal.Duration;
fields?(fields: Array<string>): Array<string>;
toString(): string;
}

/**
Expand Down Expand Up @@ -921,7 +922,7 @@ export namespace Temporal {
timeZone?: never;
getOffsetNanosecondsFor(instant: Temporal.Instant | string): number;
getOffsetStringFor?(instant: Temporal.Instant | string): string;
getDateTimeFor(instant: Temporal.Instant | string, calendar?: CalendarProtocol | string): Temporal.DateTime;
getDateTimeFor?(instant: Temporal.Instant | string, calendar?: CalendarProtocol | string): Temporal.DateTime;
getInstantFor?(dateTime: Temporal.DateTime | DateTimeLike | string, options?: ToInstantOptions): Temporal.Instant;
getNextTransition?(startingPoint: Temporal.Instant | string): Temporal.Instant | null;
getPreviousTransition?(startingPoint: Temporal.Instant | string): Temporal.Instant | null;
Expand Down