Skip to content

Commit

Permalink
Fix rounding in ZonedDateTime.toString()
Browse files Browse the repository at this point in the history
The rounding in ZonedDateTime.toString() only affects time units. Unlike
the rounding in ZonedDateTime.round(), it should be exact time rounding,
so that the actual difference between the raw and rounded exact times is
not affected by time zone offset shifts.

This is the same outcome as in round() (and we add a test for both round()
and toString() to prove it) but in toString() it can be implemented with
fewer observable calls since we don't need to be able to round to units
that are affected by DST shifts.

See: #569
  • Loading branch information
ptomato committed Nov 7, 2020
1 parent 99794dd commit f12d87f
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 36 deletions.
50 changes: 19 additions & 31 deletions polyfill/lib/zoneddatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -737,44 +737,32 @@ function zonedDateTimeToString(
showOffset = 'auto',
options = undefined
) {
const dt = dateTime(zdt);
let year = GetSlot(dt, ISO_YEAR);
let month = GetSlot(dt, ISO_MONTH);
let day = GetSlot(dt, ISO_DAY);
let hour = GetSlot(dt, ISO_HOUR);
let minute = GetSlot(dt, ISO_MINUTE);
let second = GetSlot(dt, ISO_SECOND);
let millisecond = GetSlot(dt, ISO_MILLISECOND);
let microsecond = GetSlot(dt, ISO_MICROSECOND);
let nanosecond = GetSlot(dt, ISO_NANOSECOND);
let instant = GetSlot(zdt, INSTANT);

if (options) {
const { unit, increment, roundingMode } = options;
({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = ES.RoundDateTime(
year,
month,
day,
hour,
minute,
second,
millisecond,
microsecond,
nanosecond,
increment,
unit,
roundingMode
));
const ns = ES.RoundInstant(GetSlot(zdt, EPOCHNANOSECONDS), increment, unit, roundingMode);
const TemporalInstant = GetIntrinsic('%Temporal.Instant%');
instant = new TemporalInstant(ns);
}

year = ES.ISOYearString(year);
month = ES.ISODateTimePartString(month);
day = ES.ISODateTimePartString(day);
hour = ES.ISODateTimePartString(hour);
minute = ES.ISODateTimePartString(minute);
const seconds = ES.FormatSecondsStringPart(second, millisecond, microsecond, nanosecond, precision);
const tz = GetSlot(zdt, TIME_ZONE);
const dateTime = ES.GetTemporalDateTimeFor(tz, instant, 'iso8601');

const year = ES.ISOYearString(GetSlot(dateTime, ISO_YEAR));
const month = ES.ISODateTimePartString(GetSlot(dateTime, ISO_MONTH));
const day = ES.ISODateTimePartString(GetSlot(dateTime, ISO_DAY));
const hour = ES.ISODateTimePartString(GetSlot(dateTime, ISO_HOUR));
const minute = ES.ISODateTimePartString(GetSlot(dateTime, ISO_MINUTE));
const seconds = ES.FormatSecondsStringPart(
GetSlot(dateTime, ISO_SECOND),
GetSlot(dateTime, ISO_MILLISECOND),
GetSlot(dateTime, ISO_MICROSECOND),
GetSlot(dateTime, ISO_NANOSECOND),
precision
);
let result = `${year}-${month}-${day}T${hour}:${minute}${seconds}`;
if (showOffset !== 'never') result += ES.GetOffsetStringFor(tz, GetSlot(zdt, INSTANT));
if (showOffset !== 'never') result += ES.GetOffsetStringFor(tz, instant);
if (showTimeZone !== 'never') result += `[${ES.TimeZoneToString(tz)}]`;
result += ES.FormatCalendarAnnotation(GetSlot(zdt, CALENDAR), showCalendar);
return result;
Expand Down
13 changes: 13 additions & 0 deletions polyfill/test/zoneddatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1925,6 +1925,12 @@ describe('ZonedDateTime', () => {
const roundMeUp = ZonedDateTime.from('2020-03-08T11:30:01-07:00[America/Vancouver]');
equal(`${roundMeUp.round(options)}`, '2020-03-09T00:00:00-07:00[America/Vancouver]');
});
it('rounding up to a nonexistent wall-clock time', () => {
const almostSkipped = ZonedDateTime.from('2018-11-03T23:59:59.999999999-03:00[America/Sao_Paulo]');
const rounded = almostSkipped.round({ smallestUnit: 'microsecond', roundingMode: 'nearest' });
equal(`${rounded}`, '2018-11-04T01:00:00-02:00[America/Sao_Paulo]');
equal(rounded.epochNanoseconds - almostSkipped.epochNanoseconds, 1n);
});
});

