Skip to content

Commit

Permalink
Largest-units-first subtraction and addition
Browse files Browse the repository at this point in the history
This commit implements the largest-units-first behavior defined in tc39#993.
This includes both addition and subtraction.

Note that until tc39#993 is implemented, this commit inefficiently calls
`DateTime.prototype.add` (or `subtract`) multiple times. This can be
removed once tc39#993 lands.
  • Loading branch information
justingrant committed Oct 21, 2020
1 parent 710661f commit 8ab7459
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 54 deletions.
68 changes: 68 additions & 0 deletions polyfill/lib/poc/ZonedDateTime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,74 @@ describe('ZonedDateTime', () => {
});
});

describe('math order of operations and options', () => {
const breakoutUnits = (
op: 'add' | 'subtract',
zdt: ZonedDateTime,
d: Temporal.Duration,
options: Temporal.ArithmeticOptions
) =>
zdt[op]({ years: d.years }, options)
[op]({ months: d.months }, options)
[op]({ weeks: d.weeks }, options)
[op]({ days: d.days }, options)
[op](
{
hours: d.hours,
minutes: d.minutes,
seconds: d.seconds,
milliseconds: d.milliseconds,
microseconds: d.microseconds,
nanoseconds: d.nanoseconds
},
options
);

it('order of operations: add / none', () => {
const zdt = ZonedDateTime.from('2020-01-31T00:00-08:00[America/Los_Angeles]');
const d = Temporal.Duration.from({ months: 1, days: 1 });
const options: Temporal.ArithmeticOptions | undefined = undefined;
const result = zdt.add(d, options);
equal(result.toString(), '2020-03-01T00:00-08:00[America/Los_Angeles]');
equal(breakoutUnits('add', zdt, d, options).toString(), result.toString());
});
it('order of operations: add / constrain', () => {
const zdt = ZonedDateTime.from('2020-01-31T00:00-08:00[America/Los_Angeles]');
const d = Temporal.Duration.from({ months: 1, days: 1 });
const options: Temporal.ArithmeticOptions | undefined = { overflow: 'constrain' };
const result = zdt.add(d, options);
equal(result.toString(), '2020-03-01T00:00-08:00[America/Los_Angeles]');
equal(breakoutUnits('add', zdt, d, options).toString(), result.toString());
});
it('order of operations: add / reject', () => {
const zdt = ZonedDateTime.from('2020-01-31T00:00-08:00[America/Los_Angeles]');
const d = Temporal.Duration.from({ months: 1, days: 1 });
const options: Temporal.ArithmeticOptions | undefined = { overflow: 'reject' };
throws(() => zdt.add(d, options));
});
it('order of operations: subtract / none', () => {
const zdt = ZonedDateTime.from('2020-03-31T00:00-07:00[America/Los_Angeles]');
const d = Temporal.Duration.from({ months: 1, days: 1 });
const options: Temporal.ArithmeticOptions | undefined = undefined;
const result = zdt.subtract(d, options);
equal(result.toString(), '2020-02-28T00:00-08:00[America/Los_Angeles]');
equal(breakoutUnits('subtract', zdt, d, options).toString(), result.toString());
});
it('order of operations: subtract / constrain', () => {
const zdt = ZonedDateTime.from('2020-03-31T00:00-07:00[America/Los_Angeles]');
const d = Temporal.Duration.from({ months: 1, days: 1 });
const options: Temporal.ArithmeticOptions | undefined = { overflow: 'constrain' };
const result = zdt.subtract(d, options);
equal(result.toString(), '2020-02-28T00:00-08:00[America/Los_Angeles]');
equal(breakoutUnits('subtract', zdt, d, options).toString(), result.toString());
});
it('order of operations: subtract / reject', () => {
const zdt = ZonedDateTime.from('2020-03-31T00:00-07:00[America/Los_Angeles]');
const d = Temporal.Duration.from({ months: 1, days: 1 });
const options: Temporal.ArithmeticOptions | undefined = { overflow: 'reject' };
throws(() => zdt.subtract(d, options));
});
});
describe('Structure', () => {
it('ZonedDateTime is a Function', () => {
equal(typeof ZonedDateTime, 'function');
Expand Down
40 changes: 13 additions & 27 deletions polyfill/lib/poc/ZonedDateTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,39 +184,25 @@ function doAddOrSubtract(
options: Temporal.ArithmeticOptions | undefined,
zonedDateTime: ZonedDateTime
): ZonedDateTime {
// If negative duration for add, change to a positive duration subtract.
// If negative duration for subtract, change to a positive duration add.
// By doing this, none of the code below must worry about negative durations.
let duration = Temporal.Duration.from(durationLike);
if (duration.sign < 0) {
duration = duration.negated();
op = op === 'add' ? 'subtract' : 'add';
}

const overflow = getOption(options, 'overflow', OVERFLOW_OPTIONS, 'constrain');
const { timeZone, calendar } = zonedDateTime;
const { timeDuration, dateDuration } = splitDuration(durationLike);

// RFC 5545 requires the date portion to be added in calendar days and the
// time portion to be added/subtracted in exact time. Subtraction works the
// same except that the order of operations is reversed: first the time units
// are subtracted using exact time, then date units are subtracted with
// calendar days.
// RFC 5545 requires the date portion to be added/subtracted in calendar days
// and the time portion to be added/subtracted in exact time.
// TODO: remove the manual order-of-operations hack below after #993 fix lands
// const dtIntermediate = zonedDateTime.toDateTime()[op](dateDuration, { overflow });
const { years, months, weeks, days } = dateDuration;
let dtIntermediate = zonedDateTime.toDateTime();
dtIntermediate = years ? dtIntermediate[op]({ years }, { overflow }) : dtIntermediate;
dtIntermediate = months ? dtIntermediate[op]({ months }, { overflow }) : dtIntermediate;
dtIntermediate = weeks ? dtIntermediate[op]({ weeks }, { overflow }) : dtIntermediate;
dtIntermediate = days ? dtIntermediate[op]({ days }, { overflow }) : dtIntermediate;
// Note that `{ disambiguation: 'compatible' }` is implicitly used below
// because this disambiguation behavior is required by RFC 5545.
if (op === 'add') {
// if addition, then order of operations is largest (date) units first
const dtIntermediate = zonedDateTime.toDateTime().add(dateDuration, { overflow });
const absIntermediate = dtIntermediate.toInstant(timeZone);
const absResult = absIntermediate.add(timeDuration);
return new ZonedDateTime(absResult.epochNanoseconds, timeZone, calendar);
} else {
// if subtraction, then order of operations is smallest (time) units first
const absIntermediate = zonedDateTime.toInstant().subtract(timeDuration);
const dtIntermediate = absIntermediate.toDateTime(timeZone, calendar);
const dtResult = dtIntermediate.subtract(dateDuration, { overflow });
return fromDateTime(dtResult, timeZone);
}
const absIntermediate = dtIntermediate.toInstant(timeZone);
const absResult = absIntermediate[op](timeDuration);
return new ZonedDateTime(absResult.epochNanoseconds, timeZone, calendar);
}

export class ZonedDateTime {
Expand Down
40 changes: 13 additions & 27 deletions polyfill/lib/zoneddatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -129,39 +129,25 @@ function fromDateTime(dateTime, timeZone, options) {

/** Identical logic for `add` and `subtract` */
function doAddOrSubtract(op, durationLike, options, zonedDateTime) {
// If negative duration for add, change to a positive duration subtract.
// If negative duration for subtract, change to a positive duration add.
// By doing this, none of the code below must worry about negative durations.
let duration = Temporal.Duration.from(durationLike);
if (duration.sign < 0) {
duration = duration.negated();
op = op === 'add' ? 'subtract' : 'add';
}

const overflow = getOption(options, 'overflow', OVERFLOW_OPTIONS, 'constrain');
const { timeZone, calendar } = zonedDateTime;
const { timeDuration, dateDuration } = splitDuration(durationLike);

// RFC 5545 requires the date portion to be added in calendar days and the
// time portion to be added/subtracted in exact time. Subtraction works the
// same except that the order of operations is reversed: first the time units
// are subtracted using exact time, then date units are subtracted with
// calendar days.
// RFC 5545 requires the date portion to be added/subtracted in calendar days
// and the time portion to be added/subtracted in exact time.
// TODO: remove the manual order-of-operations hack below after #993 fix lands
// const dtIntermediate = zonedDateTime.toDateTime()[op](dateDuration, { overflow });
const { years, months, weeks, days } = dateDuration;
let dtIntermediate = zonedDateTime.toDateTime();
dtIntermediate = years ? dtIntermediate[op]({ years }, { overflow }) : dtIntermediate;
dtIntermediate = months ? dtIntermediate[op]({ months }, { overflow }) : dtIntermediate;
dtIntermediate = weeks ? dtIntermediate[op]({ weeks }, { overflow }) : dtIntermediate;
dtIntermediate = days ? dtIntermediate[op]({ days }, { overflow }) : dtIntermediate;
// Note that `{ disambiguation: 'compatible' }` is implicitly used below
// because this disambiguation behavior is required by RFC 5545.
if (op === 'add') {
// if addition, then order of operations is largest (date) units first
const dtIntermediate = zonedDateTime.toDateTime().add(dateDuration, { overflow });
const absIntermediate = dtIntermediate.toInstant(timeZone);
const absResult = absIntermediate.add(timeDuration);
return new ZonedDateTime(absResult.epochNanoseconds, timeZone, calendar);
} else {
// if subtraction, then order of operations is smallest (time) units first
const absIntermediate = zonedDateTime.toInstant().subtract(timeDuration);
const dtIntermediate = absIntermediate.toDateTime(timeZone, calendar);
const dtResult = dtIntermediate.subtract(dateDuration, { overflow });
return fromDateTime(dtResult, timeZone);
}
const absIntermediate = dtIntermediate.toInstant(timeZone);
const absResult = absIntermediate[op](timeDuration);
return new ZonedDateTime(absResult.epochNanoseconds, timeZone, calendar);
}

export class ZonedDateTime {
Expand Down
64 changes: 64 additions & 0 deletions polyfill/test/zoneddatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,70 @@ describe('ZonedDateTime', () => {
});
});

describe('math order of operations and options', () => {
const breakoutUnits = (op, zdt, d, options) =>
zdt[op]({ years: d.years }, options)
[op]({ months: d.months }, options)
[op]({ weeks: d.weeks }, options)
[op]({ days: d.days }, options)
[op](
{
hours: d.hours,
minutes: d.minutes,
seconds: d.seconds,
milliseconds: d.milliseconds,
microseconds: d.microseconds,
nanoseconds: d.nanoseconds
},

options
);

it('order of operations: add / none', () => {
const zdt = ZonedDateTime.from('2020-01-31T00:00-08:00[America/Los_Angeles]');
const d = Temporal.Duration.from({ months: 1, days: 1 });
const options = undefined;
const result = zdt.add(d, options);
equal(result.toString(), '2020-03-01T00:00-08:00[America/Los_Angeles]');
equal(breakoutUnits('add', zdt, d, options).toString(), result.toString());
});
it('order of operations: add / constrain', () => {
const zdt = ZonedDateTime.from('2020-01-31T00:00-08:00[America/Los_Angeles]');
const d = Temporal.Duration.from({ months: 1, days: 1 });
const options = { overflow: 'constrain' };
const result = zdt.add(d, options);
equal(result.toString(), '2020-03-01T00:00-08:00[America/Los_Angeles]');
equal(breakoutUnits('add', zdt, d, options).toString(), result.toString());
});
it('order of operations: add / reject', () => {
const zdt = ZonedDateTime.from('2020-01-31T00:00-08:00[America/Los_Angeles]');
const d = Temporal.Duration.from({ months: 1, days: 1 });
const options = { overflow: 'reject' };
throws(() => zdt.add(d, options));
});
it('order of operations: subtract / none', () => {
const zdt = ZonedDateTime.from('2020-03-31T00:00-07:00[America/Los_Angeles]');
const d = Temporal.Duration.from({ months: 1, days: 1 });
const options = undefined;
const result = zdt.subtract(d, options);
equal(result.toString(), '2020-02-28T00:00-08:00[America/Los_Angeles]');
equal(breakoutUnits('subtract', zdt, d, options).toString(), result.toString());
});
it('order of operations: subtract / constrain', () => {
const zdt = ZonedDateTime.from('2020-03-31T00:00-07:00[America/Los_Angeles]');
const d = Temporal.Duration.from({ months: 1, days: 1 });
const options = { overflow: 'constrain' };
const result = zdt.subtract(d, options);
equal(result.toString(), '2020-02-28T00:00-08:00[America/Los_Angeles]');
equal(breakoutUnits('subtract', zdt, d, options).toString(), result.toString());
});
it('order of operations: subtract / reject', () => {
const zdt = ZonedDateTime.from('2020-03-31T00:00-07:00[America/Los_Angeles]');
const d = Temporal.Duration.from({ months: 1, days: 1 });
const options = { overflow: 'reject' };
throws(() => zdt.subtract(d, options));
});
});
describe('Structure', () => {
it('ZonedDateTime is a Function', () => {
equal(typeof ZonedDateTime, 'function');
Expand Down

0 comments on commit 8ab7459

Please sign in to comment.