LibJS: Implement Temporal.Duration.prototype.round

Until we have re-implemented Temporal.PlainDate/ZonedDateTime, some of
Temporal.Duration.prototype.round (and its invoked AOs) are left
unimplemented.
This commit is contained in:
Timothy Flynn 2024-11-18 14:41:29 -05:00 committed by Tim Flynn
parent 4742775262
commit 5689621c2b
Notes: github-actions[bot] 2024-11-21 00:05:45 +00:00
7 changed files with 450 additions and 0 deletions

View file

@ -43,6 +43,39 @@ StringView temporal_unit_to_string(Unit unit)
return temporal_units[to_underlying(unit)].singular_property_name; return temporal_units[to_underlying(unit)].singular_property_name;
} }
// 13.14 ValidateTemporalRoundingIncrement ( increment, dividend, inclusive ), https://tc39.es/proposal-temporal/#sec-validatetemporalroundingincrement
ThrowCompletionOr<void> validate_temporal_rounding_increment(VM& vm, u64 increment, u64 dividend, bool inclusive)
{
u64 maximum = 0;
// 1. If inclusive is true, then
if (inclusive) {
// a. Let maximum be dividend.
maximum = dividend;
}
// 2. Else,
else {
// a. Assert: dividend > 1.
VERIFY(dividend > 1);
// b. Let maximum be dividend - 1.
maximum = dividend - 1;
}
// 3. If increment > maximum, throw a RangeError exception.
if (increment > maximum)
return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, increment, "roundingIncrement");
// 5. If dividend modulo increment ≠ 0, then
if (modulo(dividend, increment) != 0) {
// a. Throw a RangeError exception.
return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, increment, "roundingIncrement");
}
// 6. Return UNUSED.
return {};
}
// 13.15 GetTemporalFractionalSecondDigitsOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporalfractionalseconddigitsoption // 13.15 GetTemporalFractionalSecondDigitsOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporalfractionalseconddigitsoption
ThrowCompletionOr<Precision> get_temporal_fractional_second_digits_option(VM& vm, Object const& options) ThrowCompletionOr<Precision> get_temporal_fractional_second_digits_option(VM& vm, Object const& options)
{ {
@ -324,6 +357,14 @@ UnitCategory temporal_unit_category(Unit unit)
return temporal_units[to_underlying(unit)].category; return temporal_units[to_underlying(unit)].category;
} }
// 13.22 MaximumTemporalDurationRoundingIncrement ( unit ), https://tc39.es/proposal-temporal/#sec-temporal-maximumtemporaldurationroundingincrement
RoundingIncrement maximum_temporal_duration_rounding_increment(Unit unit)
{
// 1. Return the value from the "Maximum duration rounding increment" column of the row of Table 21 in which unit is
// in the "Value" column.
return temporal_units[to_underlying(unit)].maximum_duration_rounding_increment;
}
// AD-HOC // AD-HOC
Crypto::UnsignedBigInteger const& temporal_unit_length_in_nanoseconds(Unit unit) Crypto::UnsignedBigInteger const& temporal_unit_length_in_nanoseconds(Unit unit)
{ {
@ -930,4 +971,25 @@ ThrowCompletionOr<RoundingMode> get_rounding_mode_option(VM& vm, Object const& o
return static_cast<RoundingMode>(allowed_strings.first_index_of(string_value.as_string().utf8_string_view()).value()); return static_cast<RoundingMode>(allowed_strings.first_index_of(string_value.as_string().utf8_string_view()).value());
} }
// 14.4.1.4 GetRoundingIncrementOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-getroundingincrementoption
ThrowCompletionOr<u64> get_rounding_increment_option(VM& vm, Object const& options)
{
// 1. Let value be ? Get(options, "roundingIncrement").
auto value = TRY(options.get(vm.names.roundingIncrement));
// 2. If value is undefined, return 1𝔽.
if (value.is_undefined())
return 1;
// 3. Let integerIncrement be ? ToIntegerWithTruncation(value).
auto integer_increment = TRY(to_integer_with_truncation(vm, value, ErrorType::OptionIsNotValidValue, value, "roundingIncrement"sv));
// 4. If integerIncrement < 1 or integerIncrement > 10**9, throw a RangeError exception.
if (integer_increment < 1 || integer_increment > 1'000'000'000u)
return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, value, "roundingIncrement");
// 5. Return integerIncrement.
return static_cast<u64>(integer_increment);
}
} }

