Skip to content

Commit

Permalink
Implement ZonedDateTime.until() and since() in polyfill
Browse files Browse the repository at this point in the history
Note that the DifferenceZonedDateTime abstract operation is not final:
there is one edge case that is broken in the polyfill, which is that the
since() method needs to use `this` as the relative date for rounding.
There is a test for that which is skipped.

A follow-up commit will fix this case and add spec text for the algorithm.

Co-authored-by: Justin Grant <[email protected]>

See: #569
  • Loading branch information
ptomato committed Nov 4, 2020
1 parent c874176 commit 7bb88d4
Show file tree
Hide file tree
Showing 4 changed files with 1,294 additions and 10 deletions.
2 changes: 1 addition & 1 deletion docs/zoneddatetime.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<!-- toc -->
</details>

> **NOTE**: The `with()`, `until()`, and `since()` methods of this type are not available in the polyfill yet.
> **NOTE**: The `Temporal.ZonedDateTime.with()` method is not available in the polyfill yet.
A `Temporal.ZonedDateTime` is a timezone-aware, calendar-aware date/time type that represents a real event that has happened (or will happen) at a particular instant from the perspective of a particular region on Earth.
As the broadest `Temporal` type, `Temporal.ZonedDateTime` can be considered a combination of `Temporal.TimeZone`, `Temporal.Instant`, and `Temporal.DateTime` (which includes `Temporal.Calendar`).
Expand Down
349 changes: 348 additions & 1 deletion polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
EPOCHNANOSECONDS,
TIMEZONE_ID,
CALENDAR_ID,
INSTANT,
ISO_YEAR,
ISO_MONTH,
ISO_DAY,
Expand Down Expand Up @@ -2347,6 +2348,352 @@ export const ES = ObjectAssign({}, ES2020, {
));
return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds };
},
// TODO: remove AdjustDayRelativeTo after relativeTo lands for duration.add
AdjustDayRelativeTo: (years, months, weeks, days, direction, largestUnit, relativeTo) => {
const calendar = GetSlot(relativeTo, CALENDAR);
const dtRelative = ES.GetTemporalDateTimeFor(
GetSlot(relativeTo, TIME_ZONE),
GetSlot(relativeTo, INSTANT),
calendar
);
const relYear = GetSlot(dtRelative, ISO_YEAR);
const relMonth = GetSlot(dtRelative, ISO_MONTH);
const relDay = GetSlot(dtRelative, ISO_DAY);
const relHour = GetSlot(dtRelative, ISO_HOUR);
const relMinute = GetSlot(dtRelative, ISO_MINUTE);
const relSecond = GetSlot(dtRelative, ISO_SECOND);
const relMillisecond = GetSlot(dtRelative, ISO_MILLISECOND);
const relMicrosecond = GetSlot(dtRelative, ISO_MICROSECOND);
const relNanosecond = GetSlot(dtRelative, ISO_NANOSECOND);
const oneDayEarlier = ES.AddDateTime(
relYear,
relMonth,
relDay,
relHour,
relMinute,
relSecond,
relMillisecond,
relMicrosecond,
relNanosecond,
calendar,
years,
months,
weeks,
days + direction,
0,
0,
0,
0,
0,
0,
'constrain'
);
let hours, minutes, seconds, milliseconds, microseconds, nanoseconds;
({
years,
months,
weeks,
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds
} = ES.DifferenceDateTime(
relYear,
relMonth,
relDay,
relHour,
relMinute,
relSecond,
relMillisecond,
relMicrosecond,
relNanosecond,
oneDayEarlier.year,
oneDayEarlier.month,
oneDayEarlier.day,
oneDayEarlier.hour,
oneDayEarlier.minute,
oneDayEarlier.second,
oneDayEarlier.millisecond,
oneDayEarlier.microsecond,
oneDayEarlier.nanosecond,
calendar,
largestUnit
));
return ES.RoundDuration(
years,
months,
weeks,
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
1,
'days',
'ceil',
dtRelative
);
},
DifferenceZonedDateTime: (earlier, later, largestUnit, roundingIncrement, smallestUnit, roundingMode) => {
const dateUnits = ['years', 'months', 'weeks', 'days'];
const wantDateUnits = dateUnits.includes(largestUnit);
const wantDateUnitsOnly = dateUnits.includes(smallestUnit);

const ns1 = GetSlot(earlier, EPOCHNANOSECONDS);
const ns2 = GetSlot(later, EPOCHNANOSECONDS);

if (!wantDateUnits) {
// The user is only asking for a time difference, so return difference of instants.
const { seconds, milliseconds, microseconds, nanoseconds } = ES.DifferenceInstant(
ns1,
ns2,
roundingIncrement,
smallestUnit,
roundingMode
);
return ES.BalanceDuration(0, 0, 0, seconds, milliseconds, microseconds, nanoseconds, largestUnit);
}

const timeZone = GetSlot(earlier, TIME_ZONE);
if (!ES.TimeZoneEquals(timeZone, GetSlot(later, TIME_ZONE))) {
throw new RangeError(
"When calculating difference between time zones, largestUnit must be 'hours' " +
'or smaller because day lengths can vary between time zones due to DST or time zone offset changes.'
);
}

const nsDiff = ns2.subtract(ns1);
if (nsDiff.isZero()) return {};
const direction = nsDiff.divide(nsDiff.abs()).toJSNumber();

// Find the difference in dates only.
const calendar = GetSlot(earlier, CALENDAR);
const dtEarlier = ES.GetTemporalDateTimeFor(timeZone, GetSlot(earlier, INSTANT), calendar);
const dtLater = ES.GetTemporalDateTimeFor(timeZone, GetSlot(later, INSTANT), calendar);
let { years, months, weeks, days } = ES.DifferenceDateTime(
GetSlot(dtEarlier, ISO_YEAR),
GetSlot(dtEarlier, ISO_MONTH),
GetSlot(dtEarlier, ISO_DAY),
GetSlot(dtEarlier, ISO_HOUR),
GetSlot(dtEarlier, ISO_MINUTE),
GetSlot(dtEarlier, ISO_SECOND),
GetSlot(dtEarlier, ISO_MILLISECOND),
GetSlot(dtEarlier, ISO_MICROSECOND),
GetSlot(dtEarlier, ISO_NANOSECOND),
GetSlot(dtLater, ISO_YEAR),
GetSlot(dtLater, ISO_MONTH),
GetSlot(dtLater, ISO_DAY),
GetSlot(dtLater, ISO_HOUR),
GetSlot(dtLater, ISO_MINUTE),
GetSlot(dtLater, ISO_SECOND),
GetSlot(dtLater, ISO_MILLISECOND),
GetSlot(dtLater, ISO_MICROSECOND),
GetSlot(dtLater, ISO_NANOSECOND),
calendar,
largestUnit
);
let intermediateNs = ES.AddZonedDateTime(
GetSlot(earlier, INSTANT),
timeZone,
calendar,
years,
months,
weeks,
days,
0,
0,
0,
0,
0,
0,
'constrain'
); // may disambiguate

// If clock time after addition was in the middle of a skipped period, the
// endpoint was disambiguated to a later clock time. So it's possible that
// the resulting disambiguated result is later than `this`. If so, then back
// up one day and try again. Repeat if necessary (some transitions are
// > 24 hours) until either there's zero days left or the date duration is
// back inside the period where it belongs. Note that this case only can
// happen for positive durations because the only direction that
// `disambiguation: 'compatible'` can change clock time is forwards.
while (
direction === 1 &&
ES.DurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0) === 1 &&
intermediateNs > ns2
) {
// TODO: after PlainDate.add rounding lands, uncomment use of relativeTo
// dateDuration = dateDuration.subtract({ days: -1, relativeTo: dtEarlier });
({ years, months, weeks, days } = ES.AdjustDayRelativeTo(years, months, weeks, days, -1, largestUnit, earlier));
intermediateNs = ES.AddZonedDateTime(
GetSlot(earlier, INSTANT),
timeZone,
calendar,
years,
months,
weeks,
days,
0,
0,
0,
0,
0,
0,
'constrain'
); // may do disambiguation
}

