Skip to content

Commit 068e801

Browse files
committed
Allow string param for round and total
Port of tc39/proposal-temporal#1875 I added two tests to expected-failures.txt that can be removed when tc39/test262#3304 lands.
1 parent 766e503 commit 068e801

File tree

13 files changed

+416
-226
lines changed

13 files changed

+416
-226
lines changed

index.d.ts

Lines changed: 224 additions & 160 deletions
Large diffs are not rendered by default.

lib/duration.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,10 @@ export class Duration implements Temporal.Duration {
292292
microseconds,
293293
nanoseconds
294294
);
295-
const options = ES.GetOptionsObject(optionsParam);
295+
const options =
296+
typeof optionsParam === 'string'
297+
? (ES.CreateOnePropObject('smallestUnit', optionsParam) as Exclude<typeof optionsParam, string>)
298+
: ES.GetOptionsObject(optionsParam);
296299
let smallestUnit = ES.ToSmallestTemporalUnit(options, undefined);
297300
let smallestUnitPresent = true;
298301
if (!smallestUnit) {
@@ -389,7 +392,10 @@ export class Duration implements Temporal.Duration {
389392
let nanoseconds = GetSlot(this, NANOSECONDS);
390393

391394
if (optionsParam === undefined) throw new TypeError('options argument is required');
392-
const options = ES.GetOptionsObject(optionsParam);
395+
const options =
396+
typeof optionsParam === 'string'
397+
? (ES.CreateOnePropObject('unit', optionsParam) as Exclude<typeof optionsParam, string>)
398+
: ES.GetOptionsObject(optionsParam);
393399
const unit = ES.ToTemporalDurationTotalUnit(options);
394400
if (unit === undefined) throw new RangeError('unit option is required');
395401
const relativeTo = ES.ToRelativeTemporalObject(options);

lib/ecmascript.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -837,7 +837,7 @@ export function ToSecondsStringPrecision(options: Temporal.ToStringPrecisionOpti
837837
}
838838

839839
export function ToLargestTemporalUnit<Allowed extends Temporal.DateTimeUnit, Disallowed extends Temporal.DateTimeUnit>(
840-
options: { largestUnit?: Temporal.LargestUnitOption<Allowed> },
840+
options: { largestUnit?: Temporal.LargestUnit<Allowed> },
841841
fallback: Allowed | 'auto',
842842
disallowedStrings?: ReadonlyArray<Disallowed>
843843
): Allowed | 'auto';
@@ -846,7 +846,7 @@ export function ToLargestTemporalUnit<
846846
Disallowed extends Temporal.DateTimeUnit,
847847
IfAuto extends Allowed | undefined = Allowed
848848
>(
849-
options: { largestUnit?: Temporal.LargestUnitOption<Allowed> },
849+
options: { largestUnit?: Temporal.LargestUnit<Allowed> },
850850
fallback: Allowed | 'auto',
851851
disallowedStrings: ReadonlyArray<Disallowed>,
852852
autoValue?: IfAuto
@@ -856,7 +856,7 @@ export function ToLargestTemporalUnit<
856856
Disallowed extends Temporal.DateTimeUnit,
857857
IfAuto extends Allowed | undefined = Allowed
858858
>(
859-
options: { largestUnit?: Temporal.LargestUnitOption<Allowed> },
859+
options: { largestUnit?: Temporal.LargestUnit<Allowed> },
860860
fallback: Allowed | 'auto',
861861
disallowedStrings: ReadonlyArray<Disallowed> = [],
862862
autoValue?: IfAuto
@@ -882,7 +882,7 @@ export function ToSmallestTemporalUnit<
882882
Fallback extends Allowed,
883883
Disallowed extends Temporal.DateTimeUnit
884884
>(
885-
options: { smallestUnit?: Temporal.SmallestUnitOption<Allowed> },
885+
options: { smallestUnit?: Temporal.SmallestUnit<Allowed> },
886886
fallback: Fallback,
887887
disallowedStrings: ReadonlyArray<Disallowed> = []
888888
): Allowed {
@@ -5065,6 +5065,12 @@ export function GetOptionsObject<T>(options: T) {
50655065
throw new TypeError(`Options parameter must be an object, not ${options === null ? 'null' : `${typeof options}`}`);
50665066
}
50675067

5068+
export function CreateOnePropObject<K extends string, V extends unknown>(propName: K, propValue: V): { [k in K]: V } {
5069+
const o = ObjectCreate(null);
5070+
o[propName] = propValue;
5071+
return o;
5072+
}
5073+
50685074
function GetOption<P extends string, O extends Partial<Record<P, unknown>>>(
50695075
options: O,
50705076
property: P,

lib/instant.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,10 @@ export class Instant implements Temporal.Instant {
165165
round(optionsParam: Params['round'][0]): Return['round'] {
166166
if (!ES.IsTemporalInstant(this)) throw new TypeError('invalid receiver');
167167
if (optionsParam === undefined) throw new TypeError('options parameter is required');
168-
const options = ES.GetOptionsObject(optionsParam);
168+
const options =
169+
typeof optionsParam === 'string'
170+
? (ES.CreateOnePropObject('smallestUnit', optionsParam) as Exclude<typeof optionsParam, string>)
171+
: ES.GetOptionsObject(optionsParam);
169172
const smallestUnit = ES.ToSmallestTemporalUnit(options, undefined, DISALLOWED_UNITS);
170173
if (smallestUnit === undefined) throw new RangeError('smallestUnit is required');
171174
const roundingMode = ES.ToTemporalRoundingMode(options, 'halfExpand');

lib/plaindatetime.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,10 @@ export class PlainDateTime implements Temporal.PlainDateTime {
518518
round(optionsParam: Params['round'][0]): Return['round'] {
519519
if (!ES.IsTemporalDateTime(this)) throw new TypeError('invalid receiver');
520520
if (optionsParam === undefined) throw new TypeError('options parameter is required');
521-
const options = ES.GetOptionsObject(optionsParam);
521+
const options =
522+
typeof optionsParam === 'string'
523+
? (ES.CreateOnePropObject('smallestUnit', optionsParam) as Exclude<typeof optionsParam, string>)
524+
: ES.GetOptionsObject(optionsParam);
522525
const smallestUnit = ES.ToSmallestTemporalUnit(options, undefined, ['year', 'month', 'week']);
523526
if (smallestUnit === undefined) throw new RangeError('smallestUnit is required');
524527
const roundingMode = ES.ToTemporalRoundingMode(options, 'halfExpand');

lib/plaintime.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,10 @@ export class PlainTime implements Temporal.PlainTime {
354354
round(optionsParam: Params['round'][0]): Return['round'] {
355355
if (!ES.IsTemporalTime(this)) throw new TypeError('invalid receiver');
356356
if (optionsParam === undefined) throw new TypeError('options parameter is required');
357-
const options = ES.GetOptionsObject(optionsParam);
357+
const options =
358+
typeof optionsParam === 'string'
359+
? (ES.CreateOnePropObject('smallestUnit', optionsParam) as Exclude<typeof optionsParam, string>)
360+
: ES.GetOptionsObject(optionsParam);
358361
const smallestUnit = ES.ToSmallestTemporalUnit(options, undefined, DISALLOWED_UNITS);
359362
if (smallestUnit === undefined) throw new RangeError('smallestUnit is required');
360363
const roundingMode = ES.ToTemporalRoundingMode(options, 'halfExpand');

lib/zoneddatetime.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,10 @@ export class ZonedDateTime implements Temporal.ZonedDateTime {
586586
round(optionsParam: Params['round'][0]): Return['round'] {
587587
if (!ES.IsTemporalZonedDateTime(this)) throw new TypeError('invalid receiver');
588588
if (optionsParam === undefined) throw new TypeError('options parameter is required');
589-
const options = ES.GetOptionsObject(optionsParam);
589+
const options =
590+
typeof optionsParam === 'string'
591+
? (ES.CreateOnePropObject('smallestUnit', optionsParam) as Exclude<typeof optionsParam, string>)
592+
: ES.GetOptionsObject(optionsParam);
590593
const smallestUnit = ES.ToSmallestTemporalUnit(options, undefined, ['year', 'month', 'week']);
591594
if (smallestUnit === undefined) throw new RangeError('smallestUnit is required');
592595
const roundingMode = ES.ToTemporalRoundingMode(options, 'halfExpand');

test/duration.mjs

Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -965,8 +965,8 @@ describe('Duration', () => {
965965
const d = new Duration(5, 5, 5, 5, 5, 5, 5, 5, 5, 5);
966966
const d2 = new Duration(0, 0, 0, 5, 5, 5, 5, 5, 5, 5);
967967
const relativeTo = Temporal.PlainDateTime.from('2020-01-01T00:00');
968-
it('options may only be an object', () => {
969-
[null, 1, 'hello', true, Symbol('foo'), 1n].forEach((badOptions) => throws(() => d.round(badOptions), TypeError));
968+
it('parameter may only be an object or string', () => {
969+
[null, 1, true, Symbol('foo'), 1n].forEach((badOptions) => throws(() => d.round(badOptions), TypeError));
970970
});
971971
it('throws without parameter', () => {
972972
throws(() => d.round(), TypeError);
@@ -977,11 +977,16 @@ describe('Duration', () => {
977977
it("succeeds with largestUnit: 'auto'", () => {
978978
equal(`${Duration.from({ hours: 25 }).round({ largestUnit: 'auto' })}`, 'PT25H');
979979
});
980-
it('throws on disallowed or invalid smallestUnit', () => {
980+
it('throws on disallowed or invalid smallestUnit (object param)', () => {
981981
['era', 'nonsense'].forEach((smallestUnit) => {
982982
throws(() => d.round({ smallestUnit }), RangeError);
983983
});
984984
});
985+
it('throws on disallowed or invalid smallestUnit (string param)', () => {
986+
['era', 'nonsense'].forEach((smallestUnit) => {
987+
throws(() => d.round(smallestUnit), RangeError);
988+
});
989+
});
985990
it('throws if smallestUnit is larger than largestUnit', () => {
986991
const units = [
987992
'years',
@@ -1003,6 +1008,24 @@ describe('Duration', () => {
10031008
}
10041009
}
10051010
});
1011+
it('accepts string parameter as a shortcut for {smallestUnit}', () => {
1012+
const d = Temporal.Duration.from({
1013+
days: 1,
1014+
hours: 2,
1015+
minutes: 3,
1016+
seconds: 4,
1017+
milliseconds: 5,
1018+
microseconds: 6,
1019+
nanoseconds: 7
1020+
});
1021+
equal(d.round('day').toString(), 'P1D');
1022+
equal(d.round('hour').toString(), 'P1DT2H');
1023+
equal(d.round('minute').toString(), 'P1DT2H3M');
1024+
equal(d.round('second').toString(), 'P1DT2H3M4S');
1025+
equal(d.round('millisecond').toString(), 'P1DT2H3M4.005S');
1026+
equal(d.round('microsecond').toString(), 'P1DT2H3M4.005006S');
1027+
equal(d.round('nanosecond').toString(), 'P1DT2H3M4.005006007S');
1028+
});
10061029
it('assumes a different default for largestUnit if smallestUnit is larger than the default', () => {
10071030
const almostYear = Duration.from({ days: 364 });
10081031
equal(`${almostYear.round({ smallestUnit: 'years', relativeTo })}`, 'P1Y');
@@ -1171,12 +1194,26 @@ describe('Duration', () => {
11711194
});
11721195
it('throws if neither one of largestUnit or smallestUnit is given', () => {
11731196
const hoursOnly = new Duration(0, 0, 0, 0, 1);
1174-
[{}, () => {}, { roundingMode: 'ceil' }].forEach((options) => {
1175-
throws(() => d.round(options), RangeError);
1176-
throws(() => hoursOnly.round(options), RangeError);
1177-
});
1178-
});
1179-
it('relativeTo is not required for rounding non-calendar units in durations without calendar units', () => {
1197+
[{}, () => {}, { roundingMode: 'ceil' }].forEach((roundTo) => {
1198+
throws(() => d.round(roundTo), RangeError);
1199+
throws(() => hoursOnly.round(roundTo), RangeError);
1200+
});
1201+
});
1202+
it('relativeTo not required to round non-calendar units in durations w/o calendar units (string param)', () => {
1203+
equal(`${d2.round('days')}`, 'P5D');
1204+
equal(`${d2.round('hours')}`, 'P5DT5H');
1205+
equal(`${d2.round('minutes')}`, 'P5DT5H5M');
1206+
equal(`${d2.round('seconds')}`, 'P5DT5H5M5S');
1207+
equal(`${d2.round('milliseconds')}`, 'P5DT5H5M5.005S');
1208+
equal(`${d2.round('microseconds')}`, 'P5DT5H5M5.005005S');
1209+
equal(`${d2.round('nanoseconds')}`, 'P5DT5H5M5.005005005S');
1210+
});
1211+
it('relativeTo is required to round calendar units even in durations w/o calendar units (string param)', () => {
1212+
throws(() => d2.round('years'), RangeError);
1213+
throws(() => d2.round('months'), RangeError);
1214+
throws(() => d2.round('weeks'), RangeError);
1215+
});
1216+
it('relativeTo not required to round non-calendar units in durations w/o calendar units (object param)', () => {
11801217
equal(`${d2.round({ smallestUnit: 'days' })}`, 'P5D');
11811218
equal(`${d2.round({ smallestUnit: 'hours' })}`, 'P5DT5H');
11821219
equal(`${d2.round({ smallestUnit: 'minutes' })}`, 'P5DT5H5M');
@@ -1185,7 +1222,7 @@ describe('Duration', () => {
11851222
equal(`${d2.round({ smallestUnit: 'microseconds' })}`, 'P5DT5H5M5.005005S');
11861223
equal(`${d2.round({ smallestUnit: 'nanoseconds' })}`, 'P5DT5H5M5.005005005S');
11871224
});
1188-
it('relativeTo is required for rounding calendar units even in durations without calendar units', () => {
1225+
it('relativeTo is required to round calendar units even in durations w/o calendar units (object param)', () => {
11891226
throws(() => d2.round({ smallestUnit: 'years' }), RangeError);
11901227
throws(() => d2.round({ smallestUnit: 'months' }), RangeError);
11911228
throws(() => d2.round({ smallestUnit: 'weeks' }), RangeError);
@@ -1378,16 +1415,16 @@ describe('Duration', () => {
13781415
['minutes', 'seconds'].forEach((smallestUnit) => {
13791416
it(`valid ${smallestUnit} increments divide into 60`, () => {
13801417
[1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30].forEach((roundingIncrement) => {
1381-
const options = { smallestUnit, roundingIncrement, relativeTo };
1382-
assert(d.round(options) instanceof Temporal.Duration);
1418+
const roundTo = { smallestUnit, roundingIncrement, relativeTo };
1419+
assert(d.round(roundTo) instanceof Temporal.Duration);
13831420
});
13841421
});
13851422
});
13861423
['milliseconds', 'microseconds', 'nanoseconds'].forEach((smallestUnit) => {
13871424
it(`valid ${smallestUnit} increments divide into 1000`, () => {
13881425
[1, 2, 4, 5, 8, 10, 20, 25, 40, 50, 100, 125, 200, 250, 500].forEach((roundingIncrement) => {
1389-
const options = { smallestUnit, roundingIncrement, relativeTo };
1390-
assert(d.round(options) instanceof Temporal.Duration);
1426+
const roundTo = { smallestUnit, roundingIncrement, relativeTo };
1427+
assert(d.round(roundTo) instanceof Temporal.Duration);
13911428
});
13921429
});
13931430
});
@@ -1468,14 +1505,28 @@ describe('Duration', () => {
14681505
const d = new Duration(5, 5, 5, 5, 5, 5, 5, 5, 5, 5);
14691506
const d2 = new Duration(0, 0, 0, 5, 5, 5, 5, 5, 5, 5);
14701507
const relativeTo = Temporal.PlainDateTime.from('2020-01-01T00:00');
1471-
it('options may only be an object', () => {
1472-
[null, 1, 'hello', true, Symbol('foo'), 1n].forEach((badOptions) => throws(() => d.total(badOptions), TypeError));
1473-
});
1474-
it('throws on disallowed or invalid smallestUnit', () => {
1508+
it('parameter may only be an object or string', () => {
1509+
[null, 1, true, Symbol('foo'), 1n].forEach((badOptions) => throws(() => d.total(badOptions), TypeError));
1510+
});
1511+
it('accepts string parameter as shortcut for {unit}', () => {
1512+
equal(d2.total({ unit: 'days' }).toString(), d2.total('days').toString());
1513+
equal(d2.total({ unit: 'hours' }).toString(), d2.total('hours').toString());
1514+
equal(d2.total({ unit: 'minutes' }).toString(), d2.total('minutes').toString());
1515+
equal(d2.total({ unit: 'seconds' }).toString(), d2.total('seconds').toString());
1516+
equal(d2.total({ unit: 'milliseconds' }).toString(), d2.total('milliseconds').toString());
1517+
equal(d2.total({ unit: 'microseconds' }).toString(), d2.total('microseconds').toString());
1518+
equal(d2.total({ unit: 'nanoseconds' }).toString(), d2.total('nanoseconds').toString());
1519+
});
1520+
it('throws on disallowed or invalid unit (object param)', () => {
14751521
['era', 'nonsense'].forEach((unit) => {
14761522
throws(() => d.total({ unit }), RangeError);
14771523
});
14781524
});
1525+
it('throws on disallowed or invalid unit (string param)', () => {
1526+
['era', 'nonsense'].forEach((unit) => {
1527+
throws(() => d.total(unit), RangeError);
1528+
});
1529+
});
14791530
it('does not lose precision for seconds and smaller units', () => {
14801531
const s = Temporal.Duration.from({ milliseconds: 2, microseconds: 31 }).total({ unit: 'seconds' });
14811532
equal(s, 0.002031);
@@ -1533,14 +1584,19 @@ describe('Duration', () => {
15331584
equal(oneMonth.total({ unit: 'months', relativeTo: { year: 2020, month: 1, day: 1, months: 2 } }), 1);
15341585
});
15351586
it('throws RangeError if unit property is missing', () => {
1536-
[{}, () => {}, { roundingMode: 'ceil' }].forEach((options) => throws(() => d.total(options), RangeError));
1587+
[{}, () => {}, { roundingMode: 'ceil' }].forEach((roundTo) => throws(() => d.total(roundTo), RangeError));
15371588
});
1538-
it('relativeTo is required for rounding calendar units even in durations without calendar units', () => {
1589+
it('relativeTo required to round calendar units even in durations w/o calendar units (object param)', () => {
15391590
throws(() => d2.total({ unit: 'years' }), RangeError);
15401591
throws(() => d2.total({ unit: 'months' }), RangeError);
15411592
throws(() => d2.total({ unit: 'weeks' }), RangeError);
15421593
});
1543-
it('relativeTo is required for rounding durations with calendar units', () => {
1594+
it('relativeTo required to round calendar units even in durations w/o calendar units (string param)', () => {
1595+
throws(() => d2.total('years'), RangeError);
1596+
throws(() => d2.total('months'), RangeError);
1597+
throws(() => d2.total('weeks'), RangeError);
1598+
});
1599+
it('relativeTo is required to round durations with calendar units (object param)', () => {
15441600
throws(() => d.total({ unit: 'years' }), RangeError);
15451601
throws(() => d.total({ unit: 'months' }), RangeError);
15461602
throws(() => d.total({ unit: 'weeks' }), RangeError);
@@ -1552,6 +1608,18 @@ describe('Duration', () => {
15521608
throws(() => d.total({ unit: 'microseconds' }), RangeError);
15531609
throws(() => d.total({ unit: 'nanoseconds' }), RangeError);
15541610
});
1611+
it('relativeTo is required to round durations with calendar units (string param)', () => {
1612+
throws(() => d.total('years'), RangeError);
1613+
throws(() => d.total('months'), RangeError);
1614+
throws(() => d.total('weeks'), RangeError);
1615+
throws(() => d.total('days'), RangeError);
1616+
throws(() => d.total('hours'), RangeError);
1617+
throws(() => d.total('minutes'), RangeError);
1618+
throws(() => d.total('seconds'), RangeError);
1619+
throws(() => d.total('milliseconds'), RangeError);
1620+
throws(() => d.total('microseconds'), RangeError);
1621+
throws(() => d.total('nanoseconds'), RangeError);
1622+
});
15551623
const d2Nanoseconds =
15561624
d2.days * 24 * 3.6e12 +
15571625
d2.hours * 3.6e12 +

test/expected-failures.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Blocked on https://github.com/tc39/test262/pull/3304
2+
built-ins/Temporal/Instant/prototype/round/options-wrong-type.js
3+
built-ins/Temporal/Duration/prototype/total/options-wrong-type.js

test/instant.mjs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,13 +1220,20 @@ describe('Instant', () => {
12201220
throws(() => inst.round({}), RangeError);
12211221
throws(() => inst.round({ roundingIncrement: 1, roundingMode: 'ceil' }), RangeError);
12221222
});
1223-
it('throws on disallowed or invalid smallestUnit', () => {
1223+
it('throws on disallowed or invalid smallestUnit (object param)', () => {
12241224
['era', 'year', 'month', 'week', 'day', 'years', 'months', 'weeks', 'days', 'nonsense'].forEach(
12251225
(smallestUnit) => {
12261226
throws(() => inst.round({ smallestUnit }), RangeError);
12271227
}
12281228
);
12291229
});
1230+
it('throws on disallowed or invalid smallestUnit (string param)', () => {
1231+
['era', 'year', 'month', 'week', 'day', 'years', 'months', 'weeks', 'days', 'nonsense'].forEach(
1232+
(smallestUnit) => {
1233+
throws(() => inst.round(smallestUnit), RangeError);
1234+
}
1235+
);
1236+
});
12301237
it('throws on invalid roundingMode', () => {
12311238
throws(() => inst.round({ smallestUnit: 'second', roundingMode: 'cile' }), RangeError);
12321239
});
@@ -1319,12 +1326,14 @@ describe('Instant', () => {
13191326
throws(() => inst.round({ smallestUnit: 'nanosecond', roundingIncrement: 29 }), RangeError);
13201327
});
13211328
it('accepts plural units', () => {
1322-
assert(inst.round({ smallestUnit: 'hours' }).equals(inst.round({ smallestUnit: 'hour' })));
1323-
assert(inst.round({ smallestUnit: 'minutes' }).equals(inst.round({ smallestUnit: 'minute' })));
1324-
assert(inst.round({ smallestUnit: 'seconds' }).equals(inst.round({ smallestUnit: 'second' })));
1325-
assert(inst.round({ smallestUnit: 'milliseconds' }).equals(inst.round({ smallestUnit: 'millisecond' })));
1326-
assert(inst.round({ smallestUnit: 'microseconds' }).equals(inst.round({ smallestUnit: 'microsecond' })));
1327-
assert(inst.round({ smallestUnit: 'nanoseconds' }).equals(inst.round({ smallestUnit: 'nanosecond' })));
1329+
['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'].forEach((smallestUnit) => {
1330+
assert(inst.round({ smallestUnit }).equals(inst.round({ smallestUnit: `${smallestUnit}s` })));
1331+
});
1332+
});
1333+
it('accepts string parameter as shortcut for {smallestUnit}', () => {
1334+
['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'].forEach((smallestUnit) => {
1335+
assert(inst.round(smallestUnit).equals(inst.round({ smallestUnit })));
1336+
});
13281337
});
13291338
});
13301339
describe('Min/max range', () => {

0 commit comments

Comments
 (0)