Skip to content

Commit

Permalink
Bring custom time zones in line with custom calendars
Browse files Browse the repository at this point in the history
This makes a small change to the constructor of Temporal.TimeZone so that
it doesn't accept any arbitrary ID, only a recognized built-in one, to
bring it in line with calendars.

See: #847
See: #300
  • Loading branch information
ptomato committed Nov 16, 2020
1 parent a988320 commit 3600012
Show file tree
Hide file tree
Showing 5 changed files with 39 additions and 39 deletions.
9 changes: 7 additions & 2 deletions docs/timezone.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@ Since `Temporal.Instant` and `Temporal.PlainDateTime` do not contain any time zo
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 recommended way is to create a class inheriting from `Temporal.TimeZone`.
You must use one of the built-in time zones as the "base time zone".
In the class's constructor, call `super()` with the identifier of the base time zone.
The class must override `toString()` to return its own identifier.
Overriding `getOffsetNanosecondsFor()`, `getPossibleInstantsFor()`, `getNextTransition()`, and `getPreviousTransition()` is optional.
If you don't override the optional members, then they will behave as in the base time zone.
You don't need to override any other methods such as `getOffsetStringFor()` because they will call `getOffsetNanosecondsFor()`, `getPossibleInstantsFor()`, and `toString()` internally.

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.
Expand Down
11 changes: 1 addition & 10 deletions polyfill/lib/timezone.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,9 @@ import {
SetSlot
} from './slots.mjs';

import * as REGEX from './regex.mjs';
const OFFSET = new RegExp(`^${REGEX.offset.source}$`);
const IANA_NAME = new RegExp(`^${REGEX.timeZoneID.source}$`);

export class TimeZone {
constructor(timeZoneIdentifier) {
if (new.target === TimeZone) {
timeZoneIdentifier = ES.GetCanonicalTimeZoneIdentifier(timeZoneIdentifier);
}
if (!OFFSET.exec(timeZoneIdentifier) && !IANA_NAME.exec(timeZoneIdentifier)) {
throw new RangeError(`invalid time zone identifier ${timeZoneIdentifier}`);
}
timeZoneIdentifier = ES.GetCanonicalTimeZoneIdentifier(timeZoneIdentifier);
CreateSlots(this);
SetSlot(this, TIMEZONE_ID, timeZoneIdentifier);

Expand Down
25 changes: 15 additions & 10 deletions polyfill/test/regex.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -589,12 +589,16 @@ describe('fromString regex', () => {
});

describe('time zone ID', () => {
function makeCustomTimeZone(id) {
return class extends Temporal.TimeZone {
constructor() {
super(id);
}
let oldTemporalTimeZoneFrom = Temporal.TimeZone.from;
let fromCalledWith;
before(() => {
Temporal.TimeZone.from = function (item) {
fromCalledWith = item;
return new Temporal.TimeZone('UTC');
};
});
function testTimeZoneID(id) {
return Temporal.ZonedDateTime.from(`1970-01-01T00:00[${id}]`);
}
describe('valid', () => {
[
Expand All @@ -616,9 +620,8 @@ describe('fromString regex', () => {
'Etc/GMT+12'
].forEach((id) => {
it(id, () => {
const Custom = makeCustomTimeZone(id);
const tz = new Custom();
equal(tz.id, id);
testTimeZoneID(id);
equal(fromCalledWith, id);
});
});
});
Expand All @@ -641,11 +644,13 @@ describe('fromString regex', () => {
'Foo/Etc/GMT-8'
].forEach((id) => {
it(id, () => {
const Custom = makeCustomTimeZone(id);
throws(() => new Custom(), RangeError);
throws(() => testTimeZoneID(id), RangeError);
});
});
});
after(() => {
Temporal.TimeZone.from = oldTemporalTimeZoneFrom;
});
});

describe('calendar ID', () => {
Expand Down
10 changes: 8 additions & 2 deletions polyfill/test/usertimezone.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ describe('Userland time zone', () => {
describe('Trivial subclass', () => {
class CustomUTCSubclass extends Temporal.TimeZone {
constructor() {
super('Etc/Custom/UTC_Subclass');
super('UTC');
}
toString() {
return 'Etc/Custom/UTC_Subclass';
}
getOffsetNanosecondsFor(/* instant */) {
return 0;
Expand Down Expand Up @@ -205,7 +208,10 @@ describe('Userland time zone', () => {
describe('sub-minute offset', () => {
class SubminuteTimeZone extends Temporal.TimeZone {
constructor() {
super('Custom/Subminute');
super('-00:00:01.111111111');
}
toString() {
return 'Custom/Subminute';
}
getOffsetNanosecondsFor() {
return -1111111111;
Expand Down
23 changes: 8 additions & 15 deletions spec/timezone.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,14 @@ <h1>Temporal.TimeZone ( _identifier_ )</h1>
<emu-alg>
1. If NewTarget is *undefined*, then
1. Throw a *TypeError* exception.
1. Let _identifier_ be ? ToString(_identifier_).
1. If _identifier_ does not satisfy the syntax of a |TemporalTimeZoneIdentifier| (see <emu-xref href="#sec-temporal-iso8601grammar"></emu-xref>), then
1. Throw a *RangeError* exception.
1. Let _sign_, _hour_, _minute_, and _id_ be the parts of _identifier_ produced respectively by the |TimeZoneUTCOffsetSign|, |TimeZoneUTCOffsetHour|, |TimeZoneUTCOffsetMinute| and |TimeZoneIANAName| productions, or *undefined* if not present.
1. If _hour_ is not *undefined*, then
1. Assert: _sign_ is not *undefined*.
1. Set _hour_ to ! ToInteger(_hour_).
1. If _sign_ = *"-"*, then
1. Set _hour_ to −1 × _hour_.
1. If _minute_ is not *undefined*, then
1. Set _minute_ to ! ToInteger(_minute_).
1. <mark>Do something with the offset.</mark>
1. If ! IsValidTimeZoneName(_id_) is *false*, then
1. Throw a *TypeError* exception.
1. Let _canonical_ be ! CanonicalizeTimeZoneName(_identifier_).
1. Set _identifier_ to ? ToString(_identifier_).
1. If _identifier_ satisfies the syntax of a |TimeZoneNumericUTCOffset| (see <emu-xref href="#sec-temporal-iso8601grammar"></emu-xref>), then
1. Let _offsetNanoseconds_ be ? ParseTimeZoneOffsetString(_identifier_).
1. Let _canonical_ be ? FormatTimeZoneOffsetString(_offsetNanoseconds_).
1. Else,
1. If ! IsValidTimeZoneName(_id_) is *false*, then
1. Throw a *RangeError* exception.
1. Let _canonical_ be ! CanonicalizeTimeZoneName(_identifier_).
1. Return ? CreateTemporalTimeZone(_canonical_, NewTarget).
</emu-alg>
</emu-clause>
Expand Down

0 comments on commit 3600012

Please sign in to comment.