Skip to content

Commit

Permalink
Define valid time zone IDs
Browse files Browse the repository at this point in the history
A valid calendar ID according to the tzdata documentation is a number of
components consisting of 1 to 14 ASCII alpha characters, dashes, dots, or
underscores each, joined by slashes. `.`, `..`, or any string starting
with a dash are not valid components. Add this to the grammar and enforce
it in custom time zones.

Etc/GMT time zones are an exception, as they may contain + or digits.
These are valid for parsing, but not for custom time zones.

See: #665
  • Loading branch information
ptomato committed Oct 16, 2020
1 parent e5ad6bd commit 992acd6
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 38 deletions.
5 changes: 5 additions & 0 deletions docs/timezone.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ For specialized applications where you need to do calculations in a time zone th
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.

The identifier of a custom time zone must not be any of the built-in time zone identifiers, and must consist, as described in the [tzdata documentation](https://htmlpreview.github.io/?https://github.com/eggert/tz/blob/master/theory.html#naming) of one or more components separated by slashes (`/`).
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.
Expand Down
5 changes: 4 additions & 1 deletion polyfill/lib/regex.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
const tzComponent = /\.[-A-Za-z_]|\.\.[-A-Za-z._]{1,12}|\.[-A-Za-z_][-A-Za-z._]{0,12}|[A-Za-z_][-A-Za-z._]{0,13}/;
export const timeZoneID = new RegExp(`(?:${tzComponent.source}(?:\\/(?:${tzComponent.source}))*|Etc/GMT[-+]\\d{1,2})`);

const calComponent = /[A-Za-z0-9]{2,8}/;
export const calendarID = new RegExp(`(?:${calComponent.source}(?:-${calComponent.source})*)`);

const yearpart = /(?:[+-\u2212]\d{6}|\d{4})/;
const datesplit = new RegExp(`(${yearpart.source})(?:-(\\d{2})-(\\d{2})|(\\d{2})(\\d{2}))`);
const timesplit = /(\d{2})(?::(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?|(\d{2})(?:(\d{2})(?:[.,](\d{1,9}))?)?)?/;
export const offset = /([+-\u2212])([0-2][0-9])(?::?([0-5][0-9]))?/;
const zonesplit = new RegExp(`(?:([zZ])|(?:${offset.source}?(?:\\[(?!c=)([^\\]\\s]*)?\\])?))`);
const zonesplit = new RegExp(`(?:([zZ])|(?:${offset.source}?(?:\\[(${timeZoneID.source})\\])?))`);
const calendar = new RegExp(`\\[c=(${calendarID.source})\\]`);

export const instant = new RegExp(
Expand Down
4 changes: 4 additions & 0 deletions polyfill/lib/timezone.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {

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

function parseOffsetString(string) {
const match = OFFSET.exec(String(string));
Expand All @@ -37,6 +38,9 @@ export class TimeZone {
if (new.target === TimeZone) {
timeZoneIdentifier = ES.GetCanonicalTimeZoneIdentifier(timeZoneIdentifier);
}
if (!OFFSET.exec(timeZoneIdentifier) && !IANA_NAME.exec(timeZoneIdentifier)) {
throw new RangeError(`invalid time zone identifier ${timeZoneIdentifier}`);
}
CreateSlots(this);
SetSlot(this, TIMEZONE_ID, timeZoneIdentifier);

Expand Down
62 changes: 62 additions & 0 deletions polyfill/test/regex.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,68 @@ describe('fromString regex', () => {
}
});

describe('time zone ID', () => {
function makeCustomTimeZone(id) {
return class extends Temporal.TimeZone {
constructor() {
super(id);
}
};
}
describe('valid', () => {
[
'.a',
'..a',
'...',
'_',
'a-a',
'Etc/.a',
'Etc/..a',
'Etc/...',
'Etc/_',
'Etc/a-a',
'Etc/FourteenCharsZ',
'Etc/FourteenCharsZ/FourteenCharsZ'
].forEach((id) => {
it(id, () => {
const Custom = makeCustomTimeZone(id);
const tz = new Custom();
equal(tz.id, id);
});
});
});
describe('valid for built-in', () => {
['Etc/GMT-8', 'Etc/GMT-12', 'Etc/GMT+8', 'Etc/GMT+12'].forEach((id) => {
const tz = Temporal.TimeZone.from(id);
equal(tz.id, id);
});
});
describe('not valid', () => {
[
'.',
'..',
'-',
'3',
'-Foo',
'Etc/.',
'Etc/..',
'Etc/-',
'Etc/3',
'Etc/-Foo',
'Etc/😺',
'Etc/FifteenCharsZZZ',
'GMT-8',
'GMT+8',
'Foo/Etc/GMT-8'
].forEach((id) => {
it(id, () => {
const Custom = makeCustomTimeZone(id);
throws(() => new Custom(), RangeError);
});
});
});
});

describe('calendar ID', () => {
function makeCustomCalendar(id) {
return class extends Temporal.Calendar {
Expand Down
66 changes: 33 additions & 33 deletions polyfill/test/usertimezone.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('Userland time zone', () => {
describe('Trivial subclass', () => {
class CustomUTCSubclass extends Temporal.TimeZone {
constructor() {
super('Etc/Custom_UTC_Subclass');
super('Etc/Custom/UTC_Subclass');
}
getOffsetNanosecondsFor(/* instant */) {
return 0;
Expand All @@ -43,11 +43,11 @@ describe('Userland time zone', () => {
const dt = new Temporal.DateTime(1976, 11, 18, 15, 23, 30, 123, 456, 789);

it('is a time zone', () => equal(typeof obj, 'object'));
it('.id property', () => equal(obj.id, 'Etc/Custom_UTC_Subclass'));
it('.id property', () => equal(obj.id, 'Etc/Custom/UTC_Subclass'));
// FIXME: what should happen in Temporal.TimeZone.from(obj)?
it('.id is not available in from()', () => {
throws(() => Temporal.TimeZone.from('Etc/Custom_UTC_Subclass'), RangeError);
throws(() => Temporal.TimeZone.from('2020-05-26T16:02:46.251163036+00:00[Etc/Custom_UTC_Subclass]'), RangeError);
throws(() => Temporal.TimeZone.from('Etc/Custom/UTC_Subclass'), RangeError);
throws(() => Temporal.TimeZone.from('2020-05-26T16:02:46.251163036+00:00[Etc/Custom/UTC_Subclass]'), RangeError);
});
it('has offset string +00:00', () => equal(obj.getOffsetStringFor(inst), '+00:00'));
it('converts to DateTime', () => {
Expand All @@ -61,7 +61,7 @@ describe('Userland time zone', () => {
});
it('converts to string', () => equal(`${obj}`, obj.id));
it('prints in instant.toString', () =>
equal(inst.toString(obj), '1970-01-01T00:00+00:00[Etc/Custom_UTC_Subclass]'));
equal(inst.toString(obj), '1970-01-01T00:00+00:00[Etc/Custom/UTC_Subclass]'));
it('has no next transitions', () => assert.equal(obj.getNextTransition(), null));
it('has no previous transitions', () => assert.equal(obj.getPreviousTransition(), null));
it('works in Temporal.now', () => {
Expand All @@ -82,41 +82,41 @@ describe('Userland time zone', () => {
id = `${item}`;
// TODO: Use Temporal.parse here to extract the ID from an ISO string
}
if (id === 'Etc/Custom_UTC_Subclass') return new CustomUTCSubclass();
if (id === 'Etc/Custom/UTC_Subclass') return new CustomUTCSubclass();
return originalTemporalTimeZoneFrom.call(this, id);
};
});
it('works for TimeZone.from(id)', () => {
const tz = Temporal.TimeZone.from('Etc/Custom_UTC_Subclass');
const tz = Temporal.TimeZone.from('Etc/Custom/UTC_Subclass');
assert(tz instanceof CustomUTCSubclass);
});
it.skip('works for TimeZone.from(ISO string)', () => {
const tz = Temporal.TimeZone.from('1970-01-01T00:00+00:00[Etc/Custom_UTC_Subclass]');
const tz = Temporal.TimeZone.from('1970-01-01T00:00+00:00[Etc/Custom/UTC_Subclass]');
assert(tz instanceof CustomUTCSubclass);
});
it('works for Instant.from', () => {
const instant = Temporal.Instant.from('1970-01-01T00:00+00:00[Etc/Custom_UTC_Subclass]');
const instant = Temporal.Instant.from('1970-01-01T00:00+00:00[Etc/Custom/UTC_Subclass]');
equal(`${instant}`, '1970-01-01T00:00Z');
});
it('works for Instant.toString', () => {
const inst = Temporal.Instant.fromEpochSeconds(0);
equal(inst.toString('Etc/Custom_UTC_Subclass'), '1970-01-01T00:00+00:00[Etc/Custom_UTC_Subclass]');
equal(inst.toString('Etc/Custom/UTC_Subclass'), '1970-01-01T00:00+00:00[Etc/Custom/UTC_Subclass]');
});
it('works for Instant.toDateTime and toDateTimeISO', () => {
const inst = Temporal.Instant.fromEpochSeconds(0);
equal(`${inst.toDateTimeISO('Etc/Custom_UTC_Subclass')}`, '1970-01-01T00:00');
equal(`${inst.toDateTime('Etc/Custom_UTC_Subclass', 'gregory')}`, '1970-01-01T00:00[c=gregory]');
equal(`${inst.toDateTimeISO('Etc/Custom/UTC_Subclass')}`, '1970-01-01T00:00');
equal(`${inst.toDateTime('Etc/Custom/UTC_Subclass', 'gregory')}`, '1970-01-01T00:00[c=gregory]');
});
it('works for DateTime.toInstant', () => {
const dt = Temporal.DateTime.from('1970-01-01T00:00');
equal(dt.toInstant('Etc/Custom_UTC_Subclass').getEpochSeconds(), 0);
equal(dt.toInstant('Etc/Custom/UTC_Subclass').getEpochSeconds(), 0);
});
it('works for Temporal.now', () => {
assert(Temporal.now.dateTimeISO('Etc/Custom_UTC_Subclass') instanceof Temporal.DateTime);
assert(Temporal.now.dateTime('gregory', 'Etc/Custom_UTC_Subclass') instanceof Temporal.DateTime);
assert(Temporal.now.dateISO('Etc/Custom_UTC_Subclass') instanceof Temporal.Date);
assert(Temporal.now.date('gregory', 'Etc/Custom_UTC_Subclass') instanceof Temporal.Date);
assert(Temporal.now.timeISO('Etc/Custom_UTC_Subclass') instanceof Temporal.Time);
assert(Temporal.now.dateTimeISO('Etc/Custom/UTC_Subclass') instanceof Temporal.DateTime);
assert(Temporal.now.dateTime('gregory', 'Etc/Custom/UTC_Subclass') instanceof Temporal.DateTime);
assert(Temporal.now.dateISO('Etc/Custom/UTC_Subclass') instanceof Temporal.Date);
assert(Temporal.now.date('gregory', 'Etc/Custom/UTC_Subclass') instanceof Temporal.Date);
assert(Temporal.now.timeISO('Etc/Custom/UTC_Subclass') instanceof Temporal.Time);
});
after(() => {
Temporal.TimeZone.from = originalTemporalTimeZoneFrom;
Expand All @@ -136,7 +136,7 @@ describe('Userland time zone', () => {
return [new Temporal.Instant(epochNs)];
},
toString() {
return 'Etc/Custom_UTC_Protocol';
return 'Etc/Custom/UTC_Protocol';
}
};

Expand All @@ -155,7 +155,7 @@ describe('Userland time zone', () => {
equal(`${dt.toInstant(obj)}`, '1976-11-18T15:23:30.123456789Z');
});
it('prints in instant.toString', () =>
equal(inst.toString(obj), '1970-01-01T00:00+00:00[Etc/Custom_UTC_Protocol]'));
equal(inst.toString(obj), '1970-01-01T00:00+00:00[Etc/Custom/UTC_Protocol]'));
it('works in Temporal.now', () => {
assert(Temporal.now.dateTimeISO(obj) instanceof Temporal.DateTime);
assert(Temporal.now.dateTime('gregory', obj) instanceof Temporal.DateTime);
Expand All @@ -174,41 +174,41 @@ describe('Userland time zone', () => {
id = `${item}`;
// TODO: Use Temporal.parse here to extract the ID from an ISO string
}
if (id === 'Etc/Custom_UTC_Protocol') return obj;
if (id === 'Etc/Custom/UTC_Protocol') return obj;
return originalTemporalTimeZoneFrom.call(this, id);
};
});
it('works for TimeZone.from(id)', () => {
const tz = Temporal.TimeZone.from('Etc/Custom_UTC_Protocol');
const tz = Temporal.TimeZone.from('Etc/Custom/UTC_Protocol');
assert(Object.is(tz, obj));
});
it.skip('works for TimeZone.from(ISO string)', () => {
const tz = Temporal.TimeZone.from('1970-01-01T00:00+00:00[Etc/Custom_UTC_Protocol]');
const tz = Temporal.TimeZone.from('1970-01-01T00:00+00:00[Etc/Custom/UTC_Protocol]');
assert(Object.is(tz, obj));
});
it('works for Instant.from', () => {
const inst = Temporal.Instant.from('1970-01-01T00:00+00:00[Etc/Custom_UTC_Protocol]');
const inst = Temporal.Instant.from('1970-01-01T00:00+00:00[Etc/Custom/UTC_Protocol]');
equal(`${inst}`, '1970-01-01T00:00Z');
});
it('works for Instant.toString', () => {
const inst = Temporal.Instant.fromEpochSeconds(0);
equal(inst.toString('Etc/Custom_UTC_Protocol'), '1970-01-01T00:00+00:00[Etc/Custom_UTC_Protocol]');
equal(inst.toString('Etc/Custom/UTC_Protocol'), '1970-01-01T00:00+00:00[Etc/Custom/UTC_Protocol]');
});
it('works for Instant.toDateTime and toDateTimeISO', () => {
const inst = Temporal.Instant.fromEpochSeconds(0);
equal(`${inst.toDateTimeISO('Etc/Custom_UTC_Protocol')}`, '1970-01-01T00:00');
equal(`${inst.toDateTime('Etc/Custom_UTC_Protocol', 'gregory')}`, '1970-01-01T00:00[c=gregory]');
equal(`${inst.toDateTimeISO('Etc/Custom/UTC_Protocol')}`, '1970-01-01T00:00');
equal(`${inst.toDateTime('Etc/Custom/UTC_Protocol', 'gregory')}`, '1970-01-01T00:00[c=gregory]');
});
it('works for DateTime.toInstant', () => {
const dt = Temporal.DateTime.from('1970-01-01T00:00');
equal(dt.toInstant('Etc/Custom_UTC_Protocol').getEpochSeconds(), 0);
equal(dt.toInstant('Etc/Custom/UTC_Protocol').getEpochSeconds(), 0);
});
it('works for Temporal.now', () => {
assert(Temporal.now.dateTimeISO('Etc/Custom_UTC_Protocol') instanceof Temporal.DateTime);
assert(Temporal.now.dateTime('gregory', 'Etc/Custom_UTC_Protocol') instanceof Temporal.DateTime);
assert(Temporal.now.dateISO('Etc/Custom_UTC_Protocol') instanceof Temporal.Date);
assert(Temporal.now.date('gregory', 'Etc/Custom_UTC_Protocol') instanceof Temporal.Date);
assert(Temporal.now.timeISO('Etc/Custom_UTC_Protocol') instanceof Temporal.Time);
assert(Temporal.now.dateTimeISO('Etc/Custom/UTC_Protocol') instanceof Temporal.DateTime);
assert(Temporal.now.dateTime('gregory', 'Etc/Custom/UTC_Protocol') instanceof Temporal.DateTime);
assert(Temporal.now.dateISO('Etc/Custom/UTC_Protocol') instanceof Temporal.Date);
assert(Temporal.now.date('gregory', 'Etc/Custom/UTC_Protocol') instanceof Temporal.Date);
assert(Temporal.now.timeISO('Etc/Custom/UTC_Protocol') instanceof Temporal.Time);
});
after(() => {
Temporal.TimeZone.from = originalTemporalTimeZoneFrom;
Expand Down
24 changes: 21 additions & 3 deletions spec/abstractops.html
Original file line number Diff line number Diff line change
Expand Up @@ -478,9 +478,11 @@ <h1>ISO 8601 grammar</h1>
NonzeroDigit : one of
`1` `2` `3` `4` `5` `6` `7` `8` `9`

ASCIISign : one of
`+` `-`

Sign :
`+`
`-`
ASCIISign
U+2212

DecimalSeparator : one of
Expand Down Expand Up @@ -611,8 +613,24 @@ <h1>ISO 8601 grammar</h1>
TimeZoneUTCOffsetSign TimeZoneUTCOffsetHour
TimeZoneUTCOffsetSign TimeZoneUTCOffsetHour `:`? TimeZoneUTCOffsetMinute

TZLeadingChar :
Alpha
`.`
`_`

TZChar :
Alpha
`.`
`-`
`_`

TimeZoneIANANameComponent :
TZLeadingChar TZChar? TZChar? TZChar? TZChar? TZChar? TZChar? TZChar? TZChar? TZChar? TZChar? TZChar? TZChar? TZChar? but not one of `.` or `..`

TimeZoneIANAName :
&gt; any string for which IsValidTimeZoneName returns true
TimeZoneIANANameComponent
TimeZoneIANANameComponent `/` TimeZoneIANAName
`Etc/GMT` ASCIISign TimeZoneUTCOffsetHour

TimeZone :
UTCDesignator
Expand Down
2 changes: 1 addition & 1 deletion spec/timezone.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ <h1>Temporal.TimeZone ( _identifier_ )</h1>
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 *TypeError* exception.
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*.
Expand Down

0 comments on commit 992acd6

Please sign in to comment.