let isOverflow = false;
let dayLengthNs = 0;
let timeRemainderNs = 0;
do {
// calculate length of the next day (day that contains the time remainder)
const oneDayFartherDuration = ES.AdjustDayRelativeTo(years, months, weeks, days, direction, largestUnit, earlier);
const oneDayFartherNs = ES.AddZonedDateTime(
GetSlot(earlier, INSTANT),
timeZone,
calendar,
oneDayFartherDuration.years,
oneDayFartherDuration.months,
oneDayFartherDuration.weeks,
oneDayFartherDuration.days,
0,
0,
0,
0,
0,
0,
'constrain'
);
dayLengthNs = oneDayFartherNs.subtract(intermediateNs).toJSNumber();
timeRemainderNs = ns2.subtract(intermediateNs).toJSNumber();
isOverflow = (timeRemainderNs - dayLengthNs) * direction >= 0;
if (isOverflow) {
({ years, months, weeks, days } = oneDayFartherDuration);
intermediateNs = oneDayFartherNs;
}
} while (isOverflow);

// if there's no time remainder, we're done!
if (timeRemainderNs === 0) {
return ES.RoundDuration(
years,
months,
weeks,
days,
0,
0,
0,
0,
0,
0,
roundingIncrement,
smallestUnit,
roundingMode,
dtEarlier
);
}

