Skip to content

Commit

Permalink
Add precision options for toString on types with time
Browse files Browse the repository at this point in the history
On DateTime.toString(), Time.toString(), and Instant.toString(), we add
an options bag with the options `fractionalSecondDigits`, `smallestUnit`,
and `roundingMode`. `smallestUnit` and `fractionalSecondDigits` control
which units are output, and `roundingMode` is a rarely-needed option that
allows other rounding behaviour than truncation.

The default behaviour is changed as well. Previously seconds would not be
output if the seconds and lower components would be 0, and the precision
after the decimal point would be 0, 3, 6, or 9. After this change, seconds
are always output, and all trailing zeroes after the decimal point are
dropped.

This change requires a lot of adjustments to expected test output.

Closes: #329
  • Loading branch information
ptomato committed Oct 19, 2020
1 parent 8ac0698 commit 256206b
Show file tree
Hide file tree
Showing 32 changed files with 969 additions and 355 deletions.
2 changes: 1 addition & 1 deletion docs/cookbook/adjustDayOfMonth.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ assert.equal(lastOfThisMonth.toString(), '2020-04-30');

const thisMonth18thAt8PM = date.with({ day: 18 }).toDateTime(Temporal.Time.from('20:00'));

assert.equal(thisMonth18thAt8PM.toString(), '2020-04-18T20:00');
assert.equal(thisMonth18thAt8PM.toString(), '2020-04-18T20:00:00');
2 changes: 1 addition & 1 deletion docs/cookbook/getInstantBeforeOldRecord.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ const noticeWindow = Temporal.Duration.from({ minutes: 1 });
// notification to all the runners
const reminderAt = getInstantBeforeOldRecord(raceStart, record, noticeWindow);

assert.equal(reminderAt.toString(), '2016-08-14T00:52:17.530Z');
assert.equal(reminderAt.toString(), '2016-08-14T00:52:17.53Z');
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const instant = Temporal.Instant.from('2019-04-16T21:01Z');

const nyc = Temporal.TimeZone.from('America/New_York');
const nextTransition = getInstantOfNearestOffsetTransitionToInstant(instant, nyc, false);
assert.equal(nextTransition.toString(), '2019-11-03T06:00Z');
assert.equal(nextTransition.toString(), '2019-11-03T06:00:00Z');

// Inclusive
const sameTransition = getInstantOfNearestOffsetTransitionToInstant(nextTransition, nyc, true);
Expand Down
18 changes: 9 additions & 9 deletions docs/cookbook/getInstantWithLocalTimeInZone.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ const germany = Temporal.TimeZone.from('Europe/Berlin');
const nonexistentGermanWallTime = Temporal.DateTime.from('2019-03-31T02:45');

