Skip to content

Commit

Permalink
Implement rounding in Temporal.Time.difference
Browse files Browse the repository at this point in the history
See: #827
  • Loading branch information
ptomato committed Sep 15, 2020
1 parent a6ac46c commit 124837b
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 1 deletion.
17 changes: 16 additions & 1 deletion docs/time.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,23 +245,38 @@ time.minus({ minutes: 5, nanoseconds: 800 }) // => 19:34:09.068345405
- `largestUnit` (string): The largest unit of time to allow in the resulting `Temporal.Duration` object.
Valid values are `'hours'`, `'minutes'`, `'seconds'`, `'milliseconds'`, `'microseconds'`, and `'nanoseconds'`.
The default is `'hours'`.
- `smallestUnit` (string): The smallest unit of time to round to in the resulting `Temporal.Duration` object.
Valid values are the same as for `largestUnit`.
The default is `'nanoseconds'`, i.e. no rounding.
- `roundingIncrement` (number): The granularity to round to, of the unit given by `smallestUnit`.
The default is 1.
- `roundingMode` (string): How to handle the remainder, if rounding.
Valid values are `'ceil'`, `'floor'`, `'trunc'`, and `'nearest'`.
The default is `'nearest'`.

**Returns:** a `Temporal.Duration` representing the difference between `time` and `other`.

This method computes the difference between the two times represented by `time` and `other`, and returns it as a `Temporal.Duration` object.
This method computes the difference between the two times represented by `time` and `other`, optionally rounds it, and returns it as a `Temporal.Duration` object.
If `other` is later than `time` then the resulting duration will be negative.

The `largestUnit` parameter controls how the resulting duration is expressed.
The returned `Temporal.Duration` object will not have any nonzero fields that are larger than the unit in `largestUnit`.
A difference of two hours will become 7200 seconds when `largestUnit` is `'seconds'`, for example.
However, a difference of 30 seconds will still be 30 seconds even if `largestUnit` is `'hours'`.

You can round the result using the `smallestUnit`, `roundingIncrement`, and `roundingMode` options.
These behave as in the `Temporal.Duration.round()` method.
The default is to do no rounding.

Usage example:
```javascript
time = Temporal.Time.from('20:13:20.971398099');
time.difference(Temporal.Time.from('19:39:09.068346205')) // => PT34M11.903051894S
time.difference(Temporal.Time.from('22:39:09.068346205')) // => -PT2H25M49.903051894S

// Rounding, for example if you don't care about sub-seconds
time.difference(Temporal.Time.from('19:39:09.068346205'), { smallestUnit: 'seconds' })
// => PT34M12S
```

