Skip to content

Commit

Permalink
Allow difference() to return negative durations
Browse files Browse the repository at this point in the history
This undoes the work in commit d84653e
although it's not exactly a revert, because the situation before that
was that durations were always positive. We also don't reinstate the
behaviour of never returning a duration larger than 12 hours.

Now, for all types, calling smaller.difference(larger) will result in a
negative duration.

See: #782
  • Loading branch information
ptomato committed Aug 12, 2020
1 parent db01da2 commit 51a801c
Show file tree
Hide file tree
Showing 24 changed files with 157 additions and 138 deletions.
2 changes: 1 addition & 1 deletion docs/absolute.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion docs/cookbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
44 changes: 10 additions & 34 deletions docs/cookbook/getElapsedDurationSinceInstant.mjs
Original file line number Diff line number Diff line change
@@ -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`;
4 changes: 2 additions & 2 deletions docs/date.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions docs/datetime.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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}));
Expand Down
4 changes: 2 additions & 2 deletions docs/time.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/yearmonth.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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
Expand Down
2 changes: 0 additions & 2 deletions polyfill/lib/absolute.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,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);
Expand Down
10 changes: 5 additions & 5 deletions polyfill/lib/calendar.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ export class Calendar {
void constructor;
throw new Error('not implemented');
}
difference(smaller, larger, options) {
void smaller;
void larger;
difference(one, two, options) {
void one;
void two;
void options;
throw new Error('not implemented');
}
Expand Down Expand Up @@ -174,10 +174,10 @@ class ISO8601 extends Calendar {
}
return new constructor(year, month, day, this);
}
difference(smaller, larger, options) {
difference(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);
}
Expand Down
2 changes: 0 additions & 2 deletions polyfill/lib/date.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,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.difference(other, this, options);
}
equals(other) {
Expand Down
12 changes: 8 additions & 4 deletions polyfill/lib/datetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.difference(other, adjustedLarger, dateOptions);
const dateDifference = calendar.difference(otherDate, adjusted, dateOptions);

let days;
({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.BalanceDuration(
Expand Down
66 changes: 58 additions & 8 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,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,
Expand Down Expand Up @@ -1083,6 +1092,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 };
},

Expand Down Expand Up @@ -1172,7 +1189,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;
Expand Down Expand Up @@ -1225,15 +1253,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,
Expand All @@ -1244,6 +1285,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) => {
Expand Down
2 changes: 0 additions & 2 deletions polyfill/lib/time.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,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,
Expand Down
12 changes: 5 additions & 7 deletions polyfill/lib/yearmonth.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.difference(smaller, larger, { ...options, largestUnit });
const otherDate = calendar.dateFromFields({ ...otherFields, day: 1 }, {}, TemporalDate);
const thisDate = calendar.dateFromFields({ ...thisFields, day: 1 }, {}, TemporalDate);
return calendar.difference(otherDate, thisDate, { ...options, largestUnit });
}
equals(other) {
if (!ES.IsTemporalYearMonth(this)) throw new TypeError('invalid receiver');
Expand Down
3 changes: 2 additions & 1 deletion polyfill/test/absolute.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,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", () => {
Expand Down
3 changes: 2 additions & 1 deletion polyfill/test/datetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
4 changes: 1 addition & 3 deletions polyfill/test/time.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit 51a801c

Please sign in to comment.