if (wantDateUnitsOnly) {
// There's a time remainder and `smallestUnit` is `days` or larger. This
// means that there will be no time remainder in the final result, and
// that we may have to round from hours to days.
return ES.RoundDuration(
years,
months,
weeks,
days,
0,
0,
0,
0,
0,
timeRemainderNs,
roundingIncrement,
smallestUnit,
roundingMode,
dtEarlier
);
} else {
// There's a time remainder and `smallestUnit` is `hours` or smaller.
// Calculate the time remainder (with rounding).
let hours, minutes;
let { seconds, milliseconds, microseconds, nanoseconds } = ES.DifferenceInstant(
intermediateNs,
ns2,
roundingIncrement,
smallestUnit,
roundingMode
);
timeRemainderNs = seconds * 1e9 + milliseconds * 1e6 + microseconds * 1e3 + nanoseconds;
({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.BalanceDuration(
0,
0,
0,
seconds,
milliseconds,
microseconds,
nanoseconds,
'hours'
));

// There's one more round of rounding possible: the time duration above
// could have rounded up into enough hours to exceed the day length. If
// this happens, grow the date duration by a single day and re-run the
// time rounding on the smaller remainder. DO NOT RECURSE, because once
// the extra hours are sucked up into the date duration, there's no way
// for another full day to come from the next round of rounding. And if
// it were possible (e.g. contrived calendar with 30-minute-long "days")
// then it'd risk an infinite loop.
isOverflow = (timeRemainderNs - dayLengthNs) * direction >= 0;
if (isOverflow) {
({ years, months, weeks, days } = ES.AdjustDayRelativeTo(
years,
months,
weeks,
days,
direction,
largestUnit,
earlier
));
timeRemainderNs -= dayLengthNs;

({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.RoundDuration(
0,
0,
0,
0,
0,
0,
0,
0,
0,
timeRemainderNs,
roundingIncrement,
smallestUnit,
roundingMode
));
({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.BalanceDuration(
0,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
'hours'
));
}

// Finally, merge the date and time durations and return the merged result.
return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds };
}
},
AddDate: (year, month, day, years, months, weeks, days, overflow) => {
year += years;
month += months;
Expand Down Expand Up @@ -2721,7 +3068,7 @@ export const ES = ObjectAssign({}, ES2020, {
increment,
unit,
roundingMode,
relativeTo
relativeTo = undefined
) => {
const TemporalDate = GetIntrinsic('%Temporal.Date%');
const TemporalDuration = GetIntrinsic('%Temporal.Duration%');
Expand Down
Loading

0 comments on commit 7bb88d4

Please sign in to comment.