Skip to content

Commit

Permalink
Add timeZone option to Instant.toString()
Browse files Browse the repository at this point in the history
This adds an option for the time zone in which to print an Instant,
instead of having it be a separate argument. Also removes printing the
bracketed name from Instant.

Closes: #741
  • Loading branch information
ptomato committed Nov 3, 2020
1 parent a368ad3 commit 4367e85
Show file tree
Hide file tree
Showing 10 changed files with 133 additions and 151 deletions.
9 changes: 6 additions & 3 deletions docs/cookbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,13 @@ An example of combining a day on the calendar (`Temporal.MonthDay`) and a year i

### Zoned instant from instant and time zone

Use the optional parameter of `Temporal.Instant.prototype.toString()` to map an exact-time Temporal.Instant instance and a time zone name, into a string serialization of the wall-clock time in that time zone corresponding to the exact time.
To serialize an exact-time Temporal.Instant into a string, use `toString()`.
Without any arguments, this gives you a string in UTC time.

Without the parameter, `Temporal.Instant.prototype.toString()` gives a serialization in UTC time.
Using the parameter is useful if you need your serialized strings to be in a specific time zone.
If you need your string to include a UTC offset, then use the `timeZone` option of `Temporal.Instant.prototype.toString()` which will return a string serialization of the wall-clock time in that time zone corresponding to the exact time.

This loses the information about which time zone the string was in, because it only preserves the UTC offset from the time zone at that particular exact time.
If you need your string to include the time zone name, use Temporal.ZonedDateTime instead, which retains this information.

```javascript
{{cookbook/getParseableZonedStringAtInstant.mjs}}
Expand Down
26 changes: 14 additions & 12 deletions docs/cookbook/getInstantWithLocalTimeInZone.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@ const germany = Temporal.TimeZone.from('Europe/Berlin');
const nonexistentGermanWallTime = Temporal.DateTime.from('2019-03-31T02:45');

const germanResults = {
earlier: /* */ '2019-03-31T01:45:00+01:00[Europe/Berlin]',
later: /* */ '2019-03-31T03:45:00+02:00[Europe/Berlin]',
compatible: /* */ '2019-03-31T03:45:00+02:00[Europe/Berlin]',
clipEarlier: /* */ '2019-03-31T01:59:59.999999999+01:00[Europe/Berlin]',
clipLater: /* */ '2019-03-31T03:00:00+02:00[Europe/Berlin]'
earlier: /* */ '2019-03-31T01:45:00+01:00',
later: /* */ '2019-03-31T03:45:00+02:00',
compatible: /* */ '2019-03-31T03:45:00+02:00',
clipEarlier: /* */ '2019-03-31T01:59:59.999999999+01:00',
clipLater: /* */ '2019-03-31T03:00:00+02:00'
};
for (const [disambiguation, result] of Object.entries(germanResults)) {
assert.equal(
getInstantWithLocalTimeInZone(nonexistentGermanWallTime, germany, disambiguation).toString(germany),
getInstantWithLocalTimeInZone(nonexistentGermanWallTime, germany, disambiguation).toString({ timeZone: germany }),
result
);
}
Expand All @@ -69,15 +69,17 @@ const brazilEast = Temporal.TimeZone.from('America/Sao_Paulo');
const doubleEasternBrazilianWallTime = Temporal.DateTime.from('2019-02-16T23:45');

const brazilianResults = {
earlier: /* */ '2019-02-16T23:45:00-02:00[America/Sao_Paulo]',
later: /* */ '2019-02-16T23:45:00-03:00[America/Sao_Paulo]',
compatible: /* */ '2019-02-16T23:45:00-02:00[America/Sao_Paulo]',
clipEarlier: /* */ '2019-02-16T23:45:00-02:00[America/Sao_Paulo]',
clipLater: /* */ '2019-02-16T23:45:00-03:00[America/Sao_Paulo]'
earlier: /* */ '2019-02-16T23:45:00-02:00',
later: /* */ '2019-02-16T23:45:00-03:00',
compatible: /* */ '2019-02-16T23:45:00-02:00',
clipEarlier: /* */ '2019-02-16T23:45:00-02:00',
clipLater: /* */ '2019-02-16T23:45:00-03:00'
};
for (const [disambiguation, result] of Object.entries(brazilianResults)) {
assert.equal(
getInstantWithLocalTimeInZone(doubleEasternBrazilianWallTime, brazilEast, disambiguation).toString(brazilEast),
getInstantWithLocalTimeInZone(doubleEasternBrazilianWallTime, brazilEast, disambiguation).toString({
timeZone: brazilEast
}),
result
);
}
17 changes: 10 additions & 7 deletions docs/cookbook/getParseableZonedStringAtInstant.mjs
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
const instant = Temporal.Instant.from('2020-01-03T10:41:51Z');

const result = instant.toString('Europe/Paris');
const result = instant.toString();

assert.equal(result, '2020-01-03T11:41:51+01:00[Europe/Paris]');
assert.equal(result, '2020-01-03T10:41:51Z');
assert(instant.equals(Temporal.Instant.from(result)));

// With an offset:
// Include the UTC offset of a particular time zone:

const result2 = instant.toString('-07:00');
const result2 = instant.toString({ timeZone: 'America/Yellowknife' });

assert.equal(result2, '2020-01-03T03:41:51-07:00');
assert(instant.equals(Temporal.Instant.from(result2)));

// With a Temporal.TimeZone object:
// Include the UTC offset as well as preserving the time zone name:

const timeZone = Temporal.TimeZone.from('Asia/Seoul');
const result3 = instant.toString(timeZone);
const zoned = instant.toZonedDateTimeISO('Asia/Seoul');
const result3 = zoned.toString();

assert.equal(result3, '2020-01-03T19:41:51+09:00[Asia/Seoul]');
assert(instant.equals(Temporal.Instant.from(result3)));
assert(zoned.equals(Temporal.ZonedDateTime.from(result3)));
14 changes: 9 additions & 5 deletions docs/instant.md
Original file line number Diff line number Diff line change
Expand Up @@ -545,14 +545,14 @@ one.equals(two); // => false
one.equals(one); // => true
```

