Skip to content

Commit

Permalink
Implement ZonedDateTime.add() and ZonedDateTime.subtract()
Browse files Browse the repository at this point in the history
Two tests are skipped due to the change in order-of-operations that will
land in #993.

Co-authored-by: Justin Grant <[email protected]>

See: #569
  • Loading branch information
ptomato authored and Ms2ger committed Nov 3, 2020
1 parent 72a02b8 commit 1a3b00d
Show file tree
Hide file tree
Showing 5 changed files with 347 additions and 10 deletions.
8 changes: 4 additions & 4 deletions docs/zoneddatetime.md
Original file line number Diff line number Diff line change
Expand Up @@ -772,13 +772,13 @@ Usage example:
zdt = Temporal.ZonedDateTime.from('2020-03-08T00:00-08:00[America/Los_Angeles]');
// Add a day to get midnight on the day after DST starts
laterDay = zdt.add({ days: 1 });
// => 2020-03-09T00:00-07:00[America/Los_Angeles];
// => 2020-03-09T00:00:00-07:00[America/Los_Angeles];
// Note that the new offset is different, indicating the result is adjusted for DST.
laterDay.since(zdt, { largestUnit: 'hours' }).hours;
// => 23, because one clock hour lost to DST

laterHours = zdt.add({ hours: 24 });
// => 2020-03-09T01:00-07:00[America/Los_Angeles]
// => 2020-03-09T01:00:00-07:00[America/Los_Angeles]
// Adding time units doesn't adjust for DST. Result is 1:00AM: 24 real-world
// hours later because a clock hour was skipped by DST.
laterHours.since(zdt, { largestUnit: 'hours' }).hours; // => 24
Expand Down Expand Up @@ -836,13 +836,13 @@ Usage example:
zdt = Temporal.ZonedDateTime.from('2020-03-09T00:00-07:00[America/Los_Angeles]');
// Add a day to get midnight on the day after DST starts
earlierDay = zdt.subtract({ days: 1 });
// => 2020-03-08T00:00-08:00[America/Los_Angeles]
// => 2020-03-08T00:00:00-08:00[America/Los_Angeles]
// Note that the new offset is different, indicating the result is adjusted for DST.
earlierDay.since(zdt, { largestUnit: 'hours' }).hours;
// => -23, because one clock hour lost to DST

