Skip to content

Commit

Permalink
WIP - Allow passing in plain objects with time zone methods
Browse files Browse the repository at this point in the history
FIXME: Test fails passing custom time zone ID in to Intl
FIXME: Need to remove ES.ToTemporalTimeZone and use
  Temporal.TimeZone.from throughout

The other half of the custom time zones and calendars protocol discussed
in #300 was allowing calendars and time zones to be plain objects with
the appropriate methods on them.

This adds a test verifying that it's possible to use a plain object with
getOffsetNanosecondsFor(), getPossibleAbsolutesFor(), and toString() as
a time zone, and makes the changes needed to allow that: removing brand
checks from time zone methods, and calling methods on
Temporal.TimeZone.prototype if they are not present on the plain object.
  • Loading branch information
ptomato committed Jun 5, 2020
1 parent ece70fd commit 0060c29
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 40 deletions.
8 changes: 4 additions & 4 deletions docs/absolute.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,10 @@ Same as `getEpochSeconds()`, but with nanosecond (10<sup>&minus;9</sup> second)

The value returned from this method is suitable to be passed to `new Temporal.Absolute()`.

### absolute.**inTimeZone**(_timeZone_: Temporal.TimeZone | string) : Temporal.DateTime
### absolute.**inTimeZone**(_timeZone_: object | string) : Temporal.DateTime