describe('ZonedDateTime.equals()', () => {
Expand Down Expand Up @@ -2118,6 +2124,13 @@ describe('ZonedDateTime', () => {
'2000-01-01T00:00:00.00000000+01:00[Europe/Berlin]'
);
});
it('rounding up to a nonexistent wall-clock time', () => {
const zdt5 = ZonedDateTime.from('2018-11-03T23:59:59.999999999-03:00[America/Sao_Paulo]');
const roundedString = zdt5.toString({ fractionalSecondDigits: 8, roundingMode: 'nearest' });
equal(roundedString, '2018-11-04T01:00:00.00000000-02:00[America/Sao_Paulo]');
const zdt6 = ZonedDateTime.from(roundedString);
equal(zdt6.epochNanoseconds - zdt5.epochNanoseconds, 1n);
});
it('options may only be an object or undefined', () => {
[null, 1, 'hello', true, Symbol('foo'), 1n].forEach((badOptions) =>
throws(() => zdt1.toString(badOptions), TypeError)
Expand Down
9 changes: 4 additions & 5 deletions spec/zoneddatetime.html
Original file line number Diff line number Diff line change
Expand Up @@ -1260,15 +1260,14 @@ <h1>TemporalZonedDateTimeToString ( _zonedDateTime_, _precision_, _showCalendar_
<emu-alg>
1. Assert: Type(_zonedDateTime_) is Object and _zonedDateTime_ has an [[InitializedTemporalZonedDateTime]] internal slot.
1. If _increment_ is not given, set it to 1.
1. If _unit_ is not given, set it to *"nanoseconds"*.
1. If _unit_ is not given, set it to *"nanosecond"*.
1. If _roundingMode_ is not given, set it to *"trunc"*.
1. Let _ns_ be ? RoundTemporalInstant(_zonedDateTime_.[[Nanoseconds]], _increment_, _unit_, _roundingMode_).
1. Let _timeZone_ be _zonedDateTime_.[[TimeZone]].
1. Let _instant_ be ? CreateTemporalInstant(_zonedDateTime_.[[Nanoseconds]]).
1. Let _instant_ be ? CreateTemporalInstant(_ns_).
1. Let _temporalDateTime_ be ? GetTemporalDateTimeFor(_timeZone_, _instant_).
1. Let _result_ be ? RoundDateTime(_dateTime_.[[ISOYear]], _dateTime_.[[ISOMonth]], _dateTime_.[[ISODay]], _dateTime_.[[Hour]], _dateTime_.[[Minute]], _dateTime_.[[Second]], _dateTime_.[[Millisecond]], _dateTime_.[[Microsecond]], _dateTime_.[[Nanosecond]], _increment_, _unit_, _roundingMode_).
1. <mark>TODO: Do something if _result_ is a nonexistent wall-clock time</mark>.
1. Let _isoCalendar_ be ? GetISO8601Calendar().
1. Let _dateTimeString_ be ? TemporalDateTimeToString(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]], _result_.[[Hour]], _result_.[[Minute]], _result_.[[Second]], _result_.[[Millisecond]], _result_.[[Microsecond]], _result_.[[Nanosecond]], _isoCalendar_, _precision_, *"never"*).
1. Let _dateTimeString_ be ? TemporalDateTimeToString(_temporalDateTime_.[[ISOYear]], _temporalDateTime_.[[ISOMonth]], _temporalDateTime_.[[ISODay]], _temporalDateTime_.[[ISOHour]], _temporalDateTime_.[[ISOMinute]], _temporalDateTime_.[[ISOSecond]], _temporalDateTime_.[[ISOMillisecond]], _temporalDateTime_.[[ISOMicrosecond]], _temporalDateTime_.[[ISONanosecond]], _isoCalendar_, _precision_, *"never"*).
1. If _showOffset_ is *"never"*, then
1. Let _offsetString_ be the empty String.
1. Else,
Expand Down

0 comments on commit f12d87f

Please sign in to comment.