View file

@ -103,6 +103,7 @@ struct RelativeTo {
GC::Ptr<JS::Object> zoned_relative_to; // [[ZonedRelativeTo]] GC::Ptr<JS::Object> zoned_relative_to; // [[ZonedRelativeTo]]
}; };
ThrowCompletionOr<void> validate_temporal_rounding_increment(VM&, u64 increment, u64 dividend, bool inclusive);
ThrowCompletionOr<Precision> get_temporal_fractional_second_digits_option(VM&, Object const& options); ThrowCompletionOr<Precision> get_temporal_fractional_second_digits_option(VM&, Object const& options);
SecondsStringPrecision to_seconds_string_precision_record(UnitValue, Precision); SecondsStringPrecision to_seconds_string_precision_record(UnitValue, Precision);
ThrowCompletionOr<UnitValue> get_temporal_unit_valued_option(VM&, Object const& options, PropertyKey const&, UnitGroup, UnitDefault const&, ReadonlySpan<UnitValue> extra_values = {}); ThrowCompletionOr<UnitValue> get_temporal_unit_valued_option(VM&, Object const& options, PropertyKey const&, UnitGroup, UnitDefault const&, ReadonlySpan<UnitValue> extra_values = {});
@ -110,6 +111,7 @@ ThrowCompletionOr<RelativeTo> get_temporal_relative_to_option(VM&, Object const&
Unit larger_of_two_temporal_units(Unit, Unit); Unit larger_of_two_temporal_units(Unit, Unit);
bool is_calendar_unit(Unit); bool is_calendar_unit(Unit);
UnitCategory temporal_unit_category(Unit); UnitCategory temporal_unit_category(Unit);
RoundingIncrement maximum_temporal_duration_rounding_increment(Unit);
Crypto::UnsignedBigInteger const& temporal_unit_length_in_nanoseconds(Unit); Crypto::UnsignedBigInteger const& temporal_unit_length_in_nanoseconds(Unit);
String format_fractional_seconds(u64, Precision); String format_fractional_seconds(u64, Precision);
UnsignedRoundingMode get_unsigned_rounding_mode(RoundingMode, Sign); UnsignedRoundingMode get_unsigned_rounding_mode(RoundingMode, Sign);
@ -183,5 +185,6 @@ ThrowCompletionOr<Value> get_option(VM& vm, Object const& options, PropertyKey c
} }
ThrowCompletionOr<RoundingMode> get_rounding_mode_option(VM&, Object const& options, RoundingMode fallback); ThrowCompletionOr<RoundingMode> get_rounding_mode_option(VM&, Object const& options, RoundingMode fallback);
ThrowCompletionOr<u64> get_rounding_increment_option(VM&, Object const& options);
} }

View file