earlierHours = zdt.subtract({ hours: 24 });
// => 2020-03-07T23:00-08:00[America/Los_Angeles]
// => 2020-03-07T23:00:00-08:00[America/Los_Angeles]
// Subtracting time units doesn't adjust for DST. Result is 11:00PM: 24 real-world
// hours earlier because a clock hour was skipped by DST.
earlierHours.since(zdt, { largestUnit: 'hours' }).hours; // => -24
Expand Down
66 changes: 66 additions & 0 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2441,6 +2441,72 @@ export const ES = ObjectAssign({}, ES2020, {
ES.RejectInstantRange(result);
return result;
},
AddZonedDateTime: (instant, timeZone, calendar, years, months, weeks, days, h, min, s, ms, µs, ns, overflow) => {
// If only time is to be added, then use Instant math. It's not OK to fall
// through to the date/time code below because compatible disambiguation in
// the PlainDateTime=>Instant conversion will change the offset of any
// ZonedDateTtime in the repeated clock time after a backwards transition.
// When adding/subtracting time units and not dates, this disambiguation is
// not expected and so is avoided below via a fast path for time-only
// arithmetic.
// BTW, this behavior is similar in spirit to offset: 'prefer' in `with`.
if (ES.DurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0) === 0) {
return ES.AddInstant(GetSlot(instant, EPOCHNANOSECONDS), h, min, s, ms, µs, ns);
}

// RFC 5545 requires the date portion to be added in calendar days and the
// time portion to be added in exact time.
// FIXME: "op" and the dateAdd/dateSubtract conditional will not be needed
// after #993 lands, changing the order of operations to be the same for
// both addition and subtraction.
let dt = ES.GetTemporalDateTimeFor(timeZone, instant, calendar);
const TemporalDate = GetIntrinsic('%Temporal.Date%');
const datePart = new TemporalDate(GetSlot(dt, ISO_YEAR), GetSlot(dt, ISO_MONTH), GetSlot(dt, ISO_DAY), calendar);
const addedDate = calendar.dateAdd(datePart, { years, months, weeks, days }, { overflow }, TemporalDate);
const TemporalDateTime = GetIntrinsic('%Temporal.DateTime%');
const dtIntermediate = new TemporalDateTime(
GetSlot(addedDate, ISO_YEAR),
GetSlot(addedDate, ISO_MONTH),
GetSlot(addedDate, ISO_DAY),
GetSlot(dt, ISO_HOUR),
GetSlot(dt, ISO_MINUTE),
GetSlot(dt, ISO_SECOND),
GetSlot(dt, ISO_MILLISECOND),
GetSlot(dt, ISO_MICROSECOND),
GetSlot(dt, ISO_NANOSECOND),
calendar
);

// Note that 'compatible' is used below because this disambiguation behavior
// is required by RFC 5545.
const instantIntermediate = ES.GetTemporalInstantFor(timeZone, dtIntermediate, 'compatible');
return ES.AddInstant(GetSlot(instantIntermediate, EPOCHNANOSECONDS), h, min, s, ms, µs, ns);
},
SubtractZonedDateTime: (instant, timeZone, calendar, years, months, weeks, days, h, min, s, ms, µs, ns, overflow) => {
// FIXME: to be consolidated into AddZonedDateTime by #993
if (ES.DurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0) === 0) {
return ES.AddInstant(GetSlot(instant, EPOCHNANOSECONDS), -h, -min, -s, -ms, -µs, -ns);
}
let dt = ES.GetTemporalDateTimeFor(timeZone, instant, calendar);
const TemporalDate = GetIntrinsic('%Temporal.Date%');
const datePart = new TemporalDate(GetSlot(dt, ISO_YEAR), GetSlot(dt, ISO_MONTH), GetSlot(dt, ISO_DAY), calendar);
const subtractedDate = calendar.dateSubtract(datePart, { years, months, weeks, days }, { overflow }, TemporalDate);
const TemporalDateTime = GetIntrinsic('%Temporal.DateTime%');
const dtIntermediate = new TemporalDateTime(
GetSlot(subtractedDate, ISO_YEAR),
GetSlot(subtractedDate, ISO_MONTH),
GetSlot(subtractedDate, ISO_DAY),
GetSlot(dt, ISO_HOUR),
GetSlot(dt, ISO_MINUTE),
GetSlot(dt, ISO_SECOND),
GetSlot(dt, ISO_MILLISECOND),
GetSlot(dt, ISO_MICROSECOND),
GetSlot(dt, ISO_NANOSECOND),
calendar
);
const instantIntermediate = ES.GetTemporalInstantFor(timeZone, dtIntermediate, 'compatible');
return ES.AddInstant(GetSlot(instantIntermediate, EPOCHNANOSECONDS), -h, -min, -s, -ms, -µs, -ns);
},
RoundNumberToIncrement: (quantity, increment, mode) => {
const quotient = quantity / increment;
let round;
Expand Down
48 changes: 44 additions & 4 deletions polyfill/lib/zoneddatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,28 @@ export class ZonedDateTime {
ES.RejectDurationSign(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds);
options = ES.NormalizeOptionsObject(options);
const overflow = ES.ToTemporalOverflow(options);
void overflow;
throw new Error('add() not implemented yet');
const timeZone = GetSlot(this, TIME_ZONE);
const calendar = GetSlot(this, CALENDAR);
const epochNanoseconds = ES.AddZonedDateTime(
GetSlot(this, INSTANT),
timeZone,
calendar,
years,
months,
weeks,
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
overflow
);
const Construct = ES.SpeciesConstructor(this, ZonedDateTime);
const result = new Construct(epochNanoseconds, timeZone, calendar);
if (!ES.IsTemporalZonedDateTime(result)) throw new TypeError('invalid result');
return result;
}
subtract(temporalDurationLike, options = undefined) {
if (!ES.IsTemporalZonedDateTime(this)) throw new TypeError('invalid receiver');
Expand All @@ -247,8 +267,28 @@ export class ZonedDateTime {
ES.RejectDurationSign(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds);
options = ES.NormalizeOptionsObject(options);
const overflow = ES.ToTemporalOverflow(options);
void overflow;
throw new Error('subtract() not implemented yet');
const timeZone = GetSlot(this, TIME_ZONE);
const calendar = GetSlot(this, CALENDAR);
const epochNanoseconds = ES.SubtractZonedDateTime(
GetSlot(this, INSTANT),
timeZone,
calendar,
years,
months,
weeks,
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
overflow
);
const Construct = ES.SpeciesConstructor(this, ZonedDateTime);
const result = new Construct(epochNanoseconds, timeZone, calendar);
if (!ES.IsTemporalZonedDateTime(result)) throw new TypeError('invalid result');
return result;
}
until(other, options = undefined) {
if (!ES.IsTemporalZonedDateTime(this)) throw new TypeError('invalid receiver');
Expand Down
197 changes: 197 additions & 0 deletions polyfill/test/zoneddatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,138 @@ describe('ZonedDateTime', () => {
});
});

describe('date/time maths: hours overflow', () => {
it('subtract result', () => {
const later = ZonedDateTime.from('2019-10-29T10:46:38.271986102-03:00[America/Santiago]');
const earlier = later.subtract({ hours: 12 });
equal(`${earlier}`, '2019-10-28T22:46:38.271986102-03:00[America/Santiago]');
});
it('add result', () => {
const earlier = ZonedDateTime.from('2020-05-31T23:12:38.271986102-04:00[America/Santiago]');
const later = earlier.add({ hours: 2 });
equal(`${later}`, '2020-06-01T01:12:38.271986102-04:00[America/Santiago]');
});
it('symmetrical with regard to negative durations', () => {
equal(
`${ZonedDateTime.from('2019-10-29T10:46:38.271986102-03:00[America/Santiago]').add({ hours: -12 })}`,
'2019-10-28T22:46:38.271986102-03:00[America/Santiago]'
);
equal(
`${ZonedDateTime.from('2020-05-31T23:12:38.271986102-04:00[America/Santiago]').subtract({ hours: -2 })}`,
'2020-06-01T01:12:38.271986102-04:00[America/Santiago]'
);
});
});
describe('ZonedDateTime.add()', () => {
const zdt = ZonedDateTime.from('1969-12-25T12:23:45.678901234+00:00[UTC]');
describe('cross epoch in ms', () => {
const one = zdt.subtract({ hours: 240, nanoseconds: 800 });
const two = zdt.add({ hours: 240, nanoseconds: 800 });
const three = two.subtract({ hours: 480, nanoseconds: 1600 });
const four = one.add({ hours: 480, nanoseconds: 1600 });
it(`(${zdt}).subtract({ hours: 240, nanoseconds: 800 }) = ${one}`, () =>
equal(`${one}`, '1969-12-15T12:23:45.678900434+00:00[UTC]'));
it(`(${zdt}).add({ hours: 240, nanoseconds: 800 }) = ${two}`, () =>
equal(`${two}`, '1970-01-04T12:23:45.678902034+00:00[UTC]'));
it(`(${two}).subtract({ hours: 480, nanoseconds: 1600 }) = ${one}`, () => assert(three.equals(one)));
it(`(${one}).add({ hours: 480, nanoseconds: 1600 }) = ${two}`, () => assert(four.equals(two)));
});
it('zdt.add(durationObj)', () => {
const later = zdt.add(Temporal.Duration.from('PT240H0.000000800S'));
equal(`${later}`, '1970-01-04T12:23:45.678902034+00:00[UTC]');
});
it('casts argument', () => {
equal(`${zdt.add('PT240H0.000000800S')}`, '1970-01-04T12:23:45.678902034+00:00[UTC]');
});
const jan31 = ZonedDateTime.from('2020-01-31T15:00-08:00[America/Vancouver]');
it('constrain when ambiguous result', () => {
equal(`${jan31.add({ months: 1 })}`, '2020-02-29T15:00:00-08:00[America/Vancouver]');
equal(`${jan31.add({ months: 1 }, { overflow: 'constrain' })}`, '2020-02-29T15:00:00-08:00[America/Vancouver]');
});
it('symmetrical with regard to negative durations in the time part', () => {
equal(`${jan31.add({ minutes: -30 })}`, '2020-01-31T14:30:00-08:00[America/Vancouver]');
equal(`${jan31.add({ seconds: -30 })}`, '2020-01-31T14:59:30-08:00[America/Vancouver]');
});
it('throw when ambiguous result with reject', () => {
throws(() => jan31.add({ months: 1 }, { overflow: 'reject' }), RangeError);
});
it('invalid overflow', () => {
['', 'CONSTRAIN', 'balance', 3, null].forEach((overflow) =>
throws(() => zdt.add({ months: 1 }, { overflow }), RangeError)
);
});
it('mixed positive and negative values always throw', () => {
['constrain', 'reject'].forEach((overflow) =>
throws(() => zdt.add({ hours: 1, minutes: -30 }, { overflow }), RangeError)
);
});
it('options may only be an object or undefined', () => {
[null, 1, 'hello', true, Symbol('foo'), 1n].forEach((badOptions) =>
throws(() => zdt.add({ years: 1 }, badOptions), TypeError)
);
[{}, () => {}, undefined].forEach((options) =>
equal(`${zdt.add({ years: 1 }, options)}`, '1970-12-25T12:23:45.678901234+00:00[UTC]')
);
});
it('object must contain at least one correctly-spelled property', () => {
throws(() => zdt.add({}), TypeError);
throws(() => zdt.add({ hour: 12 }), TypeError);
});
it('incorrectly-spelled properties are ignored', () => {
equal(`${zdt.add({ hour: 1, minutes: 1 })}`, '1969-12-25T12:24:45.678901234+00:00[UTC]');
});
});
describe('ZonedDateTime.subtract()', () => {
const zdt = ZonedDateTime.from('1969-12-25T12:23:45.678901234+00:00[UTC]');
it('inst.subtract(durationObj)', () => {
const earlier = zdt.subtract(Temporal.Duration.from('PT240H0.000000800S'));
equal(`${earlier}`, '1969-12-15T12:23:45.678900434+00:00[UTC]');
});
it('casts argument', () => {
equal(`${zdt.subtract('PT240H0.000000800S')}`, '1969-12-15T12:23:45.678900434+00:00[UTC]');
});
const mar31 = ZonedDateTime.from('2020-03-31T15:00-07:00[America/Vancouver]');
it('constrain when ambiguous result', () => {
equal(`${mar31.subtract({ months: 1 })}`, '2020-02-29T15:00:00-08:00[America/Vancouver]');
equal(
`${mar31.subtract({ months: 1 }, { overflow: 'constrain' })}`,
'2020-02-29T15:00:00-08:00[America/Vancouver]'
);
});
it('symmetrical with regard to negative durations in the time part', () => {
equal(`${mar31.subtract({ minutes: -30 })}`, '2020-03-31T15:30:00-07:00[America/Vancouver]');
equal(`${mar31.subtract({ seconds: -30 })}`, '2020-03-31T15:00:30-07:00[America/Vancouver]');
});
it('throw when ambiguous result with reject', () => {
throws(() => mar31.subtract({ months: 1 }, { overflow: 'reject' }), RangeError);
});
it('invalid overflow', () => {
['', 'CONSTRAIN', 'balance', 3, null].forEach((overflow) =>
throws(() => zdt.subtract({ months: 1 }, { overflow }), RangeError)
);
});
it('mixed positive and negative values always throw', () => {
['constrain', 'reject'].forEach((overflow) =>
throws(() => zdt.add({ hours: 1, minutes: -30 }, { overflow }), RangeError)
);
});
it('options may only be an object or undefined', () => {
[null, 1, 'hello', true, Symbol('foo'), 1n].forEach((badOptions) =>
throws(() => zdt.subtract({ years: 1 }, badOptions), TypeError)
);
[{}, () => {}, undefined].forEach((options) =>
equal(`${zdt.subtract({ years: 1 }, options)}`, '1968-12-25T12:23:45.678901234+00:00[UTC]')
);
});
it('object must contain at least one correctly-spelled property', () => {
throws(() => zdt.subtract({}), TypeError);
throws(() => zdt.subtract({ hour: 12 }), TypeError);
});
it('incorrectly-spelled properties are ignored', () => {
equal(`${zdt.subtract({ hour: 1, minutes: 1 })}`, '1969-12-25T12:22:45.678901234+00:00[UTC]');
});
});

describe('ZonedDateTime.round()', () => {
const zdt = ZonedDateTime.from('1976-11-18T15:23:30.123456789+01:00[Europe/Vienna]');
it('throws without parameter', () => {
Expand Down Expand Up @@ -1332,6 +1464,71 @@ 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: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: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), RangeError);
});
it.skip('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:00-08:00[America/Los_Angeles]');
equal(breakoutUnits('subtract', zdt, d, options).toString(), result.toString());
});
it.skip('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: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), RangeError);
});
});

describe('ZonedDateTime.compare()', () => {
const zdt1 = ZonedDateTime.from('1976-11-18T15:23:30.123456789+01:00[Europe/Vienna]');
const zdt2 = ZonedDateTime.from('2019-10-29T10:46:38.271986102+01:00[Europe/Vienna]');
Expand Down
Loading

0 comments on commit 1a3b00d

Please sign in to comment.