diff --git a/docs/time.md b/docs/time.md index fb6599f664..97d2e12b02 100644 --- a/docs/time.md +++ b/docs/time.md @@ -245,10 +245,18 @@ 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. @@ -256,12 +264,19 @@ The returned `Temporal.Duration` object will not have any nonzero fields that ar 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 diff --git a/polyfill/lib/time.mjs b/polyfill/lib/time.mjs index 2ca7501aad..92bf7f9bfc 100644 --- a/polyfill/lib/time.mjs +++ b/polyfill/lib/time.mjs @@ -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), @@ -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, diff --git a/polyfill/test/time.mjs b/polyfill/test/time.mjs index b6449df962..ac761103d7 100644 --- a/polyfill/test/time.mjs +++ b/polyfill/test/time.mjs @@ -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'); diff --git a/spec/time.html b/spec/time.html index 91460e5b03..e9b5f6f56d 100644 --- a/spec/time.html +++ b/spec/time.html @@ -292,8 +292,19 @@

Temporal.Time.prototype.difference ( _other_ [ , _options_ ] )

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]]).