@ -9,6 +9,7 @@
#include <AK/Math.h> #include <AK/Math.h>
#include <AK/NumericLimits.h> #include <AK/NumericLimits.h>
#include <LibCrypto/BigFraction/BigFraction.h>
#include <LibJS/Runtime/AbstractOperations.h> #include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Intrinsics.h> #include <LibJS/Runtime/Intrinsics.h>
#include <LibJS/Runtime/Realm.h> #include <LibJS/Runtime/Realm.h>
@ -740,6 +741,21 @@ ThrowCompletionOr<TimeDuration> round_time_duration(VM& vm, TimeDuration const&
return TRY(round_time_duration_to_increment(vm, time_duration, divisor.multiplied_by(increment), rounding_mode)); return TRY(round_time_duration_to_increment(vm, time_duration, divisor.multiplied_by(increment), rounding_mode));
} }
// 7.5.31 TotalTimeDuration ( timeDuration, unit ), https://tc39.es/proposal-temporal/#sec-temporal-totaltimeduration
double total_time_duration(TimeDuration const& time_duration, Unit unit)
{
// 1. Let divisor be the value in the "Length in Nanoseconds" column of the row of Table 21 whose "Value" column contains unit.
auto const& divisor = temporal_unit_length_in_nanoseconds(unit);
// 2. NOTE: The following step cannot be implemented directly using floating-point arithmetic when 𝔽(timeDuration) is
// not a safe integer. The division can be implemented in C++ with the __float128 type if the compiler supports it,
// or with software emulation such as in the SoftFP library.
// 3. Return timeDuration / divisor.
auto result = Crypto::BigFraction { time_duration } / Crypto::BigFraction { Crypto::SignedBigInteger { divisor } };
return result.to_double();
}
// 7.5.39 TemporalDurationToString ( duration, precision ), https://tc39.es/proposal-temporal/#sec-temporal-temporaldurationtostring // 7.5.39 TemporalDurationToString ( duration, precision ), https://tc39.es/proposal-temporal/#sec-temporal-temporaldurationtostring
String temporal_duration_to_string(Duration const& duration, Precision precision) String temporal_duration_to_string(Duration const& duration, Precision precision)
{ {

View file

@ -121,6 +121,7 @@ i8 compare_time_duration(TimeDuration const&, TimeDuration const&);
ThrowCompletionOr<TimeDuration> round_time_duration_to_increment(VM&, TimeDuration const&, Crypto::UnsignedBigInteger const& increment, RoundingMode); ThrowCompletionOr<TimeDuration> round_time_duration_to_increment(VM&, TimeDuration const&, Crypto::UnsignedBigInteger const& increment, RoundingMode);
i8 time_duration_sign(TimeDuration const&); i8 time_duration_sign(TimeDuration const&);
ThrowCompletionOr<TimeDuration> round_time_duration(VM&, TimeDuration const&, Crypto::UnsignedBigInteger const& increment, Unit, RoundingMode); ThrowCompletionOr<TimeDuration> round_time_duration(VM&, TimeDuration const&, Crypto::UnsignedBigInteger const& increment, Unit, RoundingMode);
double total_time_duration(TimeDuration const&, Unit);
String temporal_duration_to_string(Duration const&, Precision); String temporal_duration_to_string(Duration const&, Precision);
ThrowCompletionOr<GC::Ref<Duration>> add_durations(VM&, ArithmeticOperation, Duration const&, Value); ThrowCompletionOr<GC::Ref<Duration>> add_durations(VM&, ArithmeticOperation, Duration const&, Value);

View file

@ -41,6 +41,7 @@ void DurationPrototype::initialize(Realm& realm)
define_native_function(realm, vm.names.abs, abs, 0, attr); define_native_function(realm, vm.names.abs, abs, 0, attr);
define_native_function(realm, vm.names.add, add, 1, attr); define_native_function(realm, vm.names.add, add, 1, attr);
define_native_function(realm, vm.names.subtract, subtract, 1, attr); define_native_function(realm, vm.names.subtract, subtract, 1, attr);
define_native_function(realm, vm.names.round, round, 1, attr);
define_native_function(realm, vm.names.toString, to_string, 0, attr); define_native_function(realm, vm.names.toString, to_string, 0, attr);
define_native_function(realm, vm.names.toJSON, to_json, 0, attr); define_native_function(realm, vm.names.toJSON, to_json, 0, attr);
define_native_function(realm, vm.names.toLocaleString, to_locale_string, 0, attr); define_native_function(realm, vm.names.toLocaleString, to_locale_string, 0, attr);
@ -217,6 +218,199 @@ JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::subtract)
return TRY(add_durations(vm, ArithmeticOperation::Subtract, duration, other)); return TRY(add_durations(vm, ArithmeticOperation::Subtract, duration, other));
} }
// 7.3.20 Temporal.Duration.prototype.round ( roundTo ), https://tc39.es/proposal-temporal/#sec-temporal.duration.prototype.round
JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::round)
{
auto& realm = *vm.current_realm();
auto round_to_value = vm.argument(0);
// 1. Let duration be the this value.
// 2. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]).
auto duration = TRY(typed_this_object(vm));
// 3. If roundTo is undefined, then
if (round_to_value.is_undefined()) {
// a. Throw a TypeError exception.
return vm.throw_completion<TypeError>(ErrorType::TemporalMissingOptionsObject);
}
GC::Ptr<Object> round_to;
// 4. If roundTo is a String, then
if (round_to_value.is_string()) {
// a. Let paramString be roundTo.
auto param_string = round_to_value;
// b. Set roundTo to OrdinaryObjectCreate(null).
round_to = Object::create(realm, nullptr);
// c. Perform ! CreateDataPropertyOrThrow(roundTo, "smallestUnit", paramString).
MUST(round_to->create_data_property_or_throw(vm.names.smallestUnit, param_string));
}
// 5. Else,
else {
// a. Set roundTo to ? GetOptionsObject(roundTo).
round_to = TRY(get_options_object(vm, round_to_value));
}
// 6. Let smallestUnitPresent be true.
bool smallest_unit_present = true;
// 7. Let largestUnitPresent be true.
bool largest_unit_present = true;
// 8. NOTE: The following steps read options and perform independent validation in alphabetical order
// (GetTemporalRelativeToOption reads "relativeTo", GetRoundingIncrementOption reads "roundingIncrement" and
// GetRoundingModeOption reads "roundingMode").
// 9. Let largestUnit be ? GetTemporalUnitValuedOption(roundTo, "largestUnit", DATETIME, UNSET, « auto »).
auto largest_unit = TRY(get_temporal_unit_valued_option(vm, *round_to, vm.names.largestUnit, UnitGroup::DateTime, Unset {}, { { Auto {} } }));
// 10. Let relativeToRecord be ? GetTemporalRelativeToOption(roundTo).
// 11. Let zonedRelativeTo be relativeToRecord.[[ZonedRelativeTo]].
// 12. Let plainRelativeTo be relativeToRecord.[[PlainRelativeTo]].
auto [zoned_relative_to, plain_relative_to] = TRY(get_temporal_relative_to_option(vm, *round_to));
// 13. Let roundingIncrement be ? GetRoundingIncrementOption(roundTo).
auto rounding_increment = TRY(get_rounding_increment_option(vm, *round_to));
// 14. Let roundingMode be ? GetRoundingModeOption(roundTo, HALF-EXPAND).
auto rounding_mode = TRY(get_rounding_mode_option(vm, *round_to, RoundingMode::HalfExpand));
// 15. Let smallestUnit be ? GetTemporalUnitValuedOption(roundTo, "smallestUnit", DATETIME, UNSET).
auto smallest_unit = TRY(get_temporal_unit_valued_option(vm, *round_to, vm.names.smallestUnit, UnitGroup::DateTime, Unset {}));
// 16. If smallestUnit is UNSET, then
if (smallest_unit.has<Unset>()) {
// a. Set smallestUnitPresent to false.
smallest_unit_present = false;
// b. Set smallestUnit to NANOSECOND.
smallest_unit = Unit::Nanosecond;
}
auto smallest_unit_value = smallest_unit.get<Unit>();
// 17. Let existingLargestUnit be DefaultTemporalLargestUnit(duration).
auto existing_largest_unit = default_temporal_largest_unit(duration);
// 18. Let defaultLargestUnit be LargerOfTwoTemporalUnits(existingLargestUnit, smallestUnit).
auto default_largest_unit = larger_of_two_temporal_units(existing_largest_unit, smallest_unit_value);
// 19. If largestUnit is UNSET, then
if (largest_unit.has<Unset>()) {
// a. Set largestUnitPresent to false.
largest_unit_present = false;
// b. Set largestUnit to defaultLargestUnit.
largest_unit = default_largest_unit;
}
// 20. Else if largestUnit is AUTO, then
else if (largest_unit.has<Auto>()) {
// a. Set largestUnit to defaultLargestUnit.
largest_unit = default_largest_unit;
}
// 21. If smallestUnitPresent is false and largestUnitPresent is false, then
if (!smallest_unit_present && !largest_unit_present) {
// a. Throw a RangeError exception.
return vm.throw_completion<RangeError>(ErrorType::TemporalMissingUnits);
}
auto largest_unit_value = largest_unit.get<Unit>();
// 22. If LargerOfTwoTemporalUnits(largestUnit, smallestUnit) is not largestUnit, throw a RangeError exception.
if (larger_of_two_temporal_units(largest_unit_value, smallest_unit_value) != largest_unit_value)
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidUnitRange, temporal_unit_to_string(smallest_unit_value), temporal_unit_to_string(largest_unit_value));
// 23. Let maximum be MaximumTemporalDurationRoundingIncrement(smallestUnit).
auto maximum = maximum_temporal_duration_rounding_increment(smallest_unit_value);
// 24. If maximum is not UNSET, perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, false).
if (!maximum.has<Unset>())
TRY(validate_temporal_rounding_increment(vm, rounding_increment, maximum.get<u64>(), false));
// 25. If roundingIncrement > 1, and largestUnit is not smallestUnit, and TemporalUnitCategory(smallestUnit) is DATE,
// throw a RangeError exception.
if (rounding_increment > 1 && largest_unit_value != smallest_unit_value && temporal_unit_category(smallest_unit_value) == UnitCategory::Date)
return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, rounding_increment, "roundingIncrement");
// 26. If zonedRelativeTo is not undefined, then
if (zoned_relative_to) {
// a. Let internalDuration be ToInternalDurationRecord(duration).
auto internal_duration = to_internal_duration_record(vm, duration);
// FIXME: b. Let timeZone be zonedRelativeTo.[[TimeZone]].
// FIXME: c. Let calendar be zonedRelativeTo.[[Calendar]].
// FIXME: d. Let relativeEpochNs be zonedRelativeTo.[[EpochNanoseconds]].
// FIXME: e. Let targetEpochNs be ? AddZonedDateTime(relativeEpochNs, timeZone, calendar, internalDuration, constrain).
// FIXME: f. Set internalDuration to ? DifferenceZonedDateTimeWithRounding(relativeEpochNs, targetEpochNs, timeZone, calendar, largestUnit, roundingIncrement, smallestUnit, roundingMode).
// g. If TemporalUnitCategory(largestUnit) is date, set largestUnit to hour.
if (temporal_unit_category(largest_unit_value) == UnitCategory::Date)
largest_unit_value = Unit::Hour;
// h. Return ? TemporalDurationFromInternal(internalDuration, largestUnit).
return TRY(temporal_duration_from_internal(vm, internal_duration, largest_unit_value));
}
// 27. If plainRelativeTo is not undefined, then
if (plain_relative_to) {
// a. Let internalDuration be ToInternalDurationRecordWith24HourDays(duration).
auto internal_duration = to_internal_duration_record_with_24_hour_days(vm, duration);
// FIXME: b. Let targetTime be AddTime(MidnightTimeRecord(), internalDuration.[[Time]]).
// FIXME: c. Let calendar be plainRelativeTo.[[Calendar]].
// FIXME: d. Let dateDuration be ! AdjustDateDurationRecord(internalDuration.[[Date]], targetTime.[[Days]]).
// FIXME: e. Let targetDate be ? CalendarDateAdd(calendar, plainRelativeTo.[[ISODate]], dateDuration, constrain).
// FIXME: f. Let isoDateTime be CombineISODateAndTimeRecord(plainRelativeTo.[[ISODate]], MidnightTimeRecord()).
// FIXME: g. Let targetDateTime be CombineISODateAndTimeRecord(targetDate, targetTime).
// FIXME: h. Set internalDuration to ? DifferencePlainDateTimeWithRounding(isoDateTime, targetDateTime, calendar, largestUnit, roundingIncrement, smallestUnit, roundingMode).
// i. Return ? TemporalDurationFromInternal(internalDuration, largestUnit).
return TRY(temporal_duration_from_internal(vm, internal_duration, largest_unit_value));
}
// 28. If IsCalendarUnit(existingLargestUnit) is true, or IsCalendarUnit(largestUnit) is true, throw a RangeError exception.
if (is_calendar_unit(existing_largest_unit))
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidLargestUnit, temporal_unit_to_string(existing_largest_unit));
if (is_calendar_unit(largest_unit_value))
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidLargestUnit, temporal_unit_to_string(largest_unit_value));
// 29. Assert: IsCalendarUnit(smallestUnit) is false.
VERIFY(!is_calendar_unit(smallest_unit_value));
// 30. Let internalDuration be ToInternalDurationRecordWith24HourDays(duration).
auto internal_duration = to_internal_duration_record_with_24_hour_days(vm, duration);
// 31. If smallestUnit is DAY, then
if (smallest_unit_value == Unit::Day) {
// a. Let fractionalDays be TotalTimeDuration(internalDuration.[[Time]], DAY).
auto fractional_days = total_time_duration(internal_duration.time, Unit::Day);
// b. Let days be RoundNumberToIncrement(fractionalDays, roundingIncrement, roundingMode).
auto days = round_number_to_increment(fractional_days, rounding_increment, rounding_mode);
// c. Let dateDuration be ? CreateDateDurationRecord(0, 0, 0, days).
auto date_duration = TRY(create_date_duration_record(vm, 0, 0, 0, days));
// d. Set internalDuration to ! CombineDateAndTimeDuration(dateDuration, 0).
internal_duration = MUST(combine_date_and_time_duration(vm, date_duration, TimeDuration { 0 }));
}
// 32. Else,
else {
// a. Let timeDuration be ? RoundTimeDuration(internalDuration.[[Time]], roundingIncrement, smallestUnit, roundingMode).
auto time_duration = TRY(round_time_duration(vm, internal_duration.time, Crypto::UnsignedBigInteger { rounding_increment }, smallest_unit_value, rounding_mode));
// b. Set internalDuration to ! CombineDateAndTimeDuration(ZeroDateDuration(), timeDuration).
internal_duration = MUST(combine_date_and_time_duration(vm, zero_date_duration(vm), move(time_duration)));
}
// 33. Return ? TemporalDurationFromInternal(internalDuration, largestUnit).
return TRY(temporal_duration_from_internal(vm, internal_duration, largest_unit_value));
}
// 7.3.22 Temporal.Duration.prototype.toString ( [ options ] ), https://tc39.es/proposal-temporal/#sec-temporal.duration.prototype.tostring // 7.3.22 Temporal.Duration.prototype.toString ( [ options ] ), https://tc39.es/proposal-temporal/#sec-temporal.duration.prototype.tostring
JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::to_string) JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::to_string)
{ {

View file

@ -35,6 +35,7 @@ private:
JS_DECLARE_NATIVE_FUNCTION(abs); JS_DECLARE_NATIVE_FUNCTION(abs);
JS_DECLARE_NATIVE_FUNCTION(add); JS_DECLARE_NATIVE_FUNCTION(add);
JS_DECLARE_NATIVE_FUNCTION(subtract); JS_DECLARE_NATIVE_FUNCTION(subtract);
JS_DECLARE_NATIVE_FUNCTION(round);
JS_DECLARE_NATIVE_FUNCTION(to_string); JS_DECLARE_NATIVE_FUNCTION(to_string);
JS_DECLARE_NATIVE_FUNCTION(to_json); JS_DECLARE_NATIVE_FUNCTION(to_json);
JS_DECLARE_NATIVE_FUNCTION(to_locale_string); JS_DECLARE_NATIVE_FUNCTION(to_locale_string);

View file

@ -0,0 +1,173 @@
describe("correct behavior", () => {
test("length is 1", () => {
expect(Temporal.Duration.prototype.round).toHaveLength(1);
});
test("basic functionality", () => {
const duration = new Temporal.Duration(0, 0, 0, 21, 7, 10, 100, 200, 300, 400);
const values = [
["nanosecond", "P21DT7H11M40.2003004S"],
["microsecond", "P21DT7H11M40.2003S"],
["millisecond", "P21DT7H11M40.2S"],
["second", "P21DT7H11M40S"],
["minute", "P21DT7H12M"],
["hour", "P21DT7H"],
["day", "P21D"],
];
for (const [smallestUnit, durationString] of values) {
const singularRoundedDuration = duration.round({ smallestUnit });
const pluralRoundedDuration = duration.round({ smallestUnit: `${smallestUnit}s` });
// Passing in a string is treated as though { smallestUnit: "<string value>" } was passed in.
const singularRoundedDurationWithString = duration.round(smallestUnit);
const pluralRoundedDurationWithString = duration.round(`${smallestUnit}s`);
expect(singularRoundedDuration.toString()).toBe(durationString);
expect(singularRoundedDurationWithString.toString()).toBe(durationString);
expect(pluralRoundedDuration.toString()).toBe(durationString);
expect(pluralRoundedDurationWithString.toString()).toBe(durationString);
}
});
test("largestUnit option", () => {
const duration = new Temporal.Duration(0, 0, 0, 21, 7, 10, 100, 200, 300, 400);
// Using strings is not sufficient here, for example, the nanosecond case will produce "PT1840300.2003004S" which is 1840300 s, 200 ms, 300 us, 400 ns
const values = [
["nanosecond", { nanoseconds: 1840300200300400 }],
["microsecond", { microseconds: 1840300200300, nanoseconds: 400 }],
["millisecond", { milliseconds: 1840300200, microseconds: 300, nanoseconds: 400 }],
[
"second",
{ seconds: 1840300, milliseconds: 200, microseconds: 300, nanoseconds: 400 },
],
[
"minute",
{
minutes: 30671,
seconds: 40,
milliseconds: 200,
microseconds: 300,
nanoseconds: 400,
},
],
[
"hour",
{
hours: 511,
minutes: 11,
seconds: 40,
milliseconds: 200,
microseconds: 300,
nanoseconds: 400,
},
],
[
"day",
{
days: 21,
hours: 7,
minutes: 11,
seconds: 40,
milliseconds: 200,
microseconds: 300,
nanoseconds: 400,
},
],
];
for (const [largestUnit, durationLike] of values) {
const singularRoundedDuration = duration.round({ largestUnit });
const pluralRoundedDuration = duration.round({ largestUnit: `${largestUnit}s` });
const propertiesToCheck = Object.keys(durationLike);
for (const property of propertiesToCheck) {
expect(singularRoundedDuration[property]).toBe(durationLike[property]);
expect(pluralRoundedDuration[property]).toBe(durationLike[property]);
}
}
});
});
describe("errors", () => {
test("this value must be a Temporal.Duration object", () => {
expect(() => {
Temporal.Duration.prototype.round.call("foo");
}).toThrowWithMessage(TypeError, "Not an object of type Temporal.Duration");
});
test("missing options object", () => {
const duration = new Temporal.Duration(1);
expect(() => {
duration.round();
}).toThrowWithMessage(TypeError, "Required options object is missing or undefined");
});
test("invalid rounding mode", () => {
const duration = new Temporal.Duration(1);
expect(() => {
duration.round({ smallestUnit: "second", roundingMode: "serenityOS" });
}).toThrowWithMessage(
RangeError,
"serenityOS is not a valid value for option roundingMode"
);
});
test("invalid smallest unit", () => {
const duration = new Temporal.Duration(1);
expect(() => {
duration.round({ smallestUnit: "serenityOS" });
}).toThrowWithMessage(
RangeError,
"serenityOS is not a valid value for option smallestUnit"
);
});
test("increment may not be NaN", () => {
const duration = new Temporal.Duration(1);
expect(() => {
duration.round({ smallestUnit: "second", roundingIncrement: NaN });
}).toThrowWithMessage(RangeError, "NaN is not a valid value for option roundingIncrement");
});
test("increment may smaller than 1 or larger than maximum", () => {
const duration = new Temporal.Duration(1);
expect(() => {
duration.round({ smallestUnit: "second", roundingIncrement: -1 });
}).toThrowWithMessage(RangeError, "-1 is not a valid value for option roundingIncrement");
expect(() => {
duration.round({ smallestUnit: "second", roundingIncrement: 0 });
}).toThrowWithMessage(RangeError, "0 is not a valid value for option roundingIncrement");
expect(() => {
duration.round({ smallestUnit: "second", roundingIncrement: Infinity });
}).toThrowWithMessage(
RangeError,
"Infinity is not a valid value for option roundingIncrement"
);
});
test("must provide one or both of smallestUnit or largestUnit", () => {
const duration = new Temporal.Duration(1);
expect(() => {
duration.round({});
}).toThrowWithMessage(RangeError, "One or both of smallestUnit or largestUnit is required");
});
test("relativeTo is required when duration has calendar units", () => {
const duration = new Temporal.Duration(1);
expect(() => {
duration.round({ largestUnit: "second" });
}).toThrowWithMessage(RangeError, "Largest unit must not be year");
});
// Spec Issue: https://github.com/tc39/proposal-temporal/issues/2124
// Spec Fix: https://github.com/tc39/proposal-temporal/commit/66f7464aaec64d3cd21fb2ec37f6502743b9a730
test("balancing calendar units with largestUnit set to 'year' and relativeTo unset throws instead of crashing", () => {
const duration = new Temporal.Duration(1);
expect(() => {
duration.round({ largestUnit: "year" });
}).toThrowWithMessage(RangeError, "Largest unit must not be year");
});
});