Skip to content

Commit

Permalink
Limit math disambiguation to dateTime durations
Browse files Browse the repository at this point in the history
Absolute plus/minus doesn't use disambiguation, and hybrid duration math
must use 'compatible' disambiguation because RFC 5455 requires it.
Therefore, we'll throw a RangeError if the caller provides a non-default
disambiguation option unless `durationKind: 'dateTime'` is also used.
  • Loading branch information
justingrant committed Oct 21, 2020
1 parent fa36734 commit 4fc4300
Show file tree
Hide file tree
Showing 4 changed files with 38 additions and 26 deletions.
10 changes: 8 additions & 2 deletions polyfill/lib/localdatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,12 @@ function doPlusOrMinus(op, durationLike, options, localDateTime) {
const dateTimeOverflowOption = { disambiguation: overflow };
const { timeZone, calendar } = localDateTime;

// Absolute doesn't use disambiguation, while RFC 5455 specifies 'compatible' behavior
// for disambiguation. Therefore, only 'dateTime' durations can use this option.
if (disambiguation !== 'compatible' && durationKind !== 'dateTime') {
throw new RangeError('Arithmetic disambiguation is only allowed for `dateTime` durations');
}

switch (durationKind) {
case 'absolute': {
const result = localDateTime.absolute[op](durationLike);
Expand Down Expand Up @@ -239,14 +245,14 @@ function doPlusOrMinus(op, durationLike, options, localDateTime) {
if (weeks) newDateTime = newDateTime[op]({ weeks }, dateTimeOverflowOption);
if (days) newDateTime = newDateTime[op]({ days }, dateTimeOverflowOption);
if (isZeroDuration(timeDuration)) {
const absolute = newDateTime.inTimeZone(timeZone, { disambiguation });
const absolute = newDateTime.inTimeZone(timeZone);
return LocalDateTime.from({ absolute, timeZone, calendar: localDateTime.calendar });
} else {
// Now add/subtract the time. Because all time units are always the same
// length, we can add/subtract all of them together without worrying about
// order of operations.
newDateTime = newDateTime[op](timeDuration, dateTimeOverflowOption);
let absolute = newDateTime.inTimeZone(timeZone, { disambiguation });
let absolute = newDateTime.inTimeZone(timeZone);
const reverseOp = op === 'plus' ? 'minus' : 'plus';
const backUpAbs = absolute[reverseOp]({ nanoseconds: totalNanoseconds(timeDuration) });
const backUpOffset = timeZone.getOffsetNanosecondsFor(backUpAbs);
Expand Down
22 changes: 11 additions & 11 deletions polyfill/lib/poc/LocalDateTime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,26 +526,26 @@ describe('LocalDateTime', () => {
const dayBeforeDstStart = LocalDateTime.from({ ...new DateTime(2020, 3, 7, 2, 30).getFields(), timeZone: tz });

describe('math around DST', () => {
it('add 1 hour to get to DST start (earlier)', () => {
const added = hourBeforeDstStart.plus({ hours: 1 }, { disambiguation: 'earlier' });
it('add 1 hour to get to DST start', () => {
const added = hourBeforeDstStart.plus({ hours: 1 });
equal(added.hour, 3);
const diff = added.difference(hourBeforeDstStart, { largestUnit: 'hours' });
equal(diff.days, 0);
equal(diff.hours, 1);
equal(diff.minutes, 0);
});

it('add 2 hours to get to DST start +1 (earlier)', () => {
const added = hourBeforeDstStart.plus({ hours: 2 }, { disambiguation: 'earlier' });
it('add 2 hours to get to DST start +1', () => {
const added = hourBeforeDstStart.plus({ hours: 2 });
equal(added.hour, 4);
const diff = added.difference(hourBeforeDstStart, { largestUnit: 'hours' });
equal(diff.days, 0);
equal(diff.hours, 2);
equal(diff.minutes, 0);
});

it('add 1.5 hours to get to 0.5 hours after DST start (earlier)', () => {
const added = hourBeforeDstStart.plus({ hours: 1, minutes: 30 }, { disambiguation: 'earlier' });
it('add 1.5 hours to get to 0.5 hours after DST start', () => {
const added = hourBeforeDstStart.plus({ hours: 1, minutes: 30 });
equal(added.hour, 3);
equal(added.minute, 30);
const diff = added.difference(hourBeforeDstStart, { largestUnit: 'hours' });
Expand All @@ -554,10 +554,10 @@ describe('LocalDateTime', () => {
equal(diff.minutes, 30);
});

it('Samoa date line change: 10:00PM 29 Dec 2011 -> 11:00PM 31 Dec 2011 (hybrid)', () => {
it('Samoa date line change: 10:00PM 29 Dec 2011 -> 11:00PM 31 Dec 2011', () => {
const dayBeforeSamoaDateLineChangeAbs = new Temporal.DateTime(2011, 12, 29, 22).inTimeZone('Pacific/Apia');
const start = LocalDateTime.from({ absolute: dayBeforeSamoaDateLineChangeAbs, timeZone: 'Pacific/Apia' });
const added = start.plus({ days: 1, hours: 1 }, { disambiguation: 'later' });
const added = start.plus({ days: 1, hours: 1 });
equal(added.day, 31);
equal(added.hour, 23);
equal(added.minute, 0);
Expand All @@ -580,7 +580,7 @@ describe('LocalDateTime', () => {
});

it('2:30 day before DST start -> 3:30 day of DST start', () => {
const added = dayBeforeDstStart.plus({ days: 1 }, { disambiguation: 'later' });
const added = dayBeforeDstStart.plus({ days: 1 });
equal(added.day, 8);
equal(added.hour, 3);
equal(added.minute, 30);
Expand All @@ -604,7 +604,7 @@ describe('LocalDateTime', () => {

it('2:00 day before DST starts -> 3:00 day DST starts', () => {
const start = hourBeforeDstStart.minus({ days: 1 }).plus({ hours: 1 }); // 2:00AM
const added = start.plus({ days: 1 }, { disambiguation: 'later' });
const added = start.plus({ days: 1 });
equal(added.day, 8);
equal(added.hour, 3);
equal(added.minute, 0);
Expand All @@ -616,7 +616,7 @@ describe('LocalDateTime', () => {

it('2:00 day before DST starts -> 3:00 day DST starts', () => {
const start = hourBeforeDstStart.minus({ days: 1 }).plus({ hours: 1 }); // 2:00AM
const added = start.plus({ days: 1 }, { disambiguation: 'later' });
const added = start.plus({ days: 1 });
equal(added.day, 8);
equal(added.hour, 3);
equal(added.minute, 0);
Expand Down
10 changes: 8 additions & 2 deletions polyfill/lib/poc/LocalDateTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,12 @@ function doPlusOrMinus(
const dateTimeOverflowOption = { disambiguation: overflow };
const { timeZone, calendar } = localDateTime;

// Absolute doesn't use disambiguation, while RFC 5455 specifies 'compatible' behavior
// for disambiguation. Therefore, only 'dateTime' durations can use this option.
if (disambiguation !== 'compatible' && durationKind !== 'dateTime') {
throw new RangeError('Disambiguation options are only valid for `dateTime` durations');
}

switch (durationKind) {
case 'absolute': {
const result = localDateTime.absolute[op](durationLike);
Expand Down Expand Up @@ -362,14 +368,14 @@ function doPlusOrMinus(
if (weeks) newDateTime = newDateTime[op]({ weeks }, dateTimeOverflowOption);
if (days) newDateTime = newDateTime[op]({ days }, dateTimeOverflowOption);
if (isZeroDuration(timeDuration)) {
const absolute = newDateTime.inTimeZone(timeZone, { disambiguation });
const absolute = newDateTime.inTimeZone(timeZone);
return LocalDateTime.from({ absolute, timeZone, calendar: localDateTime.calendar });
} else {
// Now add/subtract the time. Because all time units are always the same
// length, we can add/subtract all of them together without worrying about
// order of operations.
newDateTime = newDateTime[op](timeDuration, dateTimeOverflowOption);
let absolute = newDateTime.inTimeZone(timeZone, { disambiguation });
let absolute = newDateTime.inTimeZone(timeZone);
const reverseOp = op === 'plus' ? 'minus' : 'plus';
const backUpAbs = absolute[reverseOp]({ nanoseconds: totalNanoseconds(timeDuration) });
const backUpOffset = timeZone.getOffsetNanosecondsFor(backUpAbs);
Expand Down
22 changes: 11 additions & 11 deletions polyfill/test/localdatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -492,26 +492,26 @@ describe('LocalDateTime', () => {
const dayBeforeDstStart = LocalDateTime.from({ ...new DateTime(2020, 3, 7, 2, 30).getFields(), timeZone: tz });

describe('math around DST', () => {
it('add 1 hour to get to DST start (earlier)', () => {
const added = hourBeforeDstStart.plus({ hours: 1 }, { disambiguation: 'earlier' });
it('add 1 hour to get to DST start', () => {
const added = hourBeforeDstStart.plus({ hours: 1 });
equal(added.hour, 3);
const diff = added.difference(hourBeforeDstStart, { largestUnit: 'hours' });
equal(diff.days, 0);
equal(diff.hours, 1);
equal(diff.minutes, 0);
});

it('add 2 hours to get to DST start +1 (earlier)', () => {
const added = hourBeforeDstStart.plus({ hours: 2 }, { disambiguation: 'earlier' });
it('add 2 hours to get to DST start +1', () => {
const added = hourBeforeDstStart.plus({ hours: 2 });
equal(added.hour, 4);
const diff = added.difference(hourBeforeDstStart, { largestUnit: 'hours' });
equal(diff.days, 0);
equal(diff.hours, 2);
equal(diff.minutes, 0);
});

it('add 1.5 hours to get to 0.5 hours after DST start (earlier)', () => {
const added = hourBeforeDstStart.plus({ hours: 1, minutes: 30 }, { disambiguation: 'earlier' });
it('add 1.5 hours to get to 0.5 hours after DST start', () => {
const added = hourBeforeDstStart.plus({ hours: 1, minutes: 30 });
equal(added.hour, 3);
equal(added.minute, 30);
const diff = added.difference(hourBeforeDstStart, { largestUnit: 'hours' });
Expand All @@ -520,10 +520,10 @@ describe('LocalDateTime', () => {
equal(diff.minutes, 30);
});

it('Samoa date line change: 10:00PM 29 Dec 2011 -> 11:00PM 31 Dec 2011 (hybrid)', () => {
it('Samoa date line change: 10:00PM 29 Dec 2011 -> 11:00PM 31 Dec 2011', () => {
const dayBeforeSamoaDateLineChangeAbs = new Temporal.DateTime(2011, 12, 29, 22).inTimeZone('Pacific/Apia');
const start = LocalDateTime.from({ absolute: dayBeforeSamoaDateLineChangeAbs, timeZone: 'Pacific/Apia' });
const added = start.plus({ days: 1, hours: 1 }, { disambiguation: 'later' });
const added = start.plus({ days: 1, hours: 1 });
equal(added.day, 31);
equal(added.hour, 23);
equal(added.minute, 0);
Expand All @@ -546,7 +546,7 @@ describe('LocalDateTime', () => {
});

it('2:30 day before DST start -> 3:30 day of DST start', () => {
const added = dayBeforeDstStart.plus({ days: 1 }, { disambiguation: 'later' });
const added = dayBeforeDstStart.plus({ days: 1 });
equal(added.day, 8);
equal(added.hour, 3);
equal(added.minute, 30);
Expand All @@ -570,7 +570,7 @@ describe('LocalDateTime', () => {

it('2:00 day before DST starts -> 3:00 day DST starts', () => {
const start = hourBeforeDstStart.minus({ days: 1 }).plus({ hours: 1 }); // 2:00AM
const added = start.plus({ days: 1 }, { disambiguation: 'later' });
const added = start.plus({ days: 1 });
equal(added.day, 8);
equal(added.hour, 3);
equal(added.minute, 0);
Expand All @@ -582,7 +582,7 @@ describe('LocalDateTime', () => {

it('2:00 day before DST starts -> 3:00 day DST starts', () => {
const start = hourBeforeDstStart.minus({ days: 1 }).plus({ hours: 1 }); // 2:00AM
const added = start.plus({ days: 1 }, { disambiguation: 'later' });
const added = start.plus({ days: 1 });
equal(added.day, 8);
equal(added.hour, 3);
equal(added.minute, 0);
Expand Down

0 comments on commit 4fc4300

Please sign in to comment.