Skip to content

Commit

Permalink
Take non-24-hour days into account in BalanceDuration
Browse files Browse the repository at this point in the history
When converting between days and lower units in BalanceDuration, we need
to take the exact time into account, and likewise when converting
between lower units and days, we need to take the length of the last day
into account.

See: #1171
  • Loading branch information
ptomato committed Nov 14, 2020
1 parent 533187f commit 9d7fdbd
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 66 deletions.
15 changes: 12 additions & 3 deletions polyfill/lib/duration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ export class Duration {
options = ES.NormalizeOptionsObject(options);
const smallestUnit = ES.ToSmallestTemporalDurationUnit(options, 'nanoseconds');
defaultLargestUnit = ES.LargerOfTwoTemporalDurationUnits(defaultLargestUnit, smallestUnit);
const relativeTo = ES.ToRelativeTemporalObject(options);
let relativeTo = ES.ToRelativeTemporalObject(options);
const largestUnit = ES.ToLargestTemporalUnit(options, defaultLargestUnit);
ES.ValidateTemporalUnitRange(largestUnit, smallestUnit);
const roundingMode = ES.ToTemporalRoundingMode(options, 'nearest');
Expand Down Expand Up @@ -453,6 +453,9 @@ export class Duration {
relativeTo
));
({ years, months, weeks, days } = ES.BalanceDurationRelative(years, months, weeks, days, largestUnit, relativeTo));
if (ES.IsTemporalZonedDateTime(relativeTo)) {
relativeTo = ES.MoveRelativeZonedDateTime(relativeTo, years, months, weeks, 0);
}
({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.BalanceDuration(
days,
hours,
Expand All @@ -461,7 +464,8 @@ export class Duration {
milliseconds,
microseconds,
nanoseconds,
largestUnit
largestUnit,
relativeTo
));

const Construct = ES.SpeciesConstructor(this, Duration);
Expand Down Expand Up @@ -501,6 +505,10 @@ export class Duration {
// Convert larger units down to days
({ years, months, weeks, days } = ES.UnbalanceDurationRelative(years, months, weeks, days, unit, relativeTo));
// If the unit we're totalling is smaller than `days`, convert days down to that unit.
let intermediate;
if (ES.IsTemporalZonedDateTime(relativeTo)) {
intermediate = ES.MoveRelativeZonedDateTime(relativeTo, years, months, weeks, 0);
}
({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.BalanceDuration(
days,
hours,
Expand All @@ -509,7 +517,8 @@ export class Duration {
milliseconds,
microseconds,
nanoseconds,
unit
unit,
intermediate
));
// Finally, truncate to the correct unit and calculate remainder
const rounded = ES.RoundDuration(
Expand Down
69 changes: 49 additions & 20 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1993,33 +1993,63 @@ export const ES = ObjectAssign({}, ES2020, {
} while (isOverflow);
return { days, nanoseconds, dayLengthNs };
},
BalanceDuration: (days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, largestUnit) => {
nanoseconds = ES.TotalDurationNanoseconds(
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
0
);
BalanceDuration: (
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
largestUnit,
relativeTo = undefined
) => {
if (ES.IsTemporalZonedDateTime(relativeTo)) {
const endNs = ES.AddZonedDateTime(
GetSlot(relativeTo, INSTANT),
GetSlot(relativeTo, TIME_ZONE),
GetSlot(relativeTo, CALENDAR),
0,
0,
0,
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
'constrain'
);
const startNs = GetSlot(relativeTo, EPOCHNANOSECONDS);
nanoseconds = endNs.subtract(startNs);
} else {
nanoseconds = ES.TotalDurationNanoseconds(
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
0
);
}
if (largestUnit === 'years' || largestUnit === 'months' || largestUnit === 'weeks' || largestUnit === 'days') {
({ days, nanoseconds } = ES.NanosecondsToDays(nanoseconds, relativeTo));
} else {
days = 0;
}

const sign = nanoseconds.lesser(0) ? -1 : 1;
nanoseconds = nanoseconds.abs();
microseconds = milliseconds = seconds = minutes = hours = days = bigInt.zero;
microseconds = milliseconds = seconds = minutes = hours = bigInt.zero;

switch (largestUnit) {
case 'years':
case 'months':
case 'weeks':
case 'days':
({ quotient: microseconds, remainder: nanoseconds } = nanoseconds.divmod(1000));
({ quotient: milliseconds, remainder: microseconds } = microseconds.divmod(1000));
({ quotient: seconds, remainder: milliseconds } = milliseconds.divmod(1000));
({ quotient: minutes, remainder: seconds } = seconds.divmod(60));
({ quotient: hours, remainder: minutes } = minutes.divmod(60));
({ quotient: days, remainder: hours } = hours.divmod(24));
break;
case 'hours':
({ quotient: microseconds, remainder: nanoseconds } = nanoseconds.divmod(1000));
({ quotient: milliseconds, remainder: microseconds } = microseconds.divmod(1000));
Expand Down Expand Up @@ -2051,7 +2081,6 @@ export const ES = ObjectAssign({}, ES2020, {
throw new Error('assert not reached');
}

days = days.toJSNumber() * sign;
hours = hours.toJSNumber() * sign;
minutes = minutes.toJSNumber() * sign;
seconds = seconds.toJSNumber() * sign;
Expand Down
45 changes: 22 additions & 23 deletions polyfill/test/duration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -975,32 +975,35 @@ describe('Duration', () => {
const oneDay = new Duration(0, 0, 0, 1);
const hours12 = new Duration(0, 0, 0, 0, 12);
describe('relativeTo affects days if ZonedDateTime, and duration encompasses DST change', () => {
it.skip('start inside repeated hour, end after', () => {
it('start inside repeated hour, end after', () => {
equal(`${hours25.round({ largestUnit: 'days', relativeTo: inRepeatedHour })}`, 'P1D');
equal(`${oneDay.round({ largestUnit: 'hours', relativeTo: inRepeatedHour })}`, 'PT25H');
});
it.skip('start after repeated hour, end inside (negative)', () => {
it('start after repeated hour, end inside (negative)', () => {
const relativeTo = Temporal.ZonedDateTime.from('2019-11-04T01:00[America/Vancouver]');
equal(`${hours25.negated().round({ largestUnit: 'days', relativeTo })}`, '-P1D');
equal(`${oneDay.negated().round({ largestUnit: 'hours', relativeTo })}`, '-PT25H');
});
it('start inside repeated hour, end in skipped hour', () => {
// gain one hour at the beginning, lose one at the end, days average 24h
equal(
`${Duration.from({ days: 126, hours: 1 }).round({ largestUnit: 'days', relativeTo: inRepeatedHour })}`,
'P126DT1H'
);
equal(
`${Duration.from({ days: 126, hours: 1 }).round({ largestUnit: 'hours', relativeTo: inRepeatedHour })}`,
'PT3026H'
);
});
it('start in normal hour, end in skipped hour', () => {
const relativeTo = Temporal.ZonedDateTime.from('2019-03-09T02:30[America/Vancouver]');
equal(`${hours25.round({ largestUnit: 'days', relativeTo })}`, 'P1DT1H');
equal(`${oneDay.round({ largestUnit: 'hours', relativeTo })}`, 'PT24H');
});
it.skip('start before skipped hour, end >1 day after', () => {
it('start before skipped hour, end >1 day after', () => {
equal(`${hours25.round({ largestUnit: 'days', relativeTo: skippedHourDay })}`, 'P1DT2H');
equal(`${oneDay.round({ largestUnit: 'hours', relativeTo: skippedHourDay })}`, 'PT23H');
});
it.skip('start after skipped hour, end >1 day before (negative)', () => {
it('start after skipped hour, end >1 day before (negative)', () => {
const relativeTo = Temporal.ZonedDateTime.from('2019-03-11T00:00[America/Vancouver]');
equal(`${hours25.negated().round({ largestUnit: 'days', relativeTo })}`, '-P1DT2H');
equal(`${oneDay.negated().round({ largestUnit: 'hours', relativeTo })}`, '-PT23H');
Expand All @@ -1012,11 +1015,11 @@ describe('Duration', () => {
const relativeTo = Temporal.ZonedDateTime.from('2019-03-10T12:00[America/Vancouver]');
equal(`${hours12.negated().round({ largestUnit: 'days', relativeTo })}`, '-PT12H');
});
it.skip('start before repeated hour, end >1 day after', () => {
it('start before repeated hour, end >1 day after', () => {
equal(`${hours25.round({ largestUnit: 'days', relativeTo: repeatedHourDay })}`, 'P1D');
equal(`${oneDay.round({ largestUnit: 'hours', relativeTo: repeatedHourDay })}`, 'PT25H');
});
it.skip('start after repeated hour, end >1 day before (negative)', () => {
it('start after repeated hour, end >1 day before (negative)', () => {
const relativeTo = Temporal.ZonedDateTime.from('2019-11-04T00:00[America/Vancouver]');
equal(`${hours25.negated().round({ largestUnit: 'days', relativeTo })}`, '-P1D');
equal(`${oneDay.negated().round({ largestUnit: 'hours', relativeTo })}`, '-PT25H');
Expand All @@ -1034,7 +1037,7 @@ describe('Duration', () => {
equal(`${Duration.from({ hours: 48 }).round({ largestUnit: 'days', relativeTo })}`, 'P3D');
});
});
it.skip('casts relativeTo to ZonedDateTime if possible', () => {
it('casts relativeTo to ZonedDateTime if possible', () => {
equal(`${hours25.round({ largestUnit: 'days', relativeTo: '2019-11-03T00:00[America/Vancouver]' })}`, 'P1D');
equal(
`${hours25.round({
Expand Down Expand Up @@ -1535,33 +1538,29 @@ describe('Duration', () => {
const hours12 = new Duration(0, 0, 0, 0, 12);
const hours25 = new Duration(0, 0, 0, 0, 25);
describe('relativeTo affects days if ZonedDateTime, and duration encompasses DST change', () => {
it.skip('start inside repeated hour, end after', () => {
it('start inside repeated hour, end after', () => {
equal(hours25.total({ unit: 'days', relativeTo: inRepeatedHour }), 1);
equal(oneDay.total({ unit: 'hours', relativeTo: inRepeatedHour }), 25);
});
it.skip('start after repeated hour, end inside (negative)', () => {
it('start after repeated hour, end inside (negative)', () => {
const relativeTo = Temporal.ZonedDateTime.from('2019-11-04T01:00[America/Vancouver]');
equal(hours25.negated().total({ unit: 'days', relativeTo }), -1);
equal(oneDay.negated().total({ unit: 'hours', relativeTo }), -25);
});
it.skip('start inside repeated hour, end in skipped hour', () => {
// gain one hour at the beginning, lose one at the end, days average 24h
equal(Duration.from({ days: 126, hours: 1 }).total({ unit: 'days', relativeTo: inRepeatedHour }), 126 + 1 / 24);
equal(
Duration.from({ days: 126, hours: 1 }).total({ unit: 'hours', relativeTo: inRepeatedHour }),
126 * 24 + 1
);
it('start inside repeated hour, end in skipped hour', () => {
equal(Duration.from({ days: 126, hours: 1 }).total({ unit: 'days', relativeTo: inRepeatedHour }), 126 + 1 / 23);
equal(Duration.from({ days: 126, hours: 1 }).total({ unit: 'hours', relativeTo: inRepeatedHour }), 3026);
});
it('start in normal hour, end in skipped hour', () => {
const relativeTo = Temporal.ZonedDateTime.from('2019-03-09T02:30[America/Vancouver]');
equal(hours25.total({ unit: 'days', relativeTo }), 1 + 1 / 24);
equal(oneDay.total({ unit: 'hours', relativeTo }), 24);
});
it.skip('start before skipped hour, end >1 day after', () => {
it('start before skipped hour, end >1 day after', () => {
equal(hours25.total({ unit: 'days', relativeTo: skippedHourDay }), 1 + 2 / 24);
equal(oneDay.total({ unit: 'hours', relativeTo: skippedHourDay }), 23);
});
it.skip('start after skipped hour, end >1 day before (negative)', () => {
it('start after skipped hour, end >1 day before (negative)', () => {
const relativeTo = Temporal.ZonedDateTime.from('2019-03-11T00:00[America/Vancouver]');
equal(hours25.negated().total({ unit: 'days', relativeTo }), -1 - 2 / 24);
equal(oneDay.negated().total({ unit: 'hours', relativeTo }), -23);
Expand All @@ -1573,11 +1572,11 @@ describe('Duration', () => {
const relativeTo = Temporal.ZonedDateTime.from('2019-03-10T12:00[America/Vancouver]');
equal(hours12.negated().total({ unit: 'days', relativeTo }), -12 / 23);
});
it.skip('start before repeated hour, end >1 day after', () => {
it('start before repeated hour, end >1 day after', () => {
equal(hours25.total({ unit: 'days', relativeTo: repeatedHourDay }), 1);
equal(oneDay.total({ unit: 'hours', relativeTo: repeatedHourDay }), 25);
});
it.skip('start after repeated hour, end >1 day before (negative)', () => {
it('start after repeated hour, end >1 day before (negative)', () => {
const relativeTo = Temporal.ZonedDateTime.from('2019-11-04T00:00[America/Vancouver]');
equal(hours25.negated().total({ unit: 'days', relativeTo }), -1);
equal(oneDay.negated().total({ unit: 'hours', relativeTo }), -25);
Expand All @@ -1597,12 +1596,12 @@ describe('Duration', () => {
equal(Duration.from({ days: 3 }).total({ unit: 'hours', relativeTo }), 48);
});
});
it.skip('totaling back up to days', () => {
it('totaling back up to days', () => {
const relativeTo = Temporal.ZonedDateTime.from('2019-11-02T00:00[America/Vancouver]');
equal(Duration.from({ hours: 48 }).total({ unit: 'days' }), 2);
equal(Duration.from({ hours: 48 }).total({ unit: 'days', relativeTo }), 1 + 24 / 25);
});
it.skip('casts relativeTo to ZonedDateTime if possible', () => {
it('casts relativeTo to ZonedDateTime if possible', () => {
equal(oneDay.total({ unit: 'hours', relativeTo: '2019-11-03T00:00[America/Vancouver]' }), 25);
equal(
oneDay.total({ unit: 'hours', relativeTo: { year: 2019, month: 11, day: 3, timeZone: 'America/Vancouver' } }),
Expand Down
Loading

0 comments on commit 9d7fdbd

Please sign in to comment.