const germanResults = {
earlier: /* */ '2019-03-31T01:45+01:00[Europe/Berlin]',
later: /* */ '2019-03-31T03:45+02:00[Europe/Berlin]',
compatible: /* */ '2019-03-31T03:45+02:00[Europe/Berlin]',
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+02:00[Europe/Berlin]'
clipLater: /* */ '2019-03-31T03:00:00+02:00[Europe/Berlin]'
};
for (const [disambiguation, result] of Object.entries(germanResults)) {
assert.equal(
Expand All @@ -69,11 +69,11 @@ const brazilEast = Temporal.TimeZone.from('America/Sao_Paulo');
const doubleEasternBrazilianWallTime = Temporal.DateTime.from('2019-02-16T23:45');

const brazilianResults = {
earlier: /* */ '2019-02-16T23:45-02:00[America/Sao_Paulo]',
later: /* */ '2019-02-16T23:45-03:00[America/Sao_Paulo]',
compatible: /* */ '2019-02-16T23:45-02:00[America/Sao_Paulo]',
clipEarlier: /* */ '2019-02-16T23:45-02:00[America/Sao_Paulo]',
clipLater: /* */ '2019-02-16T23:45-03:00[America/Sao_Paulo]'
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]'
};
for (const [disambiguation, result] of Object.entries(brazilianResults)) {
assert.equal(
Expand Down
2 changes: 1 addition & 1 deletion docs/cookbook/getLocalizedArrival.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ const arrival = getLocalizedArrival(
'America/Los_Angeles',
'iso8601'
);
assert.equal(arrival.toString(), '2020-03-08T09:50');
assert.equal(arrival.toString(), '2020-03-08T09:50:00');
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ const result = getParseableZonedStringWithLocalTimeInOtherZone(
Temporal.TimeZone.from('America/Los_Angeles')
);
// On this date, when it's midnight in Chicago, it's 10 PM the previous night in LA
assert.equal(result, '2020-01-08T22:00-08:00[America/Los_Angeles]');
assert.equal(result, '2020-01-08T22:00:00-08:00[America/Los_Angeles]');
2 changes: 1 addition & 1 deletion docs/cookbook/getSortedLocalDateTimes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@ let c = Temporal.DateTime.from({
const results = getSortedLocalDateTimes([a, b, c]);
assert.deepEqual(
results.map((x) => x.toString()),
['2020-02-20T08:45', '2020-02-20T15:30', '2020-02-21T13:10']
['2020-02-20T08:45:00', '2020-02-20T15:30:00', '2020-02-21T13:10:00']
);
12 changes: 6 additions & 6 deletions docs/cookbook/localTimeForFutureEvents.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ const localTimes = tc39meetings.map(({ dateTime, timeZone }) => {
assert.deepEqual(
localTimes.map((dt) => dt.toString()),
[
'2019-01-29T02:00',
'2019-03-26T23:00',
'2019-06-04T17:00',
'2019-07-24T02:00',
'2019-10-01T23:00',
'2019-12-04T03:00'
'2019-01-29T02:00:00',
'2019-03-26T23:00:00',
'2019-06-04T17:00:00',
'2019-07-24T02:00:00',
'2019-10-01T23:00:00',
'2019-12-04T03:00:00'
]
);
4 changes: 2 additions & 2 deletions docs/cookbook/nextWeeklyOccurrence.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ const eventTimeZone = Temporal.TimeZone.from('America/Los_Angeles');
const rightBefore = Temporal.Instant.from('2020-03-26T08:30-07:00[America/Los_Angeles]');
const localTimeZone = Temporal.TimeZone.from('Europe/London');
let next = nextWeeklyOccurrence(rightBefore, localTimeZone, weekday, eventTime, eventTimeZone);
assert.equal(next.toString(), '2020-03-26T15:45');
assert.equal(next.toString(), '2020-03-26T15:45:00');

const rightAfter = Temporal.Instant.from('2020-03-26T09:00-07:00[America/Los_Angeles]');
next = nextWeeklyOccurrence(rightAfter, localTimeZone, weekday, eventTime, eventTimeZone);
assert.equal(next.toString(), '2020-04-02T16:45');
assert.equal(next.toString(), '2020-04-02T16:45:00');
2 changes: 1 addition & 1 deletion docs/cookbook/noonOnDate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ const date = Temporal.Date.from('2020-05-14');
const noonOnDate = date.toDateTime(Temporal.Time.from({ hour: 12 }));

assert(noonOnDate instanceof Temporal.DateTime);
assert.equal(noonOnDate.toString(), '2020-05-14T12:00');
assert.equal(noonOnDate.toString(), '2020-05-14T12:00:00');
2 changes: 1 addition & 1 deletion docs/cookbook/roundDownToWholeHours.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ const time = Temporal.Time.from('12:38:28.138818731');

const wholeHour = time.round({ smallestUnit: 'hour', roundingMode: 'floor' });

assert.equal(wholeHour.toString(), '12:00');
assert.equal(wholeHour.toString(), '12:00:00');
42 changes: 39 additions & 3 deletions docs/datetime.md
Original file line number Diff line number Diff line change
Expand Up @@ -649,18 +649,54 @@ dt1.equals(dt2); // => false
dt1.equals(dt1); // => true
```

### datetime.**toString**() : string
### datetime.**toString**(_options_?: object) : string

**Parameters:**

- `options` (optional object): An object with properties representing options for the operation.
The following options are recognized:
- `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'`.
- `smallestUnit` (string): The smallest unit of time to include in the output string.
This option overrides `fractionalSecondDigits` if both are given.
Valid values are `'minute'`, `'second'`, `'millisecond'`, `'microsecond'`, and `'nanosecond'`.
- `roundingMode` (string): How to handle the remainder.
Valid values are `'ceil'`, `'floor'`, `'trunc'`, and `'nearest'`.
The default is `'trunc'`.

**Returns:** a string in the ISO 8601 date format representing `datetime`.

This method overrides the `Object.prototype.toString()` method and provides a convenient, unambiguous string representation of `datetime`.
The string can be passed to `Temporal.DateTime.from()` to create a new `Temporal.DateTime` object.

The output precision can be controlled with the `fractionalSecondDigits` or `smallestUnit` option.
If no options are given, the default is `fractionalSecondDigits: 'auto'`, which omits trailing zeroes after the decimal point.

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.

Example usage:

```js
dt = Temporal.DateTime.from({ year: 1999, month: 12, day: 31 });
dt.toString(); // => 1999-12-31T00:00
dt = Temporal.DateTime.from({
year: 1999,
month: 12,
day: 31,
hour: 23,
minute: 59,
second: 59,
millisecond: 999,
microsecond: 999,
nanosecond: 999
});
dt.toString(); // => 1999-12-31T23:59:59.999999999

dt.toString({ smallestUnit: 'minute' }); // => 1999-12-31T23:59
dt.toString({ fractionalSecondDigits: 0 }); // => 1999-12-31T23:59:59
dt.toString({ fractionalSecondDigits: 4 }); // => 1999-12-31T23:59:59.9999
dt.toString({ fractionalSecondDigits: 8, roundingMode: 'nearest' });
// => 2000-01-01T00:00:00.00000000
```

### datetime.**toLocaleString**(_locales_?: string | array<string>, _options_?: object) : string
Expand Down
28 changes: 27 additions & 1 deletion docs/instant.md
Original file line number Diff line number Diff line change
Expand Up @@ -563,25 +563,51 @@ one.equals(two); // => false
one.equals(one); // => true
```

### instant.**toString**(_timeZone_?: object | string) : string
### instant.**toString**(_timeZone_?: object | string, _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:
- `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'`.
- `smallestUnit` (string): The smallest unit of time to include in the output string.
This option overrides `fractionalSecondDigits` if both are given.
Valid values are `'minute'`, `'second'`, `'millisecond'`, `'microsecond'`, and `'nanosecond'`.
- `roundingMode` (string): How to handle the remainder.
Valid values are `'ceil'`, `'floor'`, `'trunc'`, and `'nearest'`.
The default is `'trunc'`.

**Returns:** a string in the ISO 8601 date format representing `instant`.

This method overrides the `Object.prototype.toString()` method and provides a convenient, unambiguous string representation of `instant`.
The string can be passed to `Temporal.Instant.from()` to create a new `Temporal.Instant` object.

The output precision can be controlled with the `fractionalSecondDigits` or `smallestUnit` option.
If no options are given, the default is `fractionalSecondDigits: 'auto'`, which omits trailing zeroes after the decimal point.

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.

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(undefined, { smallestUnit: 'minute' });
// => 2019-11-18T10:52Z
instant.toString(undefined, { fractionalSecondDigits: 0 });
// => 2019-11-18T10:52:01Z
instant.toString(undefined, { fractionalSecondDigits: 4 });
// => 2019-11-18T10:52:01.8160Z
instant.toString(undefined, { smallestUnit: 'second', roundingMode: 'nearest' });
// => 2019-11-18T10:52:02Z
```

### instant.**toLocaleString**(_locales_?: string | array<string>, _options_?: object) : string
Expand Down
28 changes: 27 additions & 1 deletion docs/time.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,18 +381,44 @@ time.equals(other); // => false
time.equals(time); // => true
```

### time.**toString**() : string
### time.**toString**(_options_?: object) : string

**Parameters:**

- `options` (optional object): An object with properties representing options for the operation.
The following options are recognized:
- `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'`.
- `smallestUnit` (string): The smallest unit of time to include in the output string.
This option overrides `fractionalSecondDigits` if both are given.
Valid values are `'minute'`, `'second'`, `'millisecond'`, `'microsecond'`, and `'nanosecond'`.
- `roundingMode` (string): How to handle the remainder.
Valid values are `'ceil'`, `'floor'`, `'trunc'`, and `'nearest'`.
The default is `'trunc'`.

**Returns:** a string in the ISO 8601 time format representing `time`.

This method overrides the `Object.prototype.toString()` method and provides a convenient, unambiguous string representation of `time`.
The string can be passed to `Temporal.Time.from()` to create a new `Temporal.Time` object.

The output precision can be controlled with the `fractionalSecondDigits` or `smallestUnit` option.
If no options are given, the default is `fractionalSecondDigits: 'auto'`, which omits trailing zeroes after the decimal point.

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.

Example usage:

```js
time = Temporal.Time.from('19:39:09.068346205');
time.toString(); // => 19:39:09.068346205

time.toString({ smallestUnit: 'minute' }); // => 19:39
time.toString({ fractionalSecondDigits: 0 }); // => 19:39:09
time.toString({ fractionalSecondDigits: 4 }); // => 19:39:09.0683
time.toString({ fractionalSecondDigits: 5, roundingMode: 'nearest' })
// => 19:39:09.06835
```

### time.**toLocaleString**(_locales_?: string | array<string>, _options_?: object) : string
Expand Down
43 changes: 39 additions & 4 deletions polyfill/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export namespace Temporal {
export type ComparisonResult = -1 | 0 | 1;
type RoundingMode = 'ceil' | 'floor' | 'trunc' | 'nearest';
type ConstructorOf<T> = new (...args: unknown[]) => T;

/**
Expand Down Expand Up @@ -84,6 +85,40 @@ export namespace Temporal {
overflow: 'constrain' | 'reject';
};

/**
* Options for outputting precision in toString() on types with seconds
*/
export type ToStringOptions = {
fractionalSecondDigits?: 'auto' | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
smallestUnit?:
| 'minute'
| 'second'
| 'millisecond'
| 'microsecond'
| 'nanosecond'
| /** @deprecated */ 'minutes'
| /** @deprecated */ 'seconds'
| /** @deprecated */ 'milliseconds'
| /** @deprecated */ 'microseconds'
| /** @deprecated */ 'nanoseconds';

/**
* Controls how rounding is performed:
* - `nearest`: Round to the nearest of the values allowed by
* `roundingIncrement` and `smallestUnit`. When there is a tie, round up.
* This mode is the default.
* - `ceil`: Always round up, towards the end of time.
* - `trunc`: Always round down, towards the beginning of time.
* - `floor`: Also round down, towards the beginning of time. This mode acts
* the same as `trunc`, but it's included for consistency with
* `Temporal.Duration.round()` where negative values are allowed and
* `trunc` rounds towards zero, unlike `floor` which rounds towards
* negative infinity which is usually unexpected. For this reason, `trunc`
* is recommended for most use cases.
*/
roundingMode?: RoundingMode;
};

/**
* Options to control the result of `difference()` methods in `Temporal`
* types.
Expand Down Expand Up @@ -131,7 +166,7 @@ export namespace Temporal {
* negative infinity which is usually unexpected. For this reason, `trunc`
* is recommended for most use cases.
*/
roundingMode?: 'ceil' | 'floor' | 'trunc' | 'nearest';
roundingMode?: RoundingMode;
}

/**
Expand Down Expand Up @@ -342,7 +377,7 @@ export namespace Temporal {
toDateTimeISO(tzLike: TimeZoneProtocol | string): Temporal.DateTime;
toLocaleString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string;
toJSON(): string;
toString(tzLike?: TimeZoneProtocol | string): string;
toString(tzLike?: TimeZoneProtocol | string, options?: ToStringOptions): string;
}

export interface CalendarProtocol {
Expand Down Expand Up @@ -685,7 +720,7 @@ export namespace Temporal {
getISOFields(): DateTimeISOFields;
toLocaleString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string;
toJSON(): string;
toString(): string;
toString(options?: ToStringOptions): string;
}

export type MonthDayLike = {
Expand Down Expand Up @@ -814,7 +849,7 @@ export namespace Temporal {
getFields(): TimeFields;
toLocaleString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string;
toJSON(): string;
toString(): string;
toString(options?: ToStringOptions): string;
}

/**
Expand Down
Loading

0 comments on commit 256206b

Please sign in to comment.