**Parameters:**
- `timeZone` (object or string): A `Temporal.TimeZone` object, or a string description of the time zone; either its IANA name or UTC offset.
- `timeZone` (object or string): A `Temporal.TimeZone` object, or an object implementing the [time zone protocol](./timezone.md#protocol), or a string description of the time zone; either its IANA name or UTC offset.

**Returns:** a `Temporal.DateTime` object indicating the calendar date and wall-clock time in `timeZone` at the absolute time indicated by `absolute`.

Expand Down Expand Up @@ -355,10 +355,10 @@ one.equals(two) // => false
one.equals(one) // => true
```

### absolute.**toString**(_timeZone_?: Temporal.TimeZone | string) : string
### absolute.**toString**(_timeZone_?: object | string) : string

**Parameters:**
- `timeZone` (optional string or `Temporal.TimeZone`): the time zone to express `absolute` in.
- `timeZone` (optional string or object): the time zone to express `absolute` in, as a `Temporal.TimeZone` object, an object implementing the [time zone protocol](./timezone.md#protocol), or a string.
The default is to use UTC.

**Returns:** a string in the ISO 8601 date format representing `absolute`.
Expand Down
4 changes: 2 additions & 2 deletions docs/datetime.md
Original file line number Diff line number Diff line change
Expand Up @@ -522,10 +522,10 @@ This method overrides `Object.prototype.valueOf()` and always throws an exceptio
This is because it's not possible to compare `Temporal.DateTime` objects with the relational operators `<`, `<=`, `>`, or `>=`.
Use `Temporal.DateTime.compare()` for this, or `datetime.equals()` for equality.

### datetime.**inTimeZone**(_timeZone_ : Temporal.TimeZone | string, _options_?: object) : Temporal.Absolute
### datetime.**inTimeZone**(_timeZone_ : object | string, _options_?: object) : Temporal.Absolute

**Parameters:**
- `timeZone` (optional string or `Temporal.TimeZone`): The time zone in which to interpret `dateTime`.
- `timeZone` (optional string or object): The time zone in which to interpret `dateTime`, as a `Temporal.TimeZone` object, an object implementing the [time zone protocol](./timezone.md#protocol), or a string.
- `options` (optional object): An object with properties representing options for the operation.
The following options are recognized:
- `disambiguation` (string): How to disambiguate if the date and time given by `dateTime` does not exist in the time zone, or exists more than once.
Expand Down
12 changes: 6 additions & 6 deletions docs/now.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ nextTransition.inTimeZone(tz);
// On 2020-03-08T03:00 the clock will change from UTC -08:00 to -07:00
```

### Temporal.now.**dateTime**(_timeZone_: Temporal.TimeZone | string = Temporal.now.timeZone()) : Temporal.DateTime
### Temporal.now.**dateTime**(_timeZone_: object | string = Temporal.now.timeZone()) : Temporal.DateTime

**Parameters:**
- `timeZone` (optional `Temporal.TimeZone` or string): The time zone to get the current date and time in.
- `timeZone` (optional object or string): The time zone to get the current date and time in, as a `Temporal.TimeZone` object, an object implementing the [time zone protocol](./timezone.md#protocol), or a string.
If not given, the current system time zone will be used.

**Returns:** a `Temporal.DateTime` object representing the current system date and time.
Expand All @@ -83,10 +83,10 @@ Object.entries(financialCentres).forEach(([name, timeZone]) => {
// Tokyo: 2020-01-25T14:52:14.759534758
```

### Temporal.now.**date**(_timeZone_: Temporal.TimeZone | string = Temporal.now.timeZone()) : Temporal.Date
### Temporal.now.**date**(_timeZone_: object | string = Temporal.now.timeZone()) : Temporal.Date

**Parameters:**
- `timeZone` (optional `Temporal.TimeZone` or string): The time zone to get the current date and time in.
- `timeZone` (optional object or string): The time zone to get the current date and time in, as a `Temporal.TimeZone` object, an object implementing the [time zone protocol](./timezone.md#protocol), or a string.
If not given, the current system time zone will be used.

**Returns:** a `Temporal.Date` object representing the current system date.
Expand All @@ -98,10 +98,10 @@ date = Temporal.now.date();
if (date.month === 2 && date.day === 29) console.log('Leap Day!');
```

### Temporal.now.**time**(_timeZone_: Temporal.TimeZone | string = Temporal.now.timeZone()) : Temporal.Time
### Temporal.now.**time**(_timeZone_: object | string = Temporal.now.timeZone()) : Temporal.Time

**Parameters:**
- `timeZone` (optional `Temporal.TimeZone` or string): The time zone to get the current date and time in.
- `timeZone` (optional object or string): The time zone to get the current date and time in, as a `Temporal.TimeZone` object, an object implementing the [time zone protocol](./timezone.md#protocol), or a string.
If not given, the current system time zone will be used.

**Returns:** a `Temporal.Time` object representing the current system time.
Expand Down
2 changes: 0 additions & 2 deletions docs/timezone-draft.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,6 @@ For example, `getOffsetStringFor()` and `getDateTimeFor()` call `getOffsetNanose
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 `getOffsetNanosecondsFor()`, `getPossibleAbsolutesFor()`, and `toString()`.
> **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.getOffsetStringFor.call(plainObject, absolute)`.
## Show Me The Code
Here's what it could look like to implement the built-in offset-based time zones as custom time zones.
Expand Down
6 changes: 6 additions & 0 deletions docs/timezone.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ 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()`, `getPossibleAbsolutesFor()`, and `getTransitions()`.
Any subclass of `Temporal.TimeZone` will be accepted in Temporal APIs where a built-in `Temporal.TimeZone` would work.

### Protocol

It's also possible for a plain object to be a custom time zone, without subclassing.
The object must have `getOffsetNanosecondsFor()`, `getPossibleAbsolutesFor()`, and `toString()` methods.
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
50 changes: 34 additions & 16 deletions polyfill/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,10 @@ export namespace Temporal {
other: Temporal.Absolute,
options?: DifferenceOptions<'days' | 'hours' | 'minutes' | 'seconds'>
): Temporal.Duration;
inTimeZone(tzLike?: Temporal.TimeZone | string): Temporal.DateTime;
inTimeZone(tzLike?: TimeZoneProtocol | string): Temporal.DateTime;
toLocaleString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string;
toJSON(): string;
toString(tzLike?: Temporal.TimeZone | string): string;
toString(tzLike?: TimeZoneProtocol | string): string;
}

/**
Expand Down Expand Up @@ -385,7 +385,7 @@ export namespace Temporal {
other: Temporal.DateTime,
options?: DifferenceOptions<'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds'>
): Temporal.Duration;
inTimeZone(tzLike: Temporal.TimeZone | string, options?: ToAbsoluteOptions): Temporal.Absolute;
inTimeZone(tzLike: TimeZoneProtocol | string, options?: ToAbsoluteOptions): Temporal.Absolute;
getDate(): Temporal.Date;
getYearMonth(): Temporal.YearMonth;
getMonthDay(): Temporal.MonthDay;
Expand Down Expand Up @@ -483,6 +483,21 @@ export namespace Temporal {
toString(): string;
}

/**
* A plain object implementing the protocol for a custom time zone.
*/
class TimeZoneProtocol {
name?: string;
getOffsetNanosecondsFor(absolute: Temporal.Absolute): number;
getOffsetStringFor?(absolute: Temporal.Absolute): string;
getDateTimeFor?(absolute: Temporal.Absolute): Temporal.DateTime;
getAbsoluteFor?(dateTime: Temporal.DateTime, options?: ToAbsoluteOptions): Temporal.Absolute;
getTransitions?(startingPoint: Temporal.Absolute): IteratorResult<Temporal.Absolute>;
getPossibleAbsolutesFor(dateTime: Temporal.DateTime): Temporal.Absolute[];
toString(): string;
toJSON?(): string;
}

/**
* A `Temporal.TimeZone` is a representation of a time zone: either an
* {@link https://www.iana.org/time-zones|IANA time zone}, including
Expand All @@ -496,7 +511,7 @@ export namespace Temporal {
*
* See https://tc39.es/proposal-temporal/docs/timezone.html for more details.
*/
export class TimeZone {
export class TimeZone implements Required<TimeZoneProtocol> {
static from(timeZone: Temporal.TimeZone | string): Temporal.TimeZone;
constructor(timeZoneIdentifier: string);
readonly name: string;
Expand Down Expand Up @@ -570,32 +585,35 @@ export namespace Temporal {
/**
* Get the current calendar date and clock time in a specific time zone.
*
* @param {Temporal.TimeZone | string} [tzLike] -
* @param {TimeZoneProtocol | string} [tzLike] -
* {@link https://en.wikipedia.org/wiki/List_of_tz_database_time_zones|IANA time zone identifier}
* string (e.g. `'Europe/London'`) or a `Temporal.TimeZone` instance. If omitted,
* the environment's current time zone will be used.
* string (e.g. `'Europe/London'`), `Temporal.TimeZone` instance, or an
* object implementing the time zone protocol. If omitted, the environment's
* current time zone will be used.
*/
export function dateTime(tzLike?: Temporal.TimeZone | string): Temporal.DateTime;
export function dateTime(tzLike?: TimeZoneProtocol | string): Temporal.DateTime;

/**
* Get the current calendar date in a specific time zone.
*
* @param {Temporal.TimeZone | string} [tzLike] -
* @param {TimeZoneProtocol | string} [tzLike] -
* {@link https://en.wikipedia.org/wiki/List_of_tz_database_time_zones|IANA time zone identifier}
* string (e.g. `'Europe/London'`) or a `Temporal.TimeZone` instance. If omitted,
* the environment's current time zone will be used.
* string (e.g. `'Europe/London'`), `Temporal.TimeZone` instance, or an
* object implementing the time zone protocol. If omitted, the environment's
* current time zone will be used.
*/
export function date(tzLike?: Temporal.TimeZone | string): Temporal.Date;
export function date(tzLike?: TimeZoneProtocol | string): Temporal.Date;

/**
* Get the current clock time in a specific time zone.
*
* @param {Temporal.TimeZone | string} [tzLike] -
* @param {TimeZoneProtocol | string} [tzLike] -
* {@link https://en.wikipedia.org/wiki/List_of_tz_database_time_zones|IANA time zone identifier}
* string (e.g. `'Europe/London'`) or a `Temporal.TimeZone` instance. If omitted,
* the environment's current time zone will be used.
* string (e.g. `'Europe/London'`), `Temporal.TimeZone` instance, or an
* object implementing the time zone protocol. If omitted, the environment's
* current time zone will be used.
*/
export function time(tzLike?: Temporal.TimeZone | string): Temporal.Time;
export function time(tzLike?: TimeZoneProtocol | string): Temporal.Time;

/**
* Get the environment's current time zone.
Expand Down
10 changes: 7 additions & 3 deletions polyfill/lib/absolute.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ export class Absolute {
}
toString(temporalTimeZoneLike = 'UTC') {
if (!ES.IsTemporalAbsolute(this)) throw new TypeError('invalid receiver');
const timeZone = ES.ToTemporalTimeZone(temporalTimeZoneLike);
let timeZone = temporalTimeZoneLike;
if (typeof timeZone !== 'object') timeZone = ES.ToTemporalTimeZone(timeZone);
return ES.TemporalAbsoluteToString(this, timeZone);
}
toJSON() {
Expand All @@ -145,8 +146,11 @@ export class Absolute {
}
inTimeZone(temporalTimeZoneLike = 'UTC') {
if (!ES.IsTemporalAbsolute(this)) throw new TypeError('invalid receiver');
const timeZone = ES.ToTemporalTimeZone(temporalTimeZoneLike);
return timeZone.getDateTimeFor(this);
let timeZone = temporalTimeZoneLike;
if (typeof timeZone !== 'object') timeZone = ES.ToTemporalTimeZone(timeZone);
if (typeof timeZone.getDateTimeFor === 'function') return timeZone.getDateTimeFor(this);
const TemporalTimeZone = GetIntrinsic('%Temporal.TimeZone%');
return TemporalTimeZone.prototype.getDateTimeFor.call(timeZone, this);
}

static fromEpochSeconds(epochSeconds) {
Expand Down
7 changes: 5 additions & 2 deletions polyfill/lib/datetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,12 @@ export class DateTime {

inTimeZone(temporalTimeZoneLike = 'UTC', options) {
if (!ES.IsTemporalDateTime(this)) throw new TypeError('invalid receiver');
const timeZone = ES.ToTemporalTimeZone(temporalTimeZoneLike);
let timeZone = temporalTimeZoneLike;
if (typeof timeZone !== 'object') timeZone = ES.ToTemporalTimeZone(timeZone);
const disambiguation = ES.ToTimeZoneTemporalDisambiguation(options);
return timeZone.getAbsoluteFor(this, { disambiguation });
if (typeof timeZone.getAbsoluteFor === 'function') return timeZone.getAbsoluteFor(this, { disambiguation });
const TemporalTimeZone = GetIntrinsic('%Temporal.TimeZone%');
return TemporalTimeZone.prototype.getAbsoluteFor.call(timeZone, this, { disambiguation });
}
getDate() {
if (!ES.IsTemporalDateTime(this)) throw new TypeError('invalid receiver');
Expand Down
18 changes: 15 additions & 3 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,13 @@ export const ES = ObjectAssign({}, ES2019, {
return result;
},
ISOTimeZoneString: (timeZone, absolute) => {
const offset = timeZone.getOffsetStringFor(absolute);
let offset;
if (typeof timeZone.getOffsetStringFor === 'function') {
offset = timeZone.getOffsetStringFor(absolute);
} else {
const TemporalTimeZone = GetIntrinsic('%Temporal.TimeZone%');
offset = TemporalTimeZone.prototype.getOffsetStringFor.call(timeZone, absolute);
}
let timeZoneString;
switch (true) {
case 'UTC' === timeZone.name:
Expand All @@ -495,7 +501,7 @@ export const ES = ObjectAssign({}, ES2019, {
timeZoneString = offset;
break;
default:
timeZoneString = `${offset}[${timeZone.name}]`;
timeZoneString = `${offset}[${timeZone.toString()}]`;
break;
}
return timeZoneString;
Expand Down Expand Up @@ -524,7 +530,13 @@ export const ES = ObjectAssign({}, ES2019, {
return `${secs}${post}`;
},
TemporalAbsoluteToString: (absolute, timeZone) => {
const dateTime = timeZone.getDateTimeFor(absolute);
let dateTime;
if (typeof timeZone.getDateTimeFor === 'function') {
dateTime = timeZone.getDateTimeFor(absolute);
} else {
const TemporalTimeZone = GetIntrinsic('%Temporal.TimeZone%');
dateTime = TemporalTimeZone.prototype.getDateTimeFor.call(timeZone, absolute);
}
const year = ES.ISOYearString(dateTime.year);
const month = ES.ISODateTimePartString(dateTime.month);
const day = ES.ISODateTimePartString(dateTime.day);
Expand Down
2 changes: 0 additions & 2 deletions polyfill/lib/timezone.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ export class TimeZone {
return ES.FormatTimeZoneOffsetString(offsetNs);
}
getDateTimeFor(absolute) {
if (!ES.IsTemporalTimeZone(this)) throw new TypeError('invalid receiver');
if (!ES.IsTemporalAbsolute(absolute)) throw new TypeError('invalid Absolute object');
const ns = GetSlot(absolute, EPOCHNANOSECONDS);
const offsetNs = this.getOffsetNanosecondsFor(absolute);
Expand All @@ -88,7 +87,6 @@ export class TimeZone {
return new DateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond);
}
getAbsoluteFor(dateTime, options) {
if (!ES.IsTemporalTimeZone(this)) throw new TypeError('invalid receiver');
if (!ES.IsTemporalDateTime(dateTime)) throw new TypeError('invalid DateTime object');
const disambiguation = ES.ToTimeZoneTemporalDisambiguation(options);

Expand Down
69 changes: 69 additions & 0 deletions polyfill/test/usertimezone.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,75 @@ describe('Userland time zone', () => {
});
});
});
describe('Trivial protocol implementation', () => {
const obj = {
getOffsetNanosecondsFor(/* absolute */) {
return 0;
},
getPossibleAbsolutesFor(dateTime) {
const { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = dateTime;
const dayNum = MakeDay(year, month, day);
const time = MakeTime(hour, minute, second, millisecond, microsecond, nanosecond);
const epochNs = MakeDate(dayNum, time);
return [new Temporal.Absolute(epochNs)];
},
toString() {
return 'Etc/Custom_UTC_Protocol';
}
};

const abs = Temporal.Absolute.fromEpochNanoseconds(0n);
const dt = new Temporal.DateTime(1976, 11, 18, 15, 23, 30, 123, 456, 789);

it('has offset string +00:00', () =>
equal(Temporal.TimeZone.prototype.getOffsetStringFor.call(obj, abs), '+00:00'));
it('converts to DateTime', () => {
equal(`${Temporal.TimeZone.prototype.getDateTimeFor.call(obj, abs)}`, '1970-01-01T00:00');
equal(`${abs.inTimeZone(obj)}`, '1970-01-01T00:00');
});
it('converts to Absolute', () => {
equal(`${Temporal.TimeZone.prototype.getAbsoluteFor.call(obj, dt)}`, '1976-11-18T15:23:30.123456789Z');
equal(`${dt.inTimeZone(obj)}`, '1976-11-18T15:23:30.123456789Z');
});
it('prints in absolute.toString', () =>
equal(abs.toString(obj), '1970-01-01T00:00+00:00[Etc/Custom_UTC_Protocol]'));
it('works in Temporal.now', () => {
assert(Temporal.now.dateTime(obj) instanceof Temporal.DateTime);
assert(Temporal.now.date(obj) instanceof Temporal.Date);
assert(Temporal.now.time(obj) instanceof Temporal.Time);
});
describe('Making available globally', () => {
const originalTemporalTimeZoneFrom = Temporal.TimeZone.from;
before(() => {
Temporal.TimeZone.from = function(item) {
let id;
if (item instanceof Temporal.TimeZone) {
id = item.name;
} else {
id = `${item}`;
// TODO: Use Temporal.parse here to extract the ID from an ISO string
}
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');
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]');
assert(Object.is(tz, obj));
});
it('works for Absolute.from', () => {
const abs = Temporal.Absolute.from('1970-01-01T00:00+00:00[Etc/Custom_UTC_Protocol]');
equal(`${abs}`, '1970-01-01T00:00Z');
});
after(() => {
Temporal.TimeZone.from = originalTemporalTimeZoneFrom;
});
});
});
});

const nsPerDay = 86400_000_000_000n;
Expand Down

0 comments on commit 0060c29

Please sign in to comment.