Skip to content

Commit

Permalink
Balance out of scope duration fields (#418)
Browse files Browse the repository at this point in the history
* Arithmetic with out-of-scope units for Date, Time, YearMonth

Out-of-scope units are ignored if they are higher than the type's units
and wouldn't have any effect (e.g. adding years to a Time).

Out-of-scope units are balanced up to the type's lowest unit if they are
lower than the type's units, and any remaining fractions of the type's
lowest unit are ignored (e.g. adding 36 hours to a Date adds 1 day.) In
the case of YearMonth, days are balanced into months based on the first
day of the month.

Closes: #324 

Co-Authored-By: Ms2ger <[email protected]>

Co-authored-by: Ms2ger <[email protected]>
  • Loading branch information
ptomato and Ms2ger authored Mar 30, 2020
1 parent 8fcc798 commit bb4718f
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 81 deletions.
50 changes: 25 additions & 25 deletions polyfill/lib/date.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,7 @@ import {
NANOSECOND,
CreateSlots,
GetSlot,
SetSlot,
HOURS,
MINUTES,
SECONDS,
MILLISECONDS,
MICROSECONDS,
NANOSECONDS
SetSlot
} from './slots.mjs';

export class Date {
Expand Down Expand Up @@ -85,16 +79,19 @@ export class Date {
plus(temporalDurationLike, options) {
if (!ES.IsTemporalDate(this)) throw new TypeError('invalid receiver');
const disambiguation = ES.ToArithmeticTemporalDisambiguation(options);
const duration = ES.ToLimitedTemporalDuration(temporalDurationLike, [
HOURS,
MINUTES,
SECONDS,
MILLISECONDS,
MICROSECONDS,
NANOSECONDS
]);
const duration = ES.ToLimitedTemporalDuration(temporalDurationLike);
let { year, month, day } = this;
const { years, months, days } = duration;
const { years, months, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration;
const { days } = ES.BalanceDuration(
duration.days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
'days'
);
({ year, month, day } = ES.AddDate(year, month, day, years, months, days, disambiguation));
({ year, month, day } = ES.RegulateDate(year, month, day, disambiguation));
const Construct = ES.SpeciesConstructor(this, Date);
Expand All @@ -105,16 +102,19 @@ export class Date {
minus(temporalDurationLike, options) {
if (!ES.IsTemporalDate(this)) throw new TypeError('invalid receiver');
const disambiguation = ES.ToArithmeticTemporalDisambiguation(options);
const duration = ES.ToLimitedTemporalDuration(temporalDurationLike, [
HOURS,
MINUTES,
SECONDS,
MILLISECONDS,
MICROSECONDS,
NANOSECONDS
]);
const duration = ES.ToLimitedTemporalDuration(temporalDurationLike);
let { year, month, day } = this;
const { years, months, days } = duration;
const { years, months, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration;
const { days } = ES.BalanceDuration(
duration.days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
'days'
);
({ year, month, day } = ES.SubtractDate(year, month, day, years, months, days, disambiguation));
({ year, month, day } = ES.RegulateDate(year, month, day, disambiguation));
const Construct = ES.SpeciesConstructor(this, Date);
Expand Down
9 changes: 3 additions & 6 deletions polyfill/lib/time.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ import {
NANOSECOND,
CreateSlots,
GetSlot,
SetSlot,
YEARS,
MONTHS,
DAYS
SetSlot
} from './slots.mjs';

export class Time {
Expand Down Expand Up @@ -102,7 +99,7 @@ export class Time {
plus(temporalDurationLike, options) {
if (!ES.IsTemporalTime(this)) throw new TypeError('invalid receiver');
let { hour, minute, second, millisecond, microsecond, nanosecond } = this;
const duration = ES.ToLimitedTemporalDuration(temporalDurationLike, [YEARS, MONTHS, DAYS]);
const duration = ES.ToLimitedTemporalDuration(temporalDurationLike);
const disambiguation = ES.ToArithmeticTemporalDisambiguation(options);
const { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration;
({ hour, minute, second, millisecond, microsecond, nanosecond } = ES.AddTime(
Expand Down Expand Up @@ -136,7 +133,7 @@ export class Time {
minus(temporalDurationLike, options) {
if (!ES.IsTemporalTime(this)) throw new TypeError('invalid receiver');
let { hour, minute, second, millisecond, microsecond, nanosecond } = this;
const duration = ES.ToLimitedTemporalDuration(temporalDurationLike, [YEARS, MONTHS, DAYS]);
const duration = ES.ToLimitedTemporalDuration(temporalDurationLike);
const disambiguation = ES.ToArithmeticTemporalDisambiguation(options);
const { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration;
({ hour, minute, second, millisecond, microsecond, nanosecond } = ES.SubtractTime(
Expand Down
64 changes: 28 additions & 36 deletions polyfill/lib/yearmonth.mjs
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
import { ES } from './ecmascript.mjs';
import { MakeIntrinsicClass } from './intrinsicclass.mjs';
import {
YEAR,
MONTH,
CreateSlots,
GetSlot,
SetSlot,
DAYS,
HOURS,
MINUTES,
SECONDS,
MILLISECONDS,
MICROSECONDS,
NANOSECONDS
} from './slots.mjs';
import { YEAR, MONTH, CreateSlots, GetSlot, SetSlot } from './slots.mjs';

export class YearMonth {
constructor(year, month) {
Expand Down Expand Up @@ -61,18 +48,20 @@ export class YearMonth {
plus(temporalDurationLike, options) {
if (!ES.IsTemporalYearMonth(this)) throw new TypeError('invalid receiver');
const disambiguation = ES.ToArithmeticTemporalDisambiguation(options);
const duration = ES.ToLimitedTemporalDuration(temporalDurationLike, [
DAYS,
HOURS,
MINUTES,
SECONDS,
MILLISECONDS,
MICROSECONDS,
NANOSECONDS
]);
const duration = ES.ToLimitedTemporalDuration(temporalDurationLike);
let { year, month } = this;
const { years, months } = duration;
({ year, month } = ES.AddDate(year, month, 1, years, months, 0, disambiguation));
const { years, months, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration;
const { days } = ES.BalanceDuration(
duration.days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
'days'
);
({ year, month } = ES.AddDate(year, month, 1, years, months, days, disambiguation));
({ year, month } = ES.BalanceYearMonth(year, month));
({ year, month } = ES.RegulateYearMonth(year, month, disambiguation));
const Construct = ES.SpeciesConstructor(this, YearMonth);
Expand All @@ -83,18 +72,21 @@ export class YearMonth {
minus(temporalDurationLike, options) {
if (!ES.IsTemporalYearMonth(this)) throw new TypeError('invalid receiver');
const disambiguation = ES.ToArithmeticTemporalDisambiguation(options);
const duration = ES.ToLimitedTemporalDuration(temporalDurationLike, [
DAYS,
HOURS,
MINUTES,
SECONDS,
MILLISECONDS,
MICROSECONDS,
NANOSECONDS
]);
const duration = ES.ToLimitedTemporalDuration(temporalDurationLike);
let { year, month } = this;
const { years, months } = duration;
({ year, month } = ES.SubtractDate(year, month, 1, years, months, 0, disambiguation));
const { years, months, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration;
const { days } = ES.BalanceDuration(
duration.days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
'days'
);
const lastDay = ES.DaysInMonth(year, month);
({ year, month } = ES.SubtractDate(year, month, lastDay, years, months, days, disambiguation));
({ year, month } = ES.BalanceYearMonth(year, month));
({ year, month } = ES.RegulateYearMonth(year, month, disambiguation));
const Construct = ES.SpeciesConstructor(this, YearMonth);
Expand Down
36 changes: 36 additions & 0 deletions polyfill/test/date.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,24 @@ describe('Date', () => {
const jan31 = Date.from('2020-01-31');
throws(() => jan31.plus({ months: 1 }, { disambiguation: 'reject' }), RangeError);
});
it("ignores lower units that don't balance up to a day", () => {
equal(`${date.plus({ hours: 1 })}`, '1976-11-18');
equal(`${date.plus({ minutes: 1 })}`, '1976-11-18');
equal(`${date.plus({ seconds: 1 })}`, '1976-11-18');
equal(`${date.plus({ milliseconds: 1 })}`, '1976-11-18');
equal(`${date.plus({ microseconds: 1 })}`, '1976-11-18');
equal(`${date.plus({ nanoseconds: 1 })}`, '1976-11-18');
});
it('adds lower units that balance up to a day or more', () => {
equal(`${date.plus({ hours: 24 })}`, '1976-11-19');
equal(`${date.plus({ hours: 36 })}`, '1976-11-19');
equal(`${date.plus({ hours: 48 })}`, '1976-11-20');
equal(`${date.plus({ minutes: 1440 })}`, '1976-11-19');
equal(`${date.plus({ seconds: 86400 })}`, '1976-11-19');
equal(`${date.plus({ milliseconds: 86400_000 })}`, '1976-11-19');
equal(`${date.plus({ microseconds: 86400_000_000 })}`, '1976-11-19');
equal(`${date.plus({ nanoseconds: 86400_000_000_000 })}`, '1976-11-19');
});
it('invalid disambiguation', () => {
['', 'CONSTRAIN', 'balance', 3, null].forEach((disambiguation) =>
throws(() => date.plus({ months: 1 }, { disambiguation }), RangeError)
Expand Down Expand Up @@ -271,6 +289,24 @@ describe('Date', () => {
const mar31 = Date.from('2020-03-31');
throws(() => mar31.minus({ months: 1 }, { disambiguation: 'reject' }), RangeError);
});
it("ignores lower units that don't balance up to a day", () => {
equal(`${date.minus({ hours: 1 })}`, '2019-11-18');
equal(`${date.minus({ minutes: 1 })}`, '2019-11-18');
equal(`${date.minus({ seconds: 1 })}`, '2019-11-18');
equal(`${date.minus({ milliseconds: 1 })}`, '2019-11-18');
equal(`${date.minus({ microseconds: 1 })}`, '2019-11-18');
equal(`${date.minus({ nanoseconds: 1 })}`, '2019-11-18');
});
it('subtracts lower units that balance up to a day or more', () => {
equal(`${date.minus({ hours: 24 })}`, '2019-11-17');
equal(`${date.minus({ hours: 36 })}`, '2019-11-17');
equal(`${date.minus({ hours: 48 })}`, '2019-11-16');
equal(`${date.minus({ minutes: 1440 })}`, '2019-11-17');
equal(`${date.minus({ seconds: 86400 })}`, '2019-11-17');
equal(`${date.minus({ milliseconds: 86400_000 })}`, '2019-11-17');
equal(`${date.minus({ microseconds: 86400_000_000 })}`, '2019-11-17');
equal(`${date.minus({ nanoseconds: 86400_000_000_000 })}`, '2019-11-17');
});
it('invalid disambiguation', () => {
['', 'CONSTRAIN', 'balance', 3, null].forEach((disambiguation) =>
throws(() => date.minus({ months: 1 }, { disambiguation }), RangeError)
Expand Down
10 changes: 10 additions & 0 deletions polyfill/test/time.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,11 @@ describe('Time', () => {
it('time.plus(durationObj)', () => {
equal(`${time.plus(Temporal.Duration.from('PT16H'))}`, '07:23:30.123456789');
});
it('ignores higher units', () => {
equal(`${time.plus({ days: 1 })}`, '15:23:30.123456789');
equal(`${time.plus({ months: 1 })}`, '15:23:30.123456789');
equal(`${time.plus({ years: 1 })}`, '15:23:30.123456789');
});
it('invalid disambiguation', () => {
['', 'CONSTRAIN', 'balance', 3, null].forEach((disambiguation) =>
throws(() => time.plus({ hours: 1 }, { disambiguation }), RangeError)
Expand All @@ -352,6 +357,11 @@ describe('Time', () => {
it('time.minus(durationObj)', () => {
equal(`${time.minus(Temporal.Duration.from('PT16H'))}`, '23:23:30.123456789');
});
it('ignores higher units', () => {
equal(`${time.minus({ days: 1 })}`, '15:23:30.123456789');
equal(`${time.minus({ months: 1 })}`, '15:23:30.123456789');
equal(`${time.minus({ years: 1 })}`, '15:23:30.123456789');
});
it('invalid disambiguation', () => {
['', 'CONSTRAIN', 'balance', 3, null].forEach((disambiguation) =>
throws(() => time.minus({ hours: 1 }, { disambiguation }), RangeError)
Expand Down
63 changes: 63 additions & 0 deletions polyfill/test/yearmonth.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,38 @@ describe('YearMonth', () => {
it('yearMonth.plus(durationObj)', () => {
equal(`${ym.plus(Temporal.Duration.from('P2M'))}`, '2020-01');
});
it("ignores lower units that don't balance up to the length of the month", () => {
equal(`${ym.plus({ days: 1 })}`, '2019-11');
equal(`${ym.plus({ days: 29 })}`, '2019-11');
equal(`${ym.plus({ hours: 1 })}`, '2019-11');
equal(`${ym.plus({ minutes: 1 })}`, '2019-11');
equal(`${ym.plus({ seconds: 1 })}`, '2019-11');
equal(`${ym.plus({ milliseconds: 1 })}`, '2019-11');
equal(`${ym.plus({ microseconds: 1 })}`, '2019-11');
equal(`${ym.plus({ nanoseconds: 1 })}`, '2019-11');
});
it('adds lower units that balance up to a month or more', () => {
equal(`${ym.plus({ days: 30 })}`, '2019-12');
equal(`${ym.plus({ days: 31 })}`, '2019-12');
equal(`${ym.plus({ days: 60 })}`, '2019-12');
equal(`${ym.plus({ days: 61 })}`, '2020-01');
equal(`${ym.plus({ hours: 720 })}`, '2019-12');
equal(`${ym.plus({ minutes: 43200 })}`, '2019-12');
equal(`${ym.plus({ seconds: 2592000 })}`, '2019-12');
equal(`${ym.plus({ milliseconds: 2592000_000 })}`, '2019-12');
equal(`${ym.plus({ microseconds: 2592000_000_000 })}`, '2019-12');
equal(`${ym.plus({ nanoseconds: 2592000_000_000_000 })}`, '2019-12');
});
it('balances days to months based on the number of days in the ISO month', () => {
equal(`${YearMonth.from('2019-02').plus({ days: 27 })}`, '2019-02');
equal(`${YearMonth.from('2019-02').plus({ days: 28 })}`, '2019-03');
equal(`${YearMonth.from('2020-02').plus({ days: 28 })}`, '2020-02');
equal(`${YearMonth.from('2020-02').plus({ days: 29 })}`, '2020-03');
equal(`${YearMonth.from('2019-11').plus({ days: 29 })}`, '2019-11');
equal(`${YearMonth.from('2019-11').plus({ days: 30 })}`, '2019-12');
equal(`${YearMonth.from('2020-01').plus({ days: 30 })}`, '2020-01');
equal(`${YearMonth.from('2020-01').plus({ days: 31 })}`, '2020-02');
});
it('invalid disambiguation', () => {
['', 'CONSTRAIN', 'balance', 3, null].forEach((disambiguation) =>
throws(() => ym.plus({ months: 1 }, { disambiguation }), RangeError)
Expand All @@ -164,6 +196,37 @@ describe('YearMonth', () => {
it('yearMonth.minus(durationObj)', () => {
equal(`${ym.minus(Temporal.Duration.from('P11M'))}`, '2018-12');
});
it("ignores lower units that don't balance up to the length of the month", () => {
equal(`${ym.minus({ days: 1 })}`, '2019-11');
equal(`${ym.minus({ hours: 1 })}`, '2019-11');
equal(`${ym.minus({ minutes: 1 })}`, '2019-11');
equal(`${ym.minus({ seconds: 1 })}`, '2019-11');
equal(`${ym.minus({ milliseconds: 1 })}`, '2019-11');
equal(`${ym.minus({ microseconds: 1 })}`, '2019-11');
equal(`${ym.minus({ nanoseconds: 1 })}`, '2019-11');
});
it('subtracts lower units that balance up to a day or more', () => {
equal(`${ym.minus({ days: 29 })}`, '2019-11');
equal(`${ym.minus({ days: 30 })}`, '2019-10');
equal(`${ym.minus({ days: 60 })}`, '2019-10');
equal(`${ym.minus({ days: 61 })}`, '2019-09');
equal(`${ym.minus({ hours: 720 })}`, '2019-10');
equal(`${ym.minus({ minutes: 43200 })}`, '2019-10');
equal(`${ym.minus({ seconds: 2592000 })}`, '2019-10');
equal(`${ym.minus({ milliseconds: 2592000_000 })}`, '2019-10');
equal(`${ym.minus({ microseconds: 2592000_000_000 })}`, '2019-10');
equal(`${ym.minus({ nanoseconds: 2592000_000_000_000 })}`, '2019-10');
});
it('balances days to months based on the number of days in the ISO month', () => {
equal(`${YearMonth.from('2019-02').minus({ days: 27 })}`, '2019-02');
equal(`${YearMonth.from('2019-02').minus({ days: 28 })}`, '2019-01');
equal(`${YearMonth.from('2020-02').minus({ days: 28 })}`, '2020-02');
equal(`${YearMonth.from('2020-02').minus({ days: 29 })}`, '2020-01');
equal(`${YearMonth.from('2019-11').minus({ days: 29 })}`, '2019-11');
equal(`${YearMonth.from('2019-11').minus({ days: 30 })}`, '2019-10');
equal(`${YearMonth.from('2020-01').minus({ days: 30 })}`, '2020-01');
equal(`${YearMonth.from('2020-01').minus({ days: 31 })}`, '2019-12');
});
it('invalid disambiguation', () => {
['', 'CONSTRAIN', 'balance', 3, null].forEach((disambiguation) =>
throws(() => ym.minus({ months: 1 }, { disambiguation }), RangeError)
Expand Down
10 changes: 6 additions & 4 deletions spec/date.html
Original file line number Diff line number Diff line change
Expand Up @@ -261,9 +261,10 @@ <h1>Temporal.Date.prototype.plus ( _temporalDurationLike_ [ , _options_ ] )</h1>
<emu-alg>
1. Let _temporalDate_ be the *this* value.
1. Perform ? RequireInternalSlot(_temporalDate_, [[InitializedTemporalDate]]).
1. Let _duration_ be ? ToLimitedTemporalDuration(_temporalDurationLike_, « *"hours"*, *"minutes"*, *"seconds"*, *"milliseconds"*, *"microseconds"*, *"nanoseconds"* »).
1. Let _duration_ be ? ToLimitedTemporalDuration(_temporalDurationLike_, « »).
1. Let _balanceResult_ be ? BalanceDuration(_duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]], *"days"*).
1. Let _disambiguation_ be ? ToArithmeticTemporalDisambiguation(_options_).
1. Let _result_ be ? AddDate(_temporalDate_.[[Year]], _temporalDate_.[[Month]], _temporalDate_.[[Day]], _duration_.[[Years]], _duration_.[[Months]], _duration_.[[Days]], _disambiguation_).
1. Let _result_ be ? AddDate(_temporalDate_.[[Year]], _temporalDate_.[[Month]], _temporalDate_.[[Day]], _duration_.[[Years]], _duration_.[[Months]], _balanceResult_.[[Days]], _disambiguation_).
1. Assert: ! ValidateDate(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]]) is *true*.
1. Return ? CreateTemporalDateFromInstance(_temporalDate_, _result_.[[Year]], _result_.[[Month]], _result_.[[Day]]).
</emu-alg>
Expand All @@ -278,9 +279,10 @@ <h1>Temporal.Date.prototype.minus ( _temporalDurationLike_ [ , _options_ ] )</h1
<emu-alg>
1. Let _temporalDate_ be the *this* value.
1. Perform ? RequireInternalSlot(_temporalDate_, [[InitializedTemporalDate]]).
1. Let _duration_ be ? ToLimitedTemporalDuration(_temporalDurationLike_, « *"hours"*, *"minutes"*, *"seconds"*, *"milliseconds"*, *"microseconds"*, *"nanoseconds"* »).
1. Let _duration_ be ? ToLimitedTemporalDuration(_temporalDurationLike_, « »).
1. Let _balanceResult_ be ? BalanceDuration(_duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]], *"days"*).
1. Let _disambiguation_ be ? ToArithmeticTemporalDisambiguation(_options_).
1. Let _result_ be ? SubtractDate(_temporalDate_.[[Year]], _temporalDate_.[[Month]], _temporalDate_.[[Day]], _duration_.[[Years]], _duration_.[[Months]], _duration_.[[Days]], _disambiguation_).
1. Let _result_ be ? SubtractDate(_temporalDate_.[[Year]], _temporalDate_.[[Month]], _temporalDate_.[[Day]], _duration_.[[Years]], _duration_.[[Months]], _balanceResult_.[[Days]], _disambiguation_).
1. Assert: ! ValidateDate(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]]) is *true*.
1. Return ? CreateTemporalDateFromInstance(_temporalDate_, _result_.[[Year]], _result_.[[Month]], _result_.[[Day]]).
</emu-alg>
Expand Down
Loading

0 comments on commit bb4718f

Please sign in to comment.