Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rounding to Date.difference and YearMonth.difference #982

Merged
merged 3 commits into from
Oct 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions docs/date.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,10 +436,18 @@ date.subtract({ months: 1 }, { overflow: 'reject' }); // => throws
- `largestUnit` (optional string): The largest unit of time to allow in the resulting `Temporal.Duration` object.
Valid values are `'auto'`, `'years'`, `'months'`, `'weeks'`, and `'days'`.
The default is `'auto'`.
- `smallestUnit` (string): The smallest unit of time to round to in the resulting `Temporal.Duration` object.
Valid values are `'years'`, `'months'`, `'weeks'`, `'days'`.
The default is `'days'`, i.e., no rounding.
- `roundingIncrement` (number): The granularity to round to, of the unit given by `smallestUnit`.
The default is 1.
- `roundingMode` (string): How to handle the remainder, if rounding.
Valid values are `'ceil'`, `'floor'`, `'trunc'`, and `'nearest'`.
The default is `'nearest'`.

**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.
This method computes the difference between the two dates represented by `date` and `other`, optionally rounds it, and returns it as a `Temporal.Duration` object.
If `other` is later than `date` then the resulting duration will be negative.

The `largestUnit` option controls how the resulting duration is expressed.
Expand All @@ -450,7 +458,13 @@ A value of `'auto'` means `'days'`, unless `smallestUnit` is `'years'`, `'months

By default, the largest unit in the result is days.
This is because months and years can be different lengths depending on which month is meant and whether the year is a leap year.
Unlike other Temporal types, hours and lower are not allowed, because the data model of `Temporal.Date` doesn't have that accuracy.

You can round the result using the `smallestUnit`, `roundingIncrement`, and `roundingMode` options.
These behave as in the `Temporal.Duration.round()` method, but increments of days and larger are allowed.
Since rounding to calendar units requires a reference point, `date` is used as the reference point.
The default is to do no rounding.

Unlike other Temporal types, hours and lower are not allowed for either `largestUnit` or `smallestUnit`, because the data model of `Temporal.Date` doesn't have that accuracy.

Computing the difference between two dates in different calendar systems is not supported.
If you need to do this, choose the calendar in which the computation takes place by converting one of the dates with `date.withCalendar()`.
Expand Down
18 changes: 15 additions & 3 deletions docs/yearmonth.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,11 +314,18 @@ ym.subtract({years: 20, months: 4}) // => 1999-02
- `largestUnit` (string): The largest unit of time to allow in the resulting `Temporal.Duration` object.
Valid values are `'auto'`, `'years'` and `'months'`.
The default is `'auto'`.

- `smallestUnit` (string): The smallest unit of time to round to in the resulting `Temporal.Duration` object.
Valid values are `'years'` and `'months'`.
The default is `'months'`, i.e., no rounding.
- `roundingIncrement` (number): The granularity to round to, of the unit given by `smallestUnit`.
The default is 1.
- `roundingMode` (string): How to handle the remainder, if rounding.
Valid values are `'ceil'`, `'floor'`, `'trunc'`, and `'nearest'`.
The default is `'nearest'`.

**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.
This method computes the difference between the two months represented by `yearMonth` and `other`, optionally rounds it, and returns it as a `Temporal.Duration` object.
If `other` is later than `yearMonth` then the resulting duration will be negative.

The `largestUnit` option controls how the resulting duration is expressed.
Expand All @@ -327,7 +334,12 @@ A difference of one year and two months will become 14 months when `largestUnit`
However, a difference of one month will still be one month even if `largestUnit` is `"years"`.
A value of `'auto'` means `'years'`.

Unlike other Temporal types, days and lower units are not allowed, because the data model of `Temporal.YearMonth` doesn't have that accuracy.
You can round the result using the `smallestUnit`, `roundingIncrement`, and `roundingMode` options.
These behave as in the `Temporal.Duration.round()` method, but increments of months and larger are allowed.
Since rounding to calendar units requires a reference point, the first day of `yearMonth` is used as the reference point.
The default is to do no rounding.

Unlike other Temporal types, weeks and lower are not allowed for either `largestUnit` or `smallestUnit`, because the data model of `Temporal.YearMonth` doesn't have that accuracy.

Computing the difference between two months in different calendar systems is not supported.

Expand Down
47 changes: 46 additions & 1 deletion polyfill/lib/date.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,52 @@ export class Date {
`cannot compute difference between dates of ${calendar.id} and ${otherCalendar.id} calendars`
);
}
return calendar.dateDifference(other, this, options);

options = ES.NormalizeOptionsObject(options);
const disallowedUnits = ['hours', 'minutes', 'seconds', 'milliseconds', 'microseconds', 'nanoseconds'];
const smallestUnit = ES.ToSmallestTemporalDurationUnit(options, 'days', disallowedUnits);
const defaultLargestUnit = ES.LargerOfTwoTemporalDurationUnits('days', smallestUnit);
const largestUnit = ES.ToLargestTemporalUnit(options, defaultLargestUnit, disallowedUnits);
ES.ValidateTemporalUnitRange(largestUnit, smallestUnit);
const roundingMode = ES.ToTemporalRoundingMode(options);
const roundingIncrement = ES.ToTemporalRoundingIncrement(options, undefined, false);

const result = calendar.dateDifference(other, this, { largestUnit });
if (smallestUnit === 'days' && roundingIncrement === 1) return result;

let { years, months, weeks, days } = result;
const TemporalDateTime = GetIntrinsic('%Temporal.DateTime%');
const relativeTo = new TemporalDateTime(
GetSlot(this, ISO_YEAR),
GetSlot(this, ISO_MONTH),
GetSlot(this, ISO_DAY),
0,
0,
0,
0,
0,
0,
GetSlot(this, CALENDAR)
);
({ years, months, weeks, days } = ES.RoundDuration(
years,
months,
weeks,
days,
0,
0,
0,
0,
0,
0,
roundingIncrement,
smallestUnit,
roundingMode,
relativeTo
));

const Duration = GetIntrinsic('%Temporal.Duration%');
return new Duration(years, months, weeks, days, 0, 0, 0, 0, 0, 0);
}
equals(other) {
if (!ES.IsTemporalDate(this)) throw new TypeError('invalid receiver');
Expand Down
5 changes: 1 addition & 4 deletions polyfill/lib/datetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -431,10 +431,7 @@ export class DateTime {
GetSlot(other, ISO_DAY),
calendar
);
let dateLargestUnit = 'days';
if (largestUnit === 'years' || largestUnit === 'months' || largestUnit === 'weeks') {
dateLargestUnit = largestUnit;
}
const dateLargestUnit = ES.LargerOfTwoTemporalDurationUnits('days', largestUnit);
const dateOptions = ObjectAssign({}, options, { largestUnit: dateLargestUnit });
const dateDifference = calendar.dateDifference(otherDate, adjustedDate, dateOptions);

Expand Down
47 changes: 44 additions & 3 deletions polyfill/lib/yearmonth.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export class YearMonth {
);
}
options = ES.NormalizeOptionsObject(options);
const largestUnit = ES.ToLargestTemporalUnit(options, 'years', [
const disallowedUnits = [
'weeks',
'days',
'hours',
Expand All @@ -140,14 +140,55 @@ export class YearMonth {
'milliseconds',
'microseconds',
'nanoseconds'
]);
];
const smallestUnit = ES.ToSmallestTemporalDurationUnit(options, 'months', disallowedUnits);
const largestUnit = ES.ToLargestTemporalUnit(options, 'years', disallowedUnits);
ES.ValidateTemporalUnitRange(largestUnit, smallestUnit);
const roundingMode = ES.ToTemporalRoundingMode(options);
const roundingIncrement = ES.ToTemporalRoundingIncrement(options, undefined, false);

const otherFields = ES.ToTemporalYearMonthRecord(other);
const thisFields = ES.ToTemporalYearMonthRecord(this);
const TemporalDate = GetIntrinsic('%Temporal.Date%');
const otherDate = calendar.dateFromFields({ ...otherFields, day: 1 }, {}, TemporalDate);
const thisDate = calendar.dateFromFields({ ...thisFields, day: 1 }, {}, TemporalDate);
return calendar.dateDifference(otherDate, thisDate, { ...options, largestUnit });

const result = calendar.dateDifference(otherDate, thisDate, { largestUnit });
if (smallestUnit === 'months' && roundingIncrement === 1) return result;

let { years, months } = result;
const TemporalDateTime = GetIntrinsic('%Temporal.DateTime%');
const relativeTo = new TemporalDateTime(
GetSlot(thisDate, ISO_YEAR),
GetSlot(thisDate, ISO_MONTH),
GetSlot(thisDate, ISO_DAY),
0,
0,
0,
0,
0,
0,
calendar
);
({ years, months } = ES.RoundDuration(
years,
months,
0,
0,
0,
0,
0,
0,
0,
0,
roundingIncrement,
smallestUnit,
roundingMode,
relativeTo
));

const Duration = GetIntrinsic('%Temporal.Duration%');
return new Duration(years, months, 0, 0, 0, 0, 0, 0, 0, 0);
}
equals(other) {
if (!ES.IsTemporalYearMonth(this)) throw new TypeError('invalid receiver');
Expand Down
113 changes: 113 additions & 0 deletions polyfill/test/date.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,119 @@ describe('Date', () => {
);
[{}, () => {}, undefined].forEach((options) => equal(`${feb21.difference(feb20, options)}`, 'P366D'));
});
const earlier = Date.from('2019-01-08');
const later = Date.from('2021-09-07');
it('throws on disallowed or invalid smallestUnit', () => {
['era', 'hours', 'minutes', 'seconds', 'milliseconds', 'microseconds', 'nanoseconds', 'nonsense'].forEach(
(smallestUnit) => {
throws(() => later.difference(earlier, { smallestUnit }), RangeError);
}
);
});
it('throws if smallestUnit is larger than largestUnit', () => {
const units = ['years', 'months', 'weeks', 'days'];
for (let largestIdx = 1; largestIdx < units.length; largestIdx++) {
for (let smallestIdx = 0; smallestIdx < largestIdx; smallestIdx++) {
const largestUnit = units[largestIdx];
const smallestUnit = units[smallestIdx];
throws(() => later.difference(earlier, { largestUnit, smallestUnit }), RangeError);
}
}
});
it('assumes a different default for largestUnit if smallestUnit is larger than days', () => {
equal(`${later.difference(earlier, { smallestUnit: 'years' })}`, 'P3Y');
equal(`${later.difference(earlier, { smallestUnit: 'months' })}`, 'P32M');
equal(`${later.difference(earlier, { smallestUnit: 'weeks' })}`, 'P139W');
});
it('throws on invalid roundingMode', () => {
throws(() => later.difference(earlier, { roundingMode: 'cile' }), RangeError);
});
const incrementOneNearest = [
['years', 'P3Y'],
['months', 'P32M'],
['weeks', 'P139W'],
['days', 'P973D']
];
incrementOneNearest.forEach(([smallestUnit, expected]) => {
const roundingMode = 'nearest';
it(`rounds to nearest ${smallestUnit}`, () => {
equal(`${later.difference(earlier, { smallestUnit, roundingMode })}`, expected);
equal(`${earlier.difference(later, { smallestUnit, roundingMode })}`, `-${expected}`);
});
});
const incrementOneCeil = [
['years', 'P3Y', '-P2Y'],
['months', 'P32M', '-P31M'],
['weeks', 'P139W', '-P139W'],
['days', 'P973D', '-P973D']
];
incrementOneCeil.forEach(([smallestUnit, expectedPositive, expectedNegative]) => {
const roundingMode = 'ceil';
it(`rounds up to ${smallestUnit}`, () => {
equal(`${later.difference(earlier, { smallestUnit, roundingMode })}`, expectedPositive);
equal(`${earlier.difference(later, { smallestUnit, roundingMode })}`, expectedNegative);
});
});
const incrementOneFloor = [
['years', 'P2Y', '-P3Y'],
['months', 'P31M', '-P32M'],
['weeks', 'P139W', '-P139W'],
['days', 'P973D', '-P973D']
];
incrementOneFloor.forEach(([smallestUnit, expectedPositive, expectedNegative]) => {
const roundingMode = 'floor';
it(`rounds down to ${smallestUnit}`, () => {
equal(`${later.difference(earlier, { smallestUnit, roundingMode })}`, expectedPositive);
equal(`${earlier.difference(later, { smallestUnit, roundingMode })}`, expectedNegative);
});
});
const incrementOneTrunc = [
['years', 'P2Y'],
['months', 'P31M'],
['weeks', 'P139W'],
['days', 'P973D']
];
incrementOneTrunc.forEach(([smallestUnit, expected]) => {
const roundingMode = 'trunc';
it(`truncates to ${smallestUnit}`, () => {
equal(`${later.difference(earlier, { smallestUnit, roundingMode })}`, expected);
equal(`${earlier.difference(later, { smallestUnit, roundingMode })}`, `-${expected}`);
});
});
it('nearest is the default', () => {
equal(`${later.difference(earlier, { smallestUnit: 'years' })}`, 'P3Y');
equal(`${earlier.difference(later, { smallestUnit: 'years' })}`, '-P3Y');
});
it('rounds to an increment of years', () => {
equal(`${later.difference(earlier, { smallestUnit: 'years', roundingIncrement: 4 })}`, 'P4Y');
});
it('rounds to an increment of months', () => {
equal(`${later.difference(earlier, { smallestUnit: 'months', roundingIncrement: 10 })}`, 'P30M');
});
it('rounds to an increment of weeks', () => {
equal(`${later.difference(earlier, { smallestUnit: 'weeks', roundingIncrement: 12 })}`, 'P144W');
});
it('rounds to an increment of days', () => {
equal(`${later.difference(earlier, { smallestUnit: 'days', roundingIncrement: 100 })}`, 'P1000D');
});
it('accepts singular units', () => {
equal(
`${later.difference(earlier, { smallestUnit: 'year' })}`,
`${later.difference(earlier, { smallestUnit: 'years' })}`
);
equal(
`${later.difference(earlier, { smallestUnit: 'month' })}`,
`${later.difference(earlier, { smallestUnit: 'months' })}`
);
equal(
`${later.difference(earlier, { smallestUnit: 'week' })}`,
`${later.difference(earlier, { smallestUnit: 'weeks' })}`
);
equal(
`${later.difference(earlier, { smallestUnit: 'day' })}`,
`${later.difference(earlier, { smallestUnit: 'days' })}`
);
});
});
describe('date.add() works', () => {
let date = new Date(1976, 11, 18);
Expand Down
Loading