Skip to content

Commit

Permalink
Allow passing in plain objects with time zone methods
Browse files Browse the repository at this point in the history
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 1, 2020
1 parent 5e4d0fd commit 4e55210
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 33 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 @@ -532,10 +532,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
27 changes: 18 additions & 9 deletions polyfill/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,10 @@ export namespace Temporal {
other: Temporal.Absolute,
options?: DifferenceOptions<'days' | 'hours' | 'minutes' | 'seconds'>
): Temporal.Duration;
inTimeZone(tzLike?: Temporal.TimeZone | string): Temporal.DateTime;
inTimeZone(tzLike?: Temporal.TimeZone | TimeZoneLike | string): Temporal.DateTime;
toLocaleString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string;
toJSON(): string;
toString(tzLike?: Temporal.TimeZone | string): string;
toString(tzLike?: Temporal.TimeZone | TimeZoneLike | string): string;
}

export type DateLike = {
Expand Down Expand Up @@ -305,7 +305,7 @@ export namespace Temporal {
other: Temporal.DateTime,
options?: DifferenceOptions<'years' | 'months' | 'days' | 'hours' | 'minutes' | 'seconds'>
): Temporal.Duration;
inTimeZone(tzLike: Temporal.TimeZone | string, options?: ToAbsoluteOptions): Temporal.Absolute;
inTimeZone(tzLike: Temporal.TimeZone | TimeZoneLike | string, options?: ToAbsoluteOptions): Temporal.Absolute;
getDate(): Temporal.Date;
getYearMonth(): Temporal.YearMonth;
getMonthDay(): Temporal.MonthDay;
Expand Down Expand Up @@ -403,6 +403,15 @@ export namespace Temporal {
toString(): string;
}

/**
* A plain object implementing the minimum for a custom time zone.
*/
export class TimeZoneLike {
getOffsetNanosecondsFor(absolute: Temporal.Absolute): number;
getPossibleAbsolutesFor(dateTime: Temporal.DateTime): Temporal.Absolute[];
toString(): 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 Down Expand Up @@ -490,32 +499,32 @@ export namespace Temporal {
/**
* Get the current calendar date and clock time in a specific time zone.
*
* @param {Temporal.TimeZone | string} [tzLike] -
* @param {Temporal.TimeZone | TimeZoneLike | 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.
*/
export function dateTime(tzLike?: Temporal.TimeZone | string): Temporal.DateTime;
export function dateTime(tzLike?: Temporal.TimeZone | TimeZoneLike | string): Temporal.DateTime;

/**
* Get the current calendar date in a specific time zone.
*
* @param {Temporal.TimeZone | string} [tzLike] -
* @param {Temporal.TimeZone | TimeZoneLike | 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.
*/
export function date(tzLike?: Temporal.TimeZone | string): Temporal.Date;
export function date(tzLike?: Temporal.TimeZone | TimeZoneLike | string): Temporal.Date;

/**
* Get the current clock time in a specific time zone.
*
* @param {Temporal.TimeZone | string} [tzLike] -
* @param {Temporal.TimeZone | TimeZoneLike | 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.
*/
export function time(tzLike?: Temporal.TimeZone | string): Temporal.Time;
export function time(tzLike?: Temporal.TimeZone | TimeZoneLike | 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 @@ -480,7 +480,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 @@ -490,7 +496,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 @@ -519,7 +525,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 4e55210

Please sign in to comment.