diff --git a/docs/absolute.md b/docs/absolute.md index a9d8ac3930..dd31d1f563 100644 --- a/docs/absolute.md +++ b/docs/absolute.md @@ -304,7 +304,7 @@ Temporal.now.absolute().minus(oneDay); **Returns:** a `Temporal.Duration` representing the difference between `absolute` and `other`. This method computes the difference between the two times represented by `absolute` and `other`, and returns it as a `Temporal.Duration` object. -A `RangeError` will be thrown if `other` is later than `absolute`, because `Temporal.Duration` objects cannot represent negative durations. +If `other` is later than `absolute` then the resulting duration will be negative. The `largestUnit` option 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`. diff --git a/docs/cookbook.md b/docs/cookbook.md index 0651ead3e4..3d99c2ca6d 100644 --- a/docs/cookbook.md +++ b/docs/cookbook.md @@ -322,7 +322,7 @@ An example HTML form inspired by [Days Calculator](https://www.timeanddate.com/d ### Unit-constrained duration between now and a past/future zoned event -Map two Temporal.Absolute instances into an ascending/descending order indicator and a Temporal.Duration instance representing the duration between the two instants without using units coarser than specified (e.g., for presenting a meaningful countdown with vs. without using months or days). +Take the difference between two Temporal.Absolute instances as a Temporal.Duration instance (positive or negative), representing the duration between the two instants without using units coarser than specified (e.g., for presenting a meaningful countdown with vs. without using months or days). ```javascript {{cookbook/getElapsedDurationSinceInstant.mjs}} diff --git a/docs/cookbook/getElapsedDurationSinceInstant.mjs b/docs/cookbook/getElapsedDurationSinceInstant.mjs index 1db6b60164..365ec665e2 100644 --- a/docs/cookbook/getElapsedDurationSinceInstant.mjs +++ b/docs/cookbook/getElapsedDurationSinceInstant.mjs @@ -1,40 +1,16 @@ -/** - * @typedef {Object} ElapsedDuration - * @property {string} return.sign - "+" or "-" - * @property {Temporal.Duration} return.duration - Elapsed duration - */ -/** - * Compute the difference between two instants, suitable for use in a countdown, - * for example. - * - * @param {Temporal.Absolute} then - Instant since when to measure the duration - * @param {Temporal.Absolute} now - Instant until when to measure the duration - * @param {string} [largestUnit=days] - Largest time unit to have in the result - * @returns {ElapsedDuration} Time between `then` and `now` - */ -function getElapsedDurationSinceInstant(then, now, largestUnit = 'days') { - const sign = Temporal.Absolute.compare(now, then) < 0 ? '-' : '+'; - const duration = sign === '-' ? then.difference(now, { largestUnit }) : now.difference(then, { largestUnit }); - return { sign, duration }; -} +const result = Temporal.Absolute.from('2020-01-09T04:00Z').difference(Temporal.Absolute.from('2020-01-09T00:00Z'), { + largestUnit: 'hours' +}); +assert.equal(`${result}`, 'PT4H'); -const result = getElapsedDurationSinceInstant( - Temporal.Absolute.from('2020-01-09T00:00Z'), - Temporal.Absolute.from('2020-01-09T04:00Z') -); -assert.equal(`${result.sign}${result.duration}`, '+PT4H'); - -const result2 = getElapsedDurationSinceInstant( - Temporal.Absolute.from('2020-01-09T04:00Z'), - Temporal.Absolute.from('2020-01-09T00:00Z'), - 'minutes' -); -assert.equal(`${result2.sign}${result2.duration}`, '-PT240M'); +const result2 = Temporal.Absolute.from('2020-01-09T00:00Z').difference(Temporal.Absolute.from('2020-01-09T04:00Z'), { + largestUnit: 'minutes' +}); +assert.equal(`${result2}`, '-PT240M'); // Example of using it in a countdown: -const { sign, duration } = getElapsedDurationSinceInstant( - Temporal.Absolute.from('2020-04-01T13:00-07:00[America/Los_Angeles]'), +const duration = Temporal.Absolute.from('2020-04-01T13:00-07:00[America/Los_Angeles]').difference( Temporal.now.absolute() ); -`It's ${duration.toLocaleString()} ${sign < 0 ? 'until' : 'since'} the TC39 Temporal presentation`; +`It's ${duration.toLocaleString()} ${duration.sign < 0 ? 'until' : 'since'} the TC39 Temporal presentation`; diff --git a/docs/date.md b/docs/date.md index 1eddea6615..c56eb04f7a 100644 --- a/docs/date.md +++ b/docs/date.md @@ -384,7 +384,7 @@ date.minus({ months: 1 }, { disambiguation: 'reject' }) // => throws **Returns:** a `Temporal.Duration` representing the difference between `date` and `other`. This method computes the difference between the two dates represented by `date` and `other`, and returns it as a `Temporal.Duration` object. -A `RangeError` will be thrown if `other` is later than `date`, because `Temporal.Duration` objects cannot represent negative durations. +If `other` is later than `date` then the resulting duration will be negative. The `largestUnit` option 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`. @@ -401,7 +401,7 @@ date = Temporal.Date.from('2019-01-31'); other = Temporal.Date.from('2006-08-24'); date.difference(other) // => P4543D date.difference(other, { largestUnit: 'years' }) // => P12Y5M7D -other.difference(date, { largestUnit: 'years' }) // => throws RangeError +other.difference(date, { largestUnit: 'years' }) // => -P12Y5M7D // If you really need to calculate the difference between two Dates in // hours, you can eliminate the ambiguity by explicitly choosing the diff --git a/docs/datetime.md b/docs/datetime.md index 9f38e31795..e9f9d42e9f 100644 --- a/docs/datetime.md +++ b/docs/datetime.md @@ -448,7 +448,7 @@ dt.minus({ months: 1 }) // => throws **Returns:** a `Temporal.Duration` representing the difference between `datetime` and `other`. This method computes the difference between the two times represented by `datetime` and `other`, and returns it as a `Temporal.Duration` object. -A `RangeError` will be thrown if `other` is later than `datetime`, because `Temporal.Duration` objects cannot represent negative durations. +If `other` is later than `datetime` then the resulting duration will be negative. The `largestUnit` option 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`. @@ -463,8 +463,8 @@ Usage example: dt1 = Temporal.DateTime.from('1995-12-07T03:24:30.000003500'); dt2 = Temporal.DateTime.from('2019-01-31T15:30'); dt2.difference(dt1); // => P8456DT12H5M29.999996500S -dt2.difference(dt1), { largestUnit: 'years' }) // => P23Y1M24DT12H5M29.999996500S -dt1.difference(dt2), { largestUnit: 'years' }) // => throws RangeError +dt2.difference(dt1, { largestUnit: 'years' }) // => P23Y1M24DT12H5M29.999996500S +dt1.difference(dt2, { largestUnit: 'years' }) // => -P23Y1M24DT12H5M29.999996500S // Months and years can be different lengths [jan1, feb1, mar1] = [1, 2, 3].map(month => Temporal.DateTime.from({year: 2020, month, day: 1})); diff --git a/docs/time.md b/docs/time.md index 0ec6705ccb..7358a4e0cd 100644 --- a/docs/time.md +++ b/docs/time.md @@ -249,7 +249,7 @@ time.minus({ minutes: 5, nanoseconds: 800 }) // => 19:34:09.068345405 **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. -A `RangeError` will be thrown if `other` is later than `time`, because `Temporal.Duration` objects cannot represent negative durations. +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`. @@ -263,7 +263,7 @@ 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')) // => throws RangeError +time.difference(Temporal.Time.from('22:39:09.068346205')) // => -PT2H25M49.903051894S ``` ### time.**equals**(_other_: Temporal.Time) : boolean diff --git a/docs/yearmonth.md b/docs/yearmonth.md index aa5390521e..4a04ec2096 100644 --- a/docs/yearmonth.md +++ b/docs/yearmonth.md @@ -303,7 +303,7 @@ ym.minus({years: 20, months: 4}) // => 1999-02 **Returns:** a `Temporal.Duration` representing the difference between `yearMonth` and `other`. This method computes the difference between the two months represented by `yearMonth` and `other`, and returns it as a `Temporal.Duration` object. -A `RangeError` will be thrown if `other` is later than `yearMonth`, because `Temporal.Duration` objects cannot represent negative durations. +If `other` is later than `yearMonth` then the resulting duration will be negative. The `largestUnit` option 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`. @@ -318,7 +318,7 @@ ym = Temporal.YearMonth.from('2019-06'); other = Temporal.YearMonth.from('2006-08'); ym.difference(other) // => P12Y10M ym.difference(other, { largestUnit: 'months' }) // => P154M -other.difference(ym, { largestUnit: 'months' }) // => throws RangeError +other.difference(ym, { largestUnit: 'months' }) // => -P154M // If you really need to calculate the difference between two YearMonths // in days, you can eliminate the ambiguity by explicitly choosing the diff --git a/polyfill/lib/absolute.mjs b/polyfill/lib/absolute.mjs index ab3168e7ed..5c38afaf36 100644 --- a/polyfill/lib/absolute.mjs +++ b/polyfill/lib/absolute.mjs @@ -109,8 +109,6 @@ export class Absolute { if (!ES.IsTemporalAbsolute(other)) throw new TypeError('invalid Absolute object'); const largestUnit = ES.ToLargestTemporalUnit(options, 'seconds', ['years', 'months', 'weeks']); - const comparison = Absolute.compare(this, other); - if (comparison < 0) throw new RangeError('other instance cannot be larger than `this`'); const onens = GetSlot(other, EPOCHNANOSECONDS); const twons = GetSlot(this, EPOCHNANOSECONDS); const diff = twons.minus(onens); diff --git a/polyfill/lib/calendar.mjs b/polyfill/lib/calendar.mjs index 6967ff2307..0a20aa5d23 100644 --- a/polyfill/lib/calendar.mjs +++ b/polyfill/lib/calendar.mjs @@ -54,9 +54,9 @@ export class Calendar { void constructor; throw new Error('not implemented'); } - dateDifference(smaller, larger, options) { - void smaller; - void larger; + dateDifference(one, two, options) { + void one; + void two; void options; throw new Error('not implemented'); } @@ -175,10 +175,10 @@ class ISO8601 extends Calendar { } return new constructor(year, month, day, this); } - dateDifference(smaller, larger, options) { + dateDifference(one, two, options) { if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver'); const largestUnit = ES.ToLargestTemporalUnit(options, 'days', ['hours', 'minutes', 'seconds']); - const { years, months, weeks, days } = ES.DifferenceDate(smaller, larger, largestUnit); + const { years, months, weeks, days } = ES.DifferenceDate(one, two, largestUnit); const Duration = GetIntrinsic('%Temporal.Duration%'); return new Duration(years, months, weeks, days, 0, 0, 0, 0, 0, 0); } diff --git a/polyfill/lib/date.mjs b/polyfill/lib/date.mjs index e53072e979..f5f8bf44a5 100644 --- a/polyfill/lib/date.mjs +++ b/polyfill/lib/date.mjs @@ -152,8 +152,6 @@ export class Date { if (calendar.id !== GetSlot(other, CALENDAR).id) { other = new Date(GetSlot(other, ISO_YEAR), GetSlot(other, ISO_MONTH), GetSlot(other, ISO_DAY), calendar); } - const comparison = Date.compare(this, other); - if (comparison < 0) throw new RangeError('other instance cannot be larger than `this`'); return calendar.dateDifference(other, this, options); } equals(other) { diff --git a/polyfill/lib/datetime.mjs b/polyfill/lib/datetime.mjs index 34e9f26e35..425e59dd96 100644 --- a/polyfill/lib/datetime.mjs +++ b/polyfill/lib/datetime.mjs @@ -383,8 +383,6 @@ export class DateTime { ); } const largestUnit = ES.ToLargestTemporalUnit(options, 'days'); - const comparison = DateTime.compare(this, other); - if (comparison < 0) throw new RangeError('other instance cannot be larger than `this`'); let { deltaDays, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.DifferenceTime( other, this @@ -395,13 +393,19 @@ export class DateTime { ({ year, month, day } = ES.BalanceDate(year, month, day)); const TemporalDate = GetIntrinsic('%Temporal.Date%'); - const adjustedLarger = new TemporalDate(year, month, day, GetSlot(this, CALENDAR)); + const adjusted = new TemporalDate(year, month, day, calendar); + const otherDate = new TemporalDate( + GetSlot(other, ISO_YEAR), + GetSlot(other, ISO_MONTH), + GetSlot(other, ISO_DAY), + calendar + ); let dateLargestUnit = 'days'; if (largestUnit === 'years' || largestUnit === 'months' || largestUnit === 'weeks') { dateLargestUnit = largestUnit; } const dateOptions = ObjectAssign({}, options, { largestUnit: dateLargestUnit }); - const dateDifference = calendar.dateDifference(other, adjustedLarger, dateOptions); + const dateDifference = calendar.dateDifference(otherDate, adjusted, dateOptions); let days; ({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.BalanceDuration( diff --git a/polyfill/lib/ecmascript.mjs b/polyfill/lib/ecmascript.mjs index 5c8332e06a..dd4ccc02b6 100644 --- a/polyfill/lib/ecmascript.mjs +++ b/polyfill/lib/ecmascript.mjs @@ -1105,6 +1105,15 @@ export const ES = ObjectAssign({}, ES2019, { return { year, month, years, months }; }, BalanceDuration: (days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, largestUnit) => { + const sign = ES.DurationSign(0, 0, 0, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); + days *= sign; + hours *= sign; + minutes *= sign; + seconds *= sign; + milliseconds *= sign; + microseconds *= sign; + nanoseconds *= sign; + let deltaDays; ({ deltaDays, @@ -1139,6 +1148,14 @@ export const ES = ObjectAssign({}, ES2019, { throw new Error('assert not reached'); } + days *= sign; + hours *= sign; + minutes *= sign; + seconds *= sign; + milliseconds *= sign; + microseconds *= sign; + nanoseconds *= sign; + return { days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; }, @@ -1228,7 +1245,18 @@ export const ES = ObjectAssign({}, ES2019, { } }, - DifferenceDate: (smaller, larger, largestUnit = 'days') => { + DifferenceDate: (one, two, largestUnit = 'days') => { + let larger, smaller, sign; + const TemporalDate = GetIntrinsic('%Temporal.Date%'); + if (TemporalDate.compare(one, two) < 0) { + smaller = one; + larger = two; + sign = 1; + } else { + smaller = two; + larger = one; + sign = -1; + } let years = larger.year - smaller.year; let weeks = 0; let months, days; @@ -1281,15 +1309,28 @@ export const ES = ObjectAssign({}, ES2019, { default: throw new Error('assert not reached'); } + years *= sign; + months *= sign; + weeks *= sign; + days *= sign; return { years, months, weeks, days }; }, - DifferenceTime(earlier, later) { - let hours = later.hour - earlier.hour; - let minutes = later.minute - earlier.minute; - let seconds = later.second - earlier.second; - let milliseconds = later.millisecond - earlier.millisecond; - let microseconds = later.microsecond - earlier.microsecond; - let nanoseconds = later.nanosecond - earlier.nanosecond; + DifferenceTime(one, two) { + let hours = two.hour - one.hour; + let minutes = two.minute - one.minute; + let seconds = two.second - one.second; + let milliseconds = two.millisecond - one.millisecond; + let microseconds = two.microsecond - one.microsecond; + let nanoseconds = two.nanosecond - one.nanosecond; + + const sign = ES.DurationSign(0, 0, 0, 0, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); + hours *= sign; + minutes *= sign; + seconds *= sign; + milliseconds *= sign; + microseconds *= sign; + nanoseconds *= sign; + let deltaDays = 0; ({ deltaDays, @@ -1300,6 +1341,15 @@ export const ES = ObjectAssign({}, ES2019, { microsecond: microseconds, nanosecond: nanoseconds } = ES.BalanceTime(hours, minutes, seconds, milliseconds, microseconds, nanoseconds)); + + deltaDays *= sign; + hours *= sign; + minutes *= sign; + seconds *= sign; + milliseconds *= sign; + microseconds *= sign; + nanoseconds *= sign; + return { deltaDays, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; }, AddDate: (year, month, day, years, months, weeks, days, disambiguation) => { diff --git a/polyfill/lib/time.mjs b/polyfill/lib/time.mjs index 8136768e91..426d1a8f0e 100644 --- a/polyfill/lib/time.mjs +++ b/polyfill/lib/time.mjs @@ -240,8 +240,6 @@ 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'); - const comparison = Time.compare(this, other); - if (comparison < 0) throw new RangeError('other instance cannot be larger than `this`'); let { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.DifferenceTime(other, this); ({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.BalanceDuration( 0, diff --git a/polyfill/lib/yearmonth.mjs b/polyfill/lib/yearmonth.mjs index f83f6763c8..cdba12f962 100644 --- a/polyfill/lib/yearmonth.mjs +++ b/polyfill/lib/yearmonth.mjs @@ -123,15 +123,13 @@ export class YearMonth { other = new Date(GetSlot(other, ISO_YEAR), GetSlot(other, ISO_MONTH), calendar, GetSlot(other, REF_ISO_DAY)); } const largestUnit = ES.ToLargestTemporalUnit(options, 'years', ['weeks', 'days', 'hours', 'minutes', 'seconds']); - const comparison = YearMonth.compare(this, other); - if (comparison < 0) throw new RangeError('other instance cannot be larger than `this`'); - const smallerFields = ES.ToTemporalYearMonthRecord(other); - const largerFields = ES.ToTemporalYearMonthRecord(this); + const otherFields = ES.ToTemporalYearMonthRecord(other); + const thisFields = ES.ToTemporalYearMonthRecord(this); const TemporalDate = GetIntrinsic('%Temporal.Date%'); - const smaller = calendar.dateFromFields({ ...smallerFields, day: 1 }, {}, TemporalDate); - const larger = calendar.dateFromFields({ ...largerFields, day: 1 }, {}, TemporalDate); - return calendar.dateDifference(smaller, larger, { ...options, largestUnit }); + const otherDate = calendar.dateFromFields({ ...otherFields, day: 1 }, {}, TemporalDate); + const thisDate = calendar.dateFromFields({ ...thisFields, day: 1 }, {}, TemporalDate); + return calendar.dateDifference(otherDate, thisDate, { ...options, largestUnit }); } equals(other) { if (!ES.IsTemporalYearMonth(this)) throw new TypeError('invalid receiver'); diff --git a/polyfill/test/absolute.mjs b/polyfill/test/absolute.mjs index 7f693aa2ec..fdee466760 100644 --- a/polyfill/test/absolute.mjs +++ b/polyfill/test/absolute.mjs @@ -439,7 +439,8 @@ describe('Absolute', () => { const earlier = Absolute.from('1976-11-18T15:23:30.123456789Z'); const later = Absolute.from('2019-10-29T10:46:38.271986102Z'); const diff = later.difference(earlier); - it('throws if out of order', () => throws(() => earlier.difference(later), RangeError)); + it(`(${earlier}).difference(${later}) == (${later}).difference(${earlier}).negated()`, () => + equal(`${earlier.difference(later)}`, `${diff.negated()}`)); it(`(${earlier}).plus(${diff}) == (${later})`, () => assert(earlier.plus(diff).equals(later))); it(`(${later}).minus(${diff}) == (${earlier})`, () => assert(later.minus(diff).equals(earlier))); it("doesn't cast argument", () => { diff --git a/polyfill/test/datetime.mjs b/polyfill/test/datetime.mjs index 71b9600c2d..f87bf1d34d 100644 --- a/polyfill/test/datetime.mjs +++ b/polyfill/test/datetime.mjs @@ -319,7 +319,8 @@ describe('DateTime', () => { const later = DateTime.from('2019-10-29T10:46:38.271986102'); ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds'].forEach((largestUnit) => { const diff = later.difference(earlier, { largestUnit }); - it('throws if out of order', () => throws(() => earlier.difference(later), RangeError)); + it(`(${earlier}).difference(${later}) == (${later}).difference(${earlier}).negated()`, () => + equal(`${earlier.difference(later, { largestUnit })}`, `${diff.negated()}`)); it(`(${earlier}).plus(${diff}) == (${later})`, () => assert(earlier.plus(diff).equals(later))); it(`(${later}).minus(${diff}) == (${earlier})`, () => assert(later.minus(diff).equals(earlier))); it('symmetrical with regard to negative durations', () => { diff --git a/polyfill/test/time.mjs b/polyfill/test/time.mjs index 83c8660d9a..b38014f2be 100644 --- a/polyfill/test/time.mjs +++ b/polyfill/test/time.mjs @@ -215,9 +215,7 @@ describe('Time', () => { const duration = time.difference(two); equal(`${duration}`, 'PT1H53M'); }); - it('reverse argument order will throw', () => { - throws(() => one.difference(time, { largestUnit: 'minutes' }), RangeError); - }); + it(`(${two}).difference(${time}) => -PT1H53M`, () => equal(`${two.difference(time)}`, '-PT1H53M')); it("doesn't cast argument", () => { throws(() => time.difference({ hour: 16, minute: 34 }), TypeError); throws(() => time.difference('16:34'), TypeError); diff --git a/polyfill/test/yearmonth.mjs b/polyfill/test/yearmonth.mjs index 4001b04b74..de83f0f784 100644 --- a/polyfill/test/yearmonth.mjs +++ b/polyfill/test/yearmonth.mjs @@ -180,7 +180,8 @@ describe('YearMonth', () => { const nov94 = YearMonth.from('1994-11'); const jun13 = YearMonth.from('2013-06'); const diff = jun13.difference(nov94); - it('throws if out of order', () => throws(() => nov94.difference(jun13), RangeError)); + it(`${nov94}.difference(${jun13}) == ${jun13}.difference(${nov94}).negated()`, () => + equal(`${nov94.difference(jun13)}`, `${diff.negated()}`)); it(`${nov94}.plus(${diff}) == ${jun13}`, () => nov94.plus(diff).equals(jun13)); it(`${jun13}.minus(${diff}) == ${nov94}`, () => jun13.minus(diff).equals(nov94)); it("doesn't cast argument", () => { diff --git a/spec/absolute.html b/spec/absolute.html index c43f7cbcae..838363bc1f 100644 --- a/spec/absolute.html +++ b/spec/absolute.html @@ -288,11 +288,7 @@