### time.**round**(_options_: object) : Temporal.Time
Expand Down
28 changes: 28 additions & 0 deletions polyfill/lib/time.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,19 @@ export class Time {
if (!ES.IsTemporalTime(this)) throw new TypeError('invalid receiver');
if (!ES.IsTemporalTime(other)) throw new TypeError('invalid Time object');
const largestUnit = ES.ToLargestTemporalUnit(options, 'hours', ['years', 'months', 'weeks', 'days']);
const smallestUnit = ES.ToSmallestTemporalDurationUnit(options, 'nanoseconds');
ES.ValidateTemporalDifferenceUnits(largestUnit, smallestUnit);
const roundingMode = ES.ToTemporalRoundingMode(options);
const maximumIncrements = {
hours: 24,
minutes: 60,
seconds: 60,
milliseconds: 1000,
microseconds: 1000,
nanoseconds: 1000
};
const roundingIncrement = ES.ToTemporalRoundingIncrement(options, maximumIncrements[smallestUnit], false);

let { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.DifferenceTime(
GetSlot(other, HOUR),
GetSlot(other, MINUTE),
Expand All @@ -254,6 +267,21 @@ export class Time {
GetSlot(this, MICROSECOND),
GetSlot(this, NANOSECOND)
);
({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.RoundDuration(
0,
0,
0,
0,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
roundingIncrement,
smallestUnit,
roundingMode
));
({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.BalanceDuration(
0,
hours,
Expand Down
174 changes: 174 additions & 0 deletions polyfill/test/time.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,180 @@ describe('Time', () => {
);
[{}, () => {}, undefined].forEach((options) => equal(`${time.difference(one, options)}`, 'PT1H'));
});
const earlier = Time.from('08:22:36.123456789');
const later = Time.from('12:39:40.987654321');
it('throws on disallowed or invalid smallestUnit', () => {
['era', 'years', 'months', 'weeks', 'days', 'year', 'month', 'week', 'day', 'nonsense'].forEach(
(smallestUnit) => {
throws(() => later.difference(earlier, { smallestUnit }), RangeError);
}
);
});
it('throws if smallestUnit is larger than largestUnit', () => {
const units = ['hours', 'minutes', 'seconds', 'milliseconds', 'microseconds', 'nanoseconds'];
for (let largestIdx = 1; largestIdx < units.length; largestIdx++) {
for (let smallestIdx = 0; smallestIdx < largestIdx; smallestIdx++) {
const largestUnit = units[largestIdx];
const smallestUnit = units[smallestIdx];
throws(() => later.difference(earlier, { largestUnit, smallestUnit }), RangeError);
}
}
});
it('throws on invalid roundingMode', () => {
throws(() => later.difference(earlier, { roundingMode: 'cile' }), RangeError);
});
const incrementOneNearest = [
['hours', 'PT4H'],
['minutes', 'PT4H17M'],
['seconds', 'PT4H17M5S'],
['milliseconds', 'PT4H17M4.864S'],
['microseconds', 'PT4H17M4.864198S'],
['nanoseconds', 'PT4H17M4.864197532S']
];
incrementOneNearest.forEach(([smallestUnit, expected]) => {
const roundingMode = 'nearest';
it(`rounds to nearest ${smallestUnit}`, () => {
equal(`${later.difference(earlier, { smallestUnit, roundingMode })}`, expected);
equal(`${earlier.difference(later, { smallestUnit, roundingMode })}`, `-${expected}`);
});
});
const incrementOneCeil = [
['hours', 'PT5H', '-PT4H'],
['minutes', 'PT4H18M', '-PT4H17M'],
['seconds', 'PT4H17M5S', '-PT4H17M4S'],
['milliseconds', 'PT4H17M4.865S', '-PT4H17M4.864S'],
['microseconds', 'PT4H17M4.864198S', '-PT4H17M4.864197S'],
['nanoseconds', 'PT4H17M4.864197532S', '-PT4H17M4.864197532S']
];
incrementOneCeil.forEach(([smallestUnit, expectedPositive, expectedNegative]) => {
const roundingMode = 'ceil';
it(`rounds up to ${smallestUnit}`, () => {
equal(`${later.difference(earlier, { smallestUnit, roundingMode })}`, expectedPositive);
equal(`${earlier.difference(later, { smallestUnit, roundingMode })}`, expectedNegative);
});
});
const incrementOneFloor = [
['hours', 'PT4H', '-PT5H'],
['minutes', 'PT4H17M', '-PT4H18M'],
['seconds', 'PT4H17M4S', '-PT4H17M5S'],
['milliseconds', 'PT4H17M4.864S', '-PT4H17M4.865S'],
['microseconds', 'PT4H17M4.864197S', '-PT4H17M4.864198S'],
['nanoseconds', 'PT4H17M4.864197532S', '-PT4H17M4.864197532S']
];
incrementOneFloor.forEach(([smallestUnit, expectedPositive, expectedNegative]) => {
const roundingMode = 'floor';
it(`rounds down to ${smallestUnit}`, () => {
equal(`${later.difference(earlier, { smallestUnit, roundingMode })}`, expectedPositive);
equal(`${earlier.difference(later, { smallestUnit, roundingMode })}`, expectedNegative);
});
});
const incrementOneTrunc = [
['hours', 'PT4H'],
['minutes', 'PT4H17M'],
['seconds', 'PT4H17M4S'],
['milliseconds', 'PT4H17M4.864S'],
['microseconds', 'PT4H17M4.864197S'],
['nanoseconds', 'PT4H17M4.864197532S']
];
incrementOneTrunc.forEach(([smallestUnit, expected]) => {
const roundingMode = 'trunc';
it(`truncates to ${smallestUnit}`, () => {
equal(`${later.difference(earlier, { smallestUnit, roundingMode })}`, expected);
equal(`${earlier.difference(later, { smallestUnit, roundingMode })}`, `-${expected}`);
});
});
it('nearest is the default', () => {
equal(`${later.difference(earlier, { smallestUnit: 'minutes' })}`, 'PT4H17M');
equal(`${later.difference(earlier, { smallestUnit: 'seconds' })}`, 'PT4H17M5S');
});
it('rounds to an increment of hours', () => {
equal(`${later.difference(earlier, { smallestUnit: 'hours', roundingIncrement: 3 })}`, 'PT3H');
});
it('rounds to an increment of minutes', () => {
equal(`${later.difference(earlier, { smallestUnit: 'minutes', roundingIncrement: 30 })}`, 'PT4H30M');
});
it('rounds to an increment of seconds', () => {
equal(`${later.difference(earlier, { smallestUnit: 'seconds', roundingIncrement: 15 })}`, 'PT4H17M');
});
it('rounds to an increment of milliseconds', () => {
equal(`${later.difference(earlier, { smallestUnit: 'milliseconds', roundingIncrement: 10 })}`, 'PT4H17M4.860S');
});
it('rounds to an increment of microseconds', () => {
equal(
`${later.difference(earlier, { smallestUnit: 'microseconds', roundingIncrement: 10 })}`,
'PT4H17M4.864200S'
);
});
it('rounds to an increment of nanoseconds', () => {
equal(
`${later.difference(earlier, { smallestUnit: 'nanoseconds', roundingIncrement: 10 })}`,
'PT4H17M4.864197530S'
);
});
it('valid hour increments divide into 24', () => {
[1, 2, 3, 4, 6, 8, 12].forEach((roundingIncrement) => {
const options = { smallestUnit: 'hours', roundingIncrement };
assert(later.difference(earlier, options) instanceof Temporal.Duration);
});
});
['minutes', 'seconds'].forEach((smallestUnit) => {
it(`valid ${smallestUnit} increments divide into 60`, () => {
[1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30].forEach((roundingIncrement) => {
const options = { smallestUnit, roundingIncrement };
assert(later.difference(earlier, options) instanceof Temporal.Duration);
});
});
});
['milliseconds', 'microseconds', 'nanoseconds'].forEach((smallestUnit) => {
it(`valid ${smallestUnit} increments divide into 1000`, () => {
[1, 2, 4, 5, 8, 10, 20, 25, 40, 50, 100, 125, 200, 250, 500].forEach((roundingIncrement) => {
const options = { smallestUnit, roundingIncrement };
assert(later.difference(earlier, options) instanceof Temporal.Duration);
});
});
});
it('throws on increments that do not divide evenly into the next highest', () => {
throws(() => later.difference(earlier, { smallestUnit: 'hours', roundingIncrement: 11 }), RangeError);
throws(() => later.difference(earlier, { smallestUnit: 'minutes', roundingIncrement: 29 }), RangeError);
throws(() => later.difference(earlier, { smallestUnit: 'seconds', roundingIncrement: 29 }), RangeError);
throws(() => later.difference(earlier, { smallestUnit: 'milliseconds', roundingIncrement: 29 }), RangeError);
throws(() => later.difference(earlier, { smallestUnit: 'microseconds', roundingIncrement: 29 }), RangeError);
throws(() => later.difference(earlier, { smallestUnit: 'nanoseconds', roundingIncrement: 29 }), RangeError);
});
it('throws on increments that are equal to the next highest', () => {
throws(() => later.difference(earlier, { smallestUnit: 'hours', roundingIncrement: 24 }), RangeError);
throws(() => later.difference(earlier, { smallestUnit: 'minutes', roundingIncrement: 60 }), RangeError);
throws(() => later.difference(earlier, { smallestUnit: 'seconds', roundingIncrement: 60 }), RangeError);
throws(() => later.difference(earlier, { smallestUnit: 'milliseconds', roundingIncrement: 1000 }), RangeError);
throws(() => later.difference(earlier, { smallestUnit: 'microseconds', roundingIncrement: 1000 }), RangeError);
throws(() => later.difference(earlier, { smallestUnit: 'nanoseconds', roundingIncrement: 1000 }), RangeError);
});
it('accepts singular units', () => {
equal(
`${later.difference(earlier, { smallestUnit: 'hour' })}`,
`${later.difference(earlier, { smallestUnit: 'hours' })}`
);
equal(
`${later.difference(earlier, { smallestUnit: 'minute' })}`,
`${later.difference(earlier, { smallestUnit: 'minutes' })}`
);
equal(
`${later.difference(earlier, { smallestUnit: 'second' })}`,
`${later.difference(earlier, { smallestUnit: 'seconds' })}`
);
equal(
`${later.difference(earlier, { smallestUnit: 'millisecond' })}`,
`${later.difference(earlier, { smallestUnit: 'milliseconds' })}`
);
equal(
`${later.difference(earlier, { smallestUnit: 'microsecond' })}`,
`${later.difference(earlier, { smallestUnit: 'microseconds' })}`
);
equal(
`${later.difference(earlier, { smallestUnit: 'nanosecond' })}`,
`${later.difference(earlier, { smallestUnit: 'nanoseconds' })}`
);
});
});
describe('Time.round works', () => {
const time = Time.from('13:46:23.123456789');
Expand Down
11 changes: 11 additions & 0 deletions spec/time.html
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,19 @@ <h1>Temporal.Time.prototype.difference ( _other_ [ , _options_ ] )</h1>
1. Let _temporalTime_ be the *this* value.
1. Perform ? RequireInternalSlot(_temporalTime_, [[InitializedTemporalTime]]).
1. Perform ? RequireInternalSlot(_other_, [[InitializedTemporalTime]]).
1. Let _smallestUnit_ be ? ToSmallestTemporalDurationUnit(_options_, « *"years"*, *"months"*, *"weeks"*, *"days"* », *"nanoseconds"*).
1. Let _largestUnit_ be ? ToLargestTemporalUnit(_options_, « *"years"*, *"months"*, *"weeks"*, *"days"* », *"hours"*).
1. Perform ? ValidateTemporalDifferenceUnits(_largestUnit_, _smallestUnit_).
1. Let _roundingMode_ be ? ToTemporalRoundingMode(_options_).
1. If _smallestUnit_ is *"hours"*, then
1. Let _maximum_ be 24.
1. Else if _smallestUnit_ is *"minutes"* or *"seconds"*, then
1. Let _maximum_ be 60.
1. Else,
1. Let _maximum_ be 1000.
1. Let _roundingIncrement_ be ? ToTemporalRoundingIncrement(_options_, _maximum_, *false*).
1. Let _result_ be ! DifferenceTime(_other_.[[Hour]], _other_.[[Minute]], _other_.[[Second]], _other_.[[Millisecond]], _other_.[[Microsecond]], _other_.[[Nanosecond]], _temporalTime_.[[Hour]], _temporalTime_.[[Minute]], _temporalTime_.[[Second]], _temporalTime_.[[Millisecond]], _temporalTime_.[[Microsecond]], _temporalTime_.[[Nanosecond]]).
1. Set _result_ to ? RoundDuration(0, 0, 0, 0, _result_.[[Hours]], _result_.[[Minutes]], _result_.[[Seconds]], _result_.[[Milliseconds]], _result_.[[Microseconds]], _result_.[[Nanoseconds]], _roundingIncrement_, _smallestUnit_, _roundingMode_).
1. Set _result_ to ! BalanceDuration(0, _result_.[[Hours]], _result_.[[Minutes]], _result_.[[Seconds]], _result_.[[Milliseconds]], _result_.[[Microseconds]], _result_.[[Nanoseconds]], _largestUnit_).
1. Return ? CreateTemporalDuration(0, 0, 0, 0, _result_.[[Hours]], _result_.[[Minutes]], _result_.[[Seconds]], _result_.[[Milliseconds]], _result_.[[Microseconds]], _result_.[[Nanoseconds]]).
</emu-alg>
Expand Down

0 comments on commit 124837b

Please sign in to comment.