### instant.**toString**(_timeZone_?: object | string, _options_?: object) : string
### instant.**toString**(_options_?: object) : string

**Parameters:**

- `timeZone` (optional string or object): the time zone to express `instant` 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.
- `options` (optional object): An object with properties representing options for the operation.
The following options are recognized:
- `timeZone` (string or object): the time zone to express `instant` 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.
- `fractionalSecondDigits` (number or string): How many digits to print after the decimal point in the output string.
Valid values are `'auto'`, 0, 1, 2, 3, 4, 5, 6, 7, 8, or 9.
The default is `'auto'`.
Expand All @@ -574,13 +574,17 @@ If no options are given, the default is `fractionalSecondDigits: 'auto'`, which
The value is truncated to fit the requested precision, unless a different rounding mode is given with the `roundingMode` option, as in `Temporal.DateTime.round()`.
Note that rounding may change the value of other units as well.

If the `timeZone` option is given, then the string will express the time in the given time zone, and contain the time zone's UTC offset.

Example usage:

```js
instant = Temporal.Instant.fromEpochMilliseconds(1574074321816);
instant.toString(); // => 2019-11-18T10:52:01.816Z
instant.toString(Temporal.TimeZone.from('UTC')); // => 2019-11-18T10:52:01.816Z
instant.toString('Asia/Seoul'); // => 2019-11-18T19:52:01.816+09:00[Asia/Seoul]
instant.toString({ timeZone: Temporal.TimeZone.from('UTC') });
// => 2019-11-18T10:52:01.816+00:00
instant.toString({ timeZone: 'Asia/Seoul' });
// => 2019-11-18T19:52:01.816+09:00

instant.toString(undefined, { smallestUnit: 'minute' });
// => 2019-11-18T10:52Z
Expand Down
8 changes: 7 additions & 1 deletion polyfill/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ export namespace Temporal {
}
>;

export type InstantToStringOptions = Partial<
ToStringPrecisionOptions & {
timeZone: TimeZoneProtocol | string;
}
>;

/**
* Options to control the result of `until()` and `since()` methods in
* `Temporal` types.
Expand Down Expand Up @@ -567,7 +573,7 @@ export namespace Temporal {
toZonedDateTimeISO(tzLike: TimeZoneProtocol | string): Temporal.ZonedDateTime;
toLocaleString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string;
toJSON(): string;
toString(tzLike?: TimeZoneProtocol | string, options?: ToStringPrecisionOptions): string;
toString(options?: InstantToStringOptions): string;
valueOf(): never;
}

Expand Down
23 changes: 7 additions & 16 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1370,20 +1370,6 @@ export const ES = ObjectAssign({}, ES2020, {
}
return ES.ToString(ES.Call(toString, timeZone));
},
ISOTimeZoneString: (timeZone, instant) => {
const name = ES.TimeZoneToString(timeZone);
const offset = ES.GetOffsetStringFor(timeZone, instant);

if (name === 'UTC') {
return 'Z';
}

if (name === offset) {
return offset;
}

return `${offset}[${name}]`;
},
ISOYearString: (year) => {
let yearString;
if (year < 1000 || year > 9999) {
Expand Down Expand Up @@ -1413,7 +1399,12 @@ export const ES = ObjectAssign({}, ES2020, {
return `${secs}.${fraction}`;
},
TemporalInstantToString: (instant, timeZone, precision) => {
const dateTime = ES.GetTemporalDateTimeFor(timeZone, instant);
let outputTimeZone = timeZone;
if (outputTimeZone === undefined) {
const TemporalTimeZone = GetIntrinsic('%Temporal.TimeZone%');
outputTimeZone = new TemporalTimeZone('UTC');
}
const dateTime = ES.GetTemporalDateTimeFor(outputTimeZone, instant, 'iso8601');
const year = ES.ISOYearString(dateTime.year);
const month = ES.ISODateTimePartString(dateTime.month);
const day = ES.ISODateTimePartString(dateTime.day);
Expand All @@ -1426,7 +1417,7 @@ export const ES = ObjectAssign({}, ES2020, {
dateTime.nanosecond,
precision
);
const timeZoneString = ES.ISOTimeZoneString(timeZone, instant);
const timeZoneString = timeZone === undefined ? 'Z' : ES.GetOffsetStringFor(outputTimeZone, instant);
return `${year}-${month}-${day}T${hour}:${minute}${seconds}${timeZoneString}`;
},
TemporalDurationToString: (duration) => {
Expand Down
9 changes: 4 additions & 5 deletions polyfill/lib/instant.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,11 @@ export class Instant {
const two = GetSlot(other, EPOCHNANOSECONDS);
return bigInt(one).equals(two);
}
toString(temporalTimeZoneLike = 'UTC', options = undefined) {
toString(options = undefined) {
if (!ES.IsTemporalInstant(this)) throw new TypeError('invalid receiver');
const timeZone = ES.ToTemporalTimeZone(temporalTimeZoneLike);
options = ES.NormalizeOptionsObject(options);
let timeZone = options.timeZone;
if (timeZone !== undefined) timeZone = ES.ToTemporalTimeZone(timeZone);
const { precision, unit, increment } = ES.ToSecondsStringPrecision(options);
const roundingMode = ES.ToTemporalRoundingMode(options, 'trunc');
const ns = GetSlot(this, EPOCHNANOSECONDS);
Expand All @@ -223,9 +224,7 @@ export class Instant {
}
toJSON() {
if (!ES.IsTemporalInstant(this)) throw new TypeError('invalid receiver');
const TemporalTimeZone = GetIntrinsic('%Temporal.TimeZone%');
const timeZone = new TemporalTimeZone('UTC');
return ES.TemporalInstantToString(this, timeZone, 'auto');
return ES.TemporalInstantToString(this, undefined, 'auto');
}
toLocaleString(locales = undefined, options = undefined) {
if (!ES.IsTemporalInstant(this)) throw new TypeError('invalid receiver');
Expand Down
100 changes: 39 additions & 61 deletions polyfill/test/instant.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -93,20 +93,19 @@ describe('Instant', () => {
equal(`${instant}`, iso);
});
it('optional time zone parameter UTC', () => {
const iso = '1976-11-18T14:23:30.123456789Z';
const inst = Instant.from(iso);
const tz = Temporal.TimeZone.from('UTC');
equal(inst.toString(tz), iso);
const inst = Instant.from('1976-11-18T14:23:30.123456789Z');
const timeZone = Temporal.TimeZone.from('UTC');
equal(inst.toString({ timeZone }), '1976-11-18T14:23:30.123456789+00:00');
});
it('optional time zone parameter non-UTC', () => {
const inst = Instant.from('1976-11-18T14:23:30.123456789Z');
const tz = Temporal.TimeZone.from('America/New_York');
equal(inst.toString(tz), '1976-11-18T09:23:30.123456789-05:00[America/New_York]');
const timeZone = Temporal.TimeZone.from('America/New_York');
equal(inst.toString({ timeZone }), '1976-11-18T09:23:30.123456789-05:00');
});
it('sub-minute offset', () => {
const inst = Instant.from('1900-01-01T12:00Z');
const tz = Temporal.TimeZone.from('Europe/Amsterdam');
equal(inst.toString(tz), '1900-01-01T12:19:32+00:19:32[Europe/Amsterdam]');
const timeZone = Temporal.TimeZone.from('Europe/Amsterdam');
equal(inst.toString({ timeZone }), '1900-01-01T12:19:32+00:19:32');
});
const i1 = Instant.from('1976-11-18T15:23Z');
const i2 = Instant.from('1976-11-18T15:23:30Z');
Expand All @@ -117,103 +116,82 @@ describe('Instant', () => {
equal(i3.toString(), '1976-11-18T15:23:30.1234Z');
});
it('truncates to minute', () => {
[i1, i2, i3].forEach((i) => equal(i.toString(undefined, { smallestUnit: 'minute' }), '1976-11-18T15:23Z'));
[i1, i2, i3].forEach((i) => equal(i.toString({ smallestUnit: 'minute' }), '1976-11-18T15:23Z'));
});
it('other smallestUnits are aliases for fractional digits', () => {
equal(i3.toString(undefined, { smallestUnit: 'second' }), i3.toString(undefined, { fractionalSecondDigits: 0 }));
equal(
i3.toString(undefined, { smallestUnit: 'millisecond' }),
i3.toString(undefined, { fractionalSecondDigits: 3 })
);
equal(
i3.toString(undefined, { smallestUnit: 'microsecond' }),
i3.toString(undefined, { fractionalSecondDigits: 6 })
);
equal(
i3.toString(undefined, { smallestUnit: 'nanosecond' }),
i3.toString(undefined, { fractionalSecondDigits: 9 })
);
equal(i3.toString({ smallestUnit: 'second' }), i3.toString({ fractionalSecondDigits: 0 }));
equal(i3.toString({ smallestUnit: 'millisecond' }), i3.toString({ fractionalSecondDigits: 3 }));
equal(i3.toString({ smallestUnit: 'microsecond' }), i3.toString({ fractionalSecondDigits: 6 }));
equal(i3.toString({ smallestUnit: 'nanosecond' }), i3.toString({ fractionalSecondDigits: 9 }));
});
it('throws on invalid or disallowed smallestUnit', () => {
['era', 'year', 'month', 'day', 'hour', 'nonsense'].forEach((smallestUnit) =>
throws(() => i1.toString(undefined, { smallestUnit }), RangeError)
throws(() => i1.toString({ smallestUnit }), RangeError)
);
});
it('accepts plural units', () => {
equal(i3.toString(undefined, { smallestUnit: 'minutes' }), i3.toString(undefined, { smallestUnit: 'minute' }));
equal(i3.toString(undefined, { smallestUnit: 'seconds' }), i3.toString(undefined, { smallestUnit: 'second' }));
equal(
i3.toString(undefined, { smallestUnit: 'milliseconds' }),
i3.toString(undefined, { smallestUnit: 'millisecond' })
);
equal(
i3.toString(undefined, { smallestUnit: 'microseconds' }),
i3.toString(undefined, { smallestUnit: 'microsecond' })
);
equal(
i3.toString(undefined, { smallestUnit: 'nanoseconds' }),
i3.toString(undefined, { smallestUnit: 'nanosecond' })
);
equal(i3.toString({ smallestUnit: 'minutes' }), i3.toString({ smallestUnit: 'minute' }));
equal(i3.toString({ smallestUnit: 'seconds' }), i3.toString({ smallestUnit: 'second' }));
equal(i3.toString({ smallestUnit: 'milliseconds' }), i3.toString({ smallestUnit: 'millisecond' }));
equal(i3.toString({ smallestUnit: 'microseconds' }), i3.toString({ smallestUnit: 'microsecond' }));
equal(i3.toString({ smallestUnit: 'nanoseconds' }), i3.toString({ smallestUnit: 'nanosecond' }));
});
it('truncates or pads to 2 places', () => {
const options = { fractionalSecondDigits: 2 };
equal(i1.toString(undefined, options), '1976-11-18T15:23:00.00Z');
equal(i2.toString(undefined, options), '1976-11-18T15:23:30.00Z');
equal(i3.toString(undefined, options), '1976-11-18T15:23:30.12Z');
equal(i1.toString(options), '1976-11-18T15:23:00.00Z');
equal(i2.toString(options), '1976-11-18T15:23:30.00Z');
equal(i3.toString(options), '1976-11-18T15:23:30.12Z');
});
it('pads to 7 places', () => {
const options = { fractionalSecondDigits: 7 };
equal(i1.toString(undefined, options), '1976-11-18T15:23:00.0000000Z');
equal(i2.toString(undefined, options), '1976-11-18T15:23:30.0000000Z');
equal(i3.toString(undefined, options), '1976-11-18T15:23:30.1234000Z');
equal(i1.toString(options), '1976-11-18T15:23:00.0000000Z');
equal(i2.toString(options), '1976-11-18T15:23:30.0000000Z');
equal(i3.toString(options), '1976-11-18T15:23:30.1234000Z');
});
it('auto is the default', () => {
[i1, i2, i3].forEach((i) => equal(i.toString(undefined, { fractionalSecondDigits: 'auto' }), i.toString()));
[i1, i2, i3].forEach((i) => equal(i.toString({ fractionalSecondDigits: 'auto' }), i.toString()));
});
it('throws on out of range or invalid fractionalSecondDigits', () => {
[-1, 10, Infinity, NaN, 'not-auto'].forEach((fractionalSecondDigits) =>
throws(() => i1.toString(undefined, { fractionalSecondDigits }), RangeError)
throws(() => i1.toString({ fractionalSecondDigits }), RangeError)
);
});
it('accepts and truncates fractional fractionalSecondDigits', () => {
equal(i3.toString(undefined, { fractionalSecondDigits: 5.5 }), '1976-11-18T15:23:30.12340Z');
equal(i3.toString({ fractionalSecondDigits: 5.5 }), '1976-11-18T15:23:30.12340Z');
});
it('smallestUnit overrides fractionalSecondDigits', () => {
equal(i3.toString(undefined, { smallestUnit: 'minute', fractionalSecondDigits: 9 }), '1976-11-18T15:23Z');
equal(i3.toString({ smallestUnit: 'minute', fractionalSecondDigits: 9 }), '1976-11-18T15:23Z');
});
it('throws on invalid roundingMode', () => {
throws(() => i1.toString(undefined, { roundingMode: 'cile' }), RangeError);
throws(() => i1.toString({ roundingMode: 'cile' }), RangeError);
});
it('rounds to nearest', () => {
equal(i2.toString(undefined, { smallestUnit: 'minute', roundingMode: 'nearest' }), '1976-11-18T15:24Z');
equal(i3.toString(undefined, { fractionalSecondDigits: 3, roundingMode: 'nearest' }), '1976-11-18T15:23:30.123Z');
equal(i2.toString({ smallestUnit: 'minute', roundingMode: 'nearest' }), '1976-11-18T15:24Z');
equal(i3.toString({ fractionalSecondDigits: 3, roundingMode: 'nearest' }), '1976-11-18T15:23:30.123Z');
});
it('rounds up', () => {
equal(i2.toString(undefined, { smallestUnit: 'minute', roundingMode: 'ceil' }), '1976-11-18T15:24Z');
equal(i3.toString(undefined, { fractionalSecondDigits: 3, roundingMode: 'ceil' }), '1976-11-18T15:23:30.124Z');
equal(i2.toString({ smallestUnit: 'minute', roundingMode: 'ceil' }), '1976-11-18T15:24Z');
equal(i3.toString({ fractionalSecondDigits: 3, roundingMode: 'ceil' }), '1976-11-18T15:23:30.124Z');
});
it('rounds down', () => {
['floor', 'trunc'].forEach((roundingMode) => {
equal(i2.toString(undefined, { smallestUnit: 'minute', roundingMode }), '1976-11-18T15:23Z');
equal(i3.toString(undefined, { fractionalSecondDigits: 3, roundingMode }), '1976-11-18T15:23:30.123Z');
equal(i2.toString({ smallestUnit: 'minute', roundingMode }), '1976-11-18T15:23Z');
equal(i3.toString({ fractionalSecondDigits: 3, roundingMode }), '1976-11-18T15:23:30.123Z');
});
});
it('rounding down is towards the Big Bang, not towards 1 BCE', () => {
const i4 = Instant.from('-000099-12-15T12:00:00.5Z');
equal(i4.toString(undefined, { smallestUnit: 'second', roundingMode: 'floor' }), '-000099-12-15T12:00:00Z');
equal(i4.toString({ smallestUnit: 'second', roundingMode: 'floor' }), '-000099-12-15T12:00:00Z');
});
it('rounding can affect all units', () => {
const i5 = Instant.from('1999-12-31T23:59:59.999999999Z');
equal(
i5.toString(undefined, { fractionalSecondDigits: 8, roundingMode: 'nearest' }),
'2000-01-01T00:00:00.00000000Z'
);
equal(i5.toString({ fractionalSecondDigits: 8, roundingMode: 'nearest' }), '2000-01-01T00:00:00.00000000Z');
});
it('options may only be an object or undefined', () => {
[null, 1, 'hello', true, Symbol('foo'), 1n].forEach((badOptions) =>
throws(() => i1.toString(undefined, badOptions), TypeError)
throws(() => i1.toString(badOptions), TypeError)
);
[{}, () => {}, undefined].forEach((options) => equal(i1.toString(undefined, options), '1976-11-18T15:23:00Z'));
[{}, () => {}, undefined].forEach((options) => equal(i1.toString(options), '1976-11-18T15:23:00Z'));
});
});
describe('Instant.toJSON() works', () => {
Expand Down
Loading

0 comments on commit 4367e85

Please sign in to comment.