Skip to content

Commit

Permalink
Implement rounding in Temporal.Absolute.difference
Browse files Browse the repository at this point in the history
Includes several new abstract operations:

- ToSmallestTemporalDurationUnit, which is distinct from
ToSmallestTemporalUnit because it prefers plural unit names and does have
a fallback value;

- ValidateTemporalDifferenceUnits, which makes sure that largestUnit is
not smaller than smallestUnit;

- and MaximumTemporalDurationRoundingIncrement, which computes the maximum
(not inclusive) value for roundingIncrement given the value of
smallestUnit, for rounding a Temporal.Duration.

All of these will be reused for Temporal.DateTime.difference and
Temporal.Time.difference in subsequent commits.

See: #827
  • Loading branch information
ptomato committed Sep 16, 2020
1 parent 0d0cba4 commit 713d518
Show file tree
Hide file tree
Showing 7 changed files with 437 additions and 9 deletions.
21 changes: 20 additions & 1 deletion docs/absolute.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,10 +302,18 @@ Temporal.now.absolute().minus(oneHour);
- `largestUnit` (string): The largest unit of time to allow in the resulting `Temporal.Duration` object.
Valid values are `'hours'`, `'minutes'`, `'seconds'`, `'milliseconds'`, `'microseconds'`, and `'nanoseconds'`.
The default is `"seconds"`.
- `smallestUnit` (string): The smallest unit of time to round to in the resulting `Temporal.Duration` object.
Valid values are the same as for `largestUnit`.
The default is `'nanoseconds'`, 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 `absolute` and `other`.

This method computes the difference between the two times represented by `absolute` and `other`, and returns it as a `Temporal.Duration` object.
This method computes the difference between the two times represented by `absolute` and `other`, optionally rounds it, and returns it as a `Temporal.Duration` object.
If `other` is later than `absolute` then the resulting duration will be negative.

The `largestUnit` option controls how the resulting duration is expressed.
Expand All @@ -319,6 +327,10 @@ This is because months and years can be different lengths depending on which mon
You cannot determine the start and end date of a difference between `Temporal.Absolute`s, because `Temporal.Absolute` has no time zone or calendar.
In addition, weeks can be different lengths in different calendars, and days can be different lengths when the time zone has a daylight saving transition.

You can round the result using the `smallestUnit`, `roundingIncrement`, and `roundingMode` options.
These behave as in the `Temporal.Duration.round()` method.
The default is to do no rounding.

If you do need to calculate the difference between two `Temporal.Absolute`s in years, months, weeks, or days, then you can make an explicit choice on how to eliminate this ambiguity, choosing your starting point by converting to a `Temporal.DateTime`.
For example, you might decide to base the calculation on your user's current time zone, or on UTC, in the Gregorian calendar.

Expand All @@ -337,6 +349,13 @@ startOfMoonMission.difference(endOfMoonMission, { largestUnit: 'days' });
missionLength.toLocaleString();
// example output: '195 hours 18 minutes 35 seconds'

// Rounding, for example if you don't care about the minutes and seconds
approxMissionLength = endOfMoonMission.difference(startOfMoonMission, {
largestUnit: 'days',
smallestUnit: 'hours'
});
// => P8DT3H

// A billion (10^9) seconds since the epoch in different units
epoch = new Temporal.Absolute(0n);
billion = Temporal.Absolute.fromEpochSeconds(1e9);
Expand Down
3 changes: 3 additions & 0 deletions polyfill/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ export namespace Temporal {
* The default depends on the type being used.
*/
largestUnit: T;
smallestUnit: T;
roundingIncrement: number;
roundingMode: 'ceil' | 'floor' | 'trunc' | 'nearest';
}

export interface RoundOptions<T extends string> {
Expand Down
47 changes: 42 additions & 5 deletions polyfill/lib/absolute.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,53 @@ export class Absolute {
difference(other, options) {
if (!ES.IsTemporalAbsolute(this)) throw new TypeError('invalid receiver');
if (!ES.IsTemporalAbsolute(other)) throw new TypeError('invalid Absolute object');
const largestUnit = ES.ToLargestTemporalUnit(options, 'seconds', ['years', 'months', 'weeks', 'days']);
const disallowedUnits = ['years', 'months', 'weeks', 'days'];
const smallestUnit = ES.ToSmallestTemporalDurationUnit(options, 'nanoseconds', disallowedUnits);
let defaultLargestUnit = 'seconds';
if (smallestUnit === 'hours' || smallestUnit === 'minutes') defaultLargestUnit = smallestUnit;
const largestUnit = ES.ToLargestTemporalUnit(options, defaultLargestUnit, disallowedUnits);
ES.ValidateTemporalDifferenceUnits(largestUnit, smallestUnit);
const roundingMode = ES.ToTemporalRoundingMode(options);
const maximumIncrements = {
hours: 24,
minutes: 60,
seconds: 60,
milliseconds: 1000,
microseconds: 1000,
nanoseconds: 1000
};
const roundingIncrement = ES.ToTemporalRoundingIncrement(options, maximumIncrements[smallestUnit], false);

const onens = GetSlot(other, EPOCHNANOSECONDS);
const twons = GetSlot(this, EPOCHNANOSECONDS);
const diff = twons.minus(onens);

const ns = +diff.mod(1e3);
const us = +diff.divide(1e3).mod(1e3);
const ms = +diff.divide(1e6).mod(1e3);
const ss = +diff.divide(1e9);
let incrementNs = roundingIncrement;
switch (smallestUnit) {
case 'hours':
incrementNs *= 60;
// fall through
case 'minutes':
incrementNs *= 60;
// fall through
case 'seconds':
incrementNs *= 1000;
// fall through
case 'milliseconds':
incrementNs *= 1000;
// fall through
case 'microseconds':
incrementNs *= 1000;
}
const remainder = diff.mod(86400e9);
const wholeDays = diff.minus(remainder);
const roundedRemainder = ES.RoundNumberToIncrement(remainder.toJSNumber(), incrementNs, roundingMode);
const roundedDiff = wholeDays.plus(roundedRemainder);

const ns = +roundedDiff.mod(1e3);
const us = +roundedDiff.divide(1e3).mod(1e3);
const ms = +roundedDiff.divide(1e6).mod(1e3);
const ss = +roundedDiff.divide(1e9);

const Duration = GetIntrinsic('%Temporal.Duration%');
const { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.BalanceDuration(
Expand Down
56 changes: 56 additions & 0 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,62 @@ export const ES = ObjectAssign({}, ES2019, {
}
return value;
},
ToSmallestTemporalDurationUnit: (options, fallback, disallowedStrings = []) => {
const plural = new Map([
['year', 'years'],
['month', 'months'],
['week', 'weeks'],
['day', 'days'],
['hour', 'hours'],
['minute', 'minutes'],
['second', 'seconds'],
['millisecond', 'milliseconds'],
['microsecond', 'microseconds'],
['nanosecond', 'nanoseconds']
]);
const allowed = new Set([
'years',
'months',
'weeks',
'days',
'hours',
'minutes',
'seconds',
'milliseconds',
'microseconds',
'nanoseconds'
]);
for (const s of disallowedStrings) {
allowed.delete(s);
}
const allowedValues = [...allowed];
options = ES.NormalizeOptionsObject(options);
let value = options.smallestUnit;
if (value === undefined) return fallback;
value = ES.ToString(value);
if (plural.has(value)) value = plural.get(value);
if (!allowedValues.includes(value)) {
throw new RangeError(`smallestUnit must be one of ${allowedValues.join(', ')}, not ${value}`);
}
return value;
},
ValidateTemporalDifferenceUnits: (largestUnit, smallestUnit) => {
const validUnits = [
'years',
'months',
'weeks',
'days',
'hours',
'minutes',
'seconds',
'milliseconds',
'microseconds',
'nanoseconds'
];
if (validUnits.indexOf(largestUnit) > validUnits.indexOf(smallestUnit)) {
throw new RangeError(`largestUnit ${largestUnit} cannot be smaller than smallestUnit ${smallestUnit}`);
}
},
ToPartialRecord: (bag, fields) => {
if (!bag || 'object' !== typeof bag) return false;
let any;
Expand Down
Loading

0 comments on commit 713d518

Please sign in to comment.