Skip to content

Commit

Permalink
polyfill: durations accept fractional time values (#1179)
Browse files Browse the repository at this point in the history
* polyfill: durations accept fractional time values

Fixes: tc39/proposal-temporal#938

* fixup! polyfill: durations accept fractional time values

* fixup! polyfill: durations accept fractional time values

* spec: add spec for handling fractional durations

* Code review suggestions

- Fix order-of-operations tests (which used 1.7 as a value for durations
  and relied upon it being truncated to 1, which we probably don't want
  regardless of the state of this)
- Remove Duration.from subclass-invalid-arg test, which is no longer
  valid; the subclass constructor is never called with a non-float
  argument
- Fix ecmarkup linter errors

Co-authored-by: Philip Chimento <pchimento@igalia.com>
  • Loading branch information
ryzokuken and ptomato committed May 6, 2021
1 parent 426ea4a commit eee56fc
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 27 deletions.
156 changes: 135 additions & 21 deletions lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const ObjectCreate = Object.create;
import bigInt from 'big-integer';
import Call from 'es-abstract/2020/Call.js';
import SpeciesConstructor from 'es-abstract/2020/SpeciesConstructor.js';
import IsInteger from 'es-abstract/2020/IsInteger.js';
import ToInteger from 'es-abstract/2020/ToInteger.js';
import ToLength from 'es-abstract/2020/ToLength.js';
import ToNumber from 'es-abstract/2020/ToNumber.js';
Expand Down Expand Up @@ -88,6 +89,7 @@ import * as PARSE from './regex.mjs';
const ES2020 = {
Call,
SpeciesConstructor,
IsInteger,
ToInteger,
ToLength,
ToNumber,
Expand Down Expand Up @@ -289,12 +291,31 @@ export const ES = ObjectAssign({}, ES2020, {
const weeks = ES.ToInteger(match[4]) * sign;
const days = ES.ToInteger(match[5]) * sign;
const hours = ES.ToInteger(match[6]) * sign;
const minutes = ES.ToInteger(match[7]) * sign;
const seconds = ES.ToInteger(match[8]) * sign;
const fraction = match[9] + '000000000';
const milliseconds = ES.ToInteger(fraction.slice(0, 3)) * sign;
const microseconds = ES.ToInteger(fraction.slice(3, 6)) * sign;
const nanoseconds = ES.ToInteger(fraction.slice(6, 9)) * sign;
let fHours = match[7];
let minutes = ES.ToInteger(match[8]) * sign;
let fMinutes = match[9];
let seconds = ES.ToInteger(match[10]) * sign;
let fSeconds = match[11] + '000000000';
let milliseconds = ES.ToInteger(fSeconds.slice(0, 3)) * sign;
let microseconds = ES.ToInteger(fSeconds.slice(3, 6)) * sign;
let nanoseconds = ES.ToInteger(fSeconds.slice(6, 9)) * sign;

fHours = fHours ? (sign * ES.ToInteger(fHours)) / 10 ** fHours.length : 0;
fMinutes = fMinutes ? (sign * ES.ToInteger(fMinutes)) / 10 ** fMinutes.length : 0;

({ minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.DurationHandleFractions(
fHours,
minutes,
fMinutes,
seconds,
0,
milliseconds,
0,
microseconds,
0,
nanoseconds,
0
));
return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds };
},
ParseTemporalInstant: (isoString) => {
Expand Down Expand Up @@ -391,6 +412,78 @@ export const ES = ObjectAssign({}, ES2020, {
}
return { month, day };
},
DurationHandleFractions: (
fHours,
minutes,
fMinutes,
seconds,
fSeconds,
milliseconds,
fMilliseconds,
microseconds,
fMicroseconds,
nanoseconds,
fNanoseconds
) => {
if (fHours !== 0) {
[
minutes,
fMinutes,
seconds,
fSeconds,
milliseconds,
fMilliseconds,
microseconds,
fMicroseconds,
nanoseconds,
fNanoseconds
].forEach((val) => {
if (val !== 0) throw new RangeError('only the smallest unit can be fractional');
});
let mins = fHours * 60;
minutes = MathTrunc(mins);
fMinutes = mins % 1;
}

if (fMinutes !== 0) {
[seconds, fSeconds, milliseconds, fMilliseconds, microseconds, fMicroseconds, nanoseconds, fNanoseconds].forEach(
(val) => {
if (val !== 0) throw new RangeError('only the smallest unit can be fractional');
}
);
let secs = fMinutes * 60;
seconds = MathTrunc(secs);
fSeconds = secs % 1;
}

if (fSeconds !== 0) {
[milliseconds, fMilliseconds, microseconds, fMicroseconds, nanoseconds, fNanoseconds].forEach((val) => {
if (val !== 0) throw new RangeError('only the smallest unit can be fractional');
});
let mils = fSeconds * 1000;
milliseconds = MathTrunc(mils);
fMilliseconds = mils % 1;
}

if (fMilliseconds !== 0) {
[microseconds, fMicroseconds, nanoseconds, fNanoseconds].forEach((val) => {
if (val !== 0) throw new RangeError('only the smallest unit can be fractional');
});
let mics = fMilliseconds * 1000;
microseconds = MathTrunc(mics);
fMicroseconds = mics % 1;
}

if (fMicroseconds !== 0) {
[nanoseconds, fNanoseconds].forEach((val) => {
if (val !== 0) throw new RangeError('only the smallest unit can be fractional');
});
let nans = fMicroseconds * 1000;
nanoseconds = MathTrunc(nans);
}

return { minutes, seconds, milliseconds, microseconds, nanoseconds };
},
ToTemporalDurationRecord: (item) => {
if (ES.IsTemporalDuration(item)) {
return {
Expand All @@ -406,20 +499,24 @@ export const ES = ObjectAssign({}, ES2020, {
nanoseconds: GetSlot(item, NANOSECONDS)
};
}
const props = ES.ToPartialRecord(item, [
'days',
'hours',
'microseconds',
'milliseconds',
'minutes',
'months',
'nanoseconds',
'seconds',
'weeks',
'years'
]);
const props = ES.ToPartialRecord(
item,
[
'days',
'hours',
'microseconds',
'milliseconds',
'minutes',
'months',
'nanoseconds',
'seconds',
'weeks',
'years'
],
ES.ToNumber
);
if (!props) throw new TypeError('invalid duration-like');
const {
let {
years = 0,
months = 0,
weeks = 0,
Expand All @@ -431,6 +528,23 @@ export const ES = ObjectAssign({}, ES2020, {
microseconds = 0,
nanoseconds = 0
} = props;
if (!ES.IsInteger(years) || !ES.IsInteger(months) || !ES.IsInteger(weeks) || !ES.IsInteger(days)) {
throw new RangeError('non-time units cannot be fractional');
}
({ minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.DurationHandleFractions(
hours % 1,
MathTrunc(minutes),
minutes % 1,
MathTrunc(seconds),
seconds % 1,
MathTrunc(milliseconds),
milliseconds % 1,
MathTrunc(microseconds),
microseconds % 1,
MathTrunc(nanoseconds),
nanoseconds % 1
));
hours = MathTrunc(hours);
return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds };
},
ToLimitedTemporalDuration: (item, disallowedProperties = []) => {
Expand Down Expand Up @@ -797,15 +911,15 @@ export const ES = ObjectAssign({}, ES2020, {
if (validUnits.indexOf(unit1) > validUnits.indexOf(unit2)) return unit2;
return unit1;
},
ToPartialRecord: (bag, fields) => {
ToPartialRecord: (bag, fields, cast = ES.ToInteger) => {
if (ES.Type(bag) !== 'Object') return false;
let any;
for (const property of fields) {
const value = bag[property];
if (value !== undefined) {
any = any || {};
if (BUILTIN_FIELDS.has(property)) {
any[property] = ES.ToInteger(value);
any[property] = cast(value);
} else {
any[property] = value;
}
Expand Down
6 changes: 5 additions & 1 deletion lib/regex.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@ export const time = new RegExp(`^${timesplit.source}(?:${zonesplit.source})?(?:$
export const yearmonth = new RegExp(`^(${yearpart.source})-?(\\d{2})$`);
export const monthday = /^(?:--)?(\d{2})-?(\d{2})$/;

export const duration = /^([+\u2212-])?P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?!$)(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)(?:[.,](\d{1,9}))?S)?)?$/i;
const fraction = /(\d+)(?:[.,](\d{1,9}))?/;

const durationDate = /(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?/;
const durationTime = new RegExp(`(?:${fraction.source}H)?(?:${fraction.source}M)?(?:${fraction.source}S)?`);
export const duration = new RegExp(`^([+\u2212-])?P${durationDate.source}(?:T(?!$)${durationTime.source})?$`, 'i');
28 changes: 23 additions & 5 deletions test/duration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ describe('Duration', () => {
equal(`${Duration.from({ milliseconds: 5 })}`, 'PT0.005S'));
it('Duration.from("P1D") == P1D', () => equal(`${Duration.from('P1D')}`, 'P1D'));
it('lowercase variant', () => equal(`${Duration.from('p1y1m1dt1h1m1s')}`, 'P1Y1M1DT1H1M1S'));
it('any number of decimal places works', () => {
it('upto nine decimal places work', () => {
equal(`${Duration.from('P1Y1M1W1DT1H1M1.1S')}`, 'P1Y1M1W1DT1H1M1.100S');
equal(`${Duration.from('P1Y1M1W1DT1H1M1.12S')}`, 'P1Y1M1W1DT1H1M1.120S');
equal(`${Duration.from('P1Y1M1W1DT1H1M1.123S')}`, 'P1Y1M1W1DT1H1M1.123S');
Expand All @@ -143,20 +143,38 @@ describe('Duration', () => {
equal(`${Duration.from('P1Y1M1W1DT1H1M1.12345678S')}`, 'P1Y1M1W1DT1H1M1.123456780S');
equal(`${Duration.from('P1Y1M1W1DT1H1M1.123456789S')}`, 'P1Y1M1W1DT1H1M1.123456789S');
});
it('above nine decimal places throw', () => {
throws(() => Duration.from('P1Y1M1W1DT1H1M1.123456789123S'), RangeError);
});
it('variant decimal separator', () => {
equal(`${Duration.from('P1Y1M1W1DT1H1M1,12S')}`, 'P1Y1M1W1DT1H1M1.120S');
});
it('decimal places only allowed in seconds', () => {
it('decimal places only allowed in time units', () => {
[
'P0.5Y',
'P1Y0,5M',
'P1Y1M0.5W',
'P1Y1M1W0,5D',
'P1Y1M1W1DT0.5H',
'P1Y1M1W1DT1H0,5M',
'P1Y1M1W1DT1H0.5M0.5S'
{ years: 0.5 },
{ months: 0.5 },
{ weeks: 0.5 },
{ days: 0.5 }
].forEach((str) => throws(() => Duration.from(str), RangeError));
});
it('decimal places only allowed in last non-zero unit', () => {
[
'P1Y1M1W1DT0.5H5S',
'P1Y1M1W1DT1.5H0,5M',
'P1Y1M1W1DT1H0.5M0.5S',
{ hours: 0.5, minutes: 20 },
{ hours: 0.5, seconds: 15 },
{ minutes: 10.7, nanoseconds: 400 }
].forEach((str) => throws(() => Duration.from(str), RangeError));
});
it('decimal places are properly handled on valid units', () => {
equal(`${Duration.from('P1DT0.5M')}`, 'P1DT30S');
equal(`${Duration.from('P1DT0,5H')}`, 'P1DT30M');
});
it('"P" by itself is not a valid string', () => {
['P', 'PT', '-P', '-PT', '+P', '+PT'].forEach((s) => throws(() => Duration.from(s), RangeError));
});
Expand Down

0 comments on commit eee56fc

Please # to comment.