From 5689621c2b78cc9e8df2a8d1cdb46d6e22c99108 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Mon, 18 Nov 2024 14:41:29 -0500 Subject: [PATCH] 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. --- .../Runtime/Temporal/AbstractOperations.cpp | 62 ++++++ .../Runtime/Temporal/AbstractOperations.h | 3 + Libraries/LibJS/Runtime/Temporal/Duration.cpp | 16 ++ Libraries/LibJS/Runtime/Temporal/Duration.h | 1 + .../Runtime/Temporal/DurationPrototype.cpp | 194 ++++++++++++++++++ .../Runtime/Temporal/DurationPrototype.h | 1 + .../Duration/Duration.prototype.round.js | 173 ++++++++++++++++ 7 files changed, 450 insertions(+) create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.round.js diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index 3f0b3f4d737..47f32ab13cb 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -43,6 +43,39 @@ StringView temporal_unit_to_string(Unit unit) return temporal_units[to_underlying(unit)].singular_property_name; } +// 13.14 ValidateTemporalRoundingIncrement ( increment, dividend, inclusive ), https://tc39.es/proposal-temporal/#sec-validatetemporalroundingincrement +ThrowCompletionOr 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(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(ErrorType::OptionIsNotValidValue, increment, "roundingIncrement"); + } + + // 6. Return UNUSED. + return {}; +} + // 13.15 GetTemporalFractionalSecondDigitsOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporalfractionalseconddigitsoption ThrowCompletionOr 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; } +// 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 Crypto::UnsignedBigInteger const& temporal_unit_length_in_nanoseconds(Unit unit) { @@ -930,4 +971,25 @@ ThrowCompletionOr get_rounding_mode_option(VM& vm, Object const& o return static_cast(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 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(ErrorType::OptionIsNotValidValue, value, "roundingIncrement"); + + // 5. Return integerIncrement. + return static_cast(integer_increment); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index ec028356479..7e4fa8ab79e 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -103,6 +103,7 @@ struct RelativeTo { GC::Ptr zoned_relative_to; // [[ZonedRelativeTo]] }; +ThrowCompletionOr validate_temporal_rounding_increment(VM&, u64 increment, u64 dividend, bool inclusive); ThrowCompletionOr get_temporal_fractional_second_digits_option(VM&, Object const& options); SecondsStringPrecision to_seconds_string_precision_record(UnitValue, Precision); ThrowCompletionOr get_temporal_unit_valued_option(VM&, Object const& options, PropertyKey const&, UnitGroup, UnitDefault const&, ReadonlySpan extra_values = {}); @@ -110,6 +111,7 @@ ThrowCompletionOr get_temporal_relative_to_option(VM&, Object const& Unit larger_of_two_temporal_units(Unit, Unit); bool is_calendar_unit(Unit); UnitCategory temporal_unit_category(Unit); +RoundingIncrement maximum_temporal_duration_rounding_increment(Unit); Crypto::UnsignedBigInteger const& temporal_unit_length_in_nanoseconds(Unit); String format_fractional_seconds(u64, Precision); UnsignedRoundingMode get_unsigned_rounding_mode(RoundingMode, Sign); @@ -183,5 +185,6 @@ ThrowCompletionOr get_option(VM& vm, Object const& options, PropertyKey c } ThrowCompletionOr get_rounding_mode_option(VM&, Object const& options, RoundingMode fallback); +ThrowCompletionOr get_rounding_increment_option(VM&, Object const& options); } diff --git a/Libraries/LibJS/Runtime/Temporal/Duration.cpp b/Libraries/LibJS/Runtime/Temporal/Duration.cpp index 9485424205b..31f6d28a54e 100644 --- a/Libraries/LibJS/Runtime/Temporal/Duration.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Duration.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -740,6 +741,21 @@ ThrowCompletionOr round_time_duration(VM& vm, TimeDuration const& 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 String temporal_duration_to_string(Duration const& duration, Precision precision) { diff --git a/Libraries/LibJS/Runtime/Temporal/Duration.h b/Libraries/LibJS/Runtime/Temporal/Duration.h index c9f174edde8..22d967d5155 100644 --- a/Libraries/LibJS/Runtime/Temporal/Duration.h +++ b/Libraries/LibJS/Runtime/Temporal/Duration.h @@ -121,6 +121,7 @@ i8 compare_time_duration(TimeDuration const&, TimeDuration const&); ThrowCompletionOr round_time_duration_to_increment(VM&, TimeDuration const&, Crypto::UnsignedBigInteger const& increment, RoundingMode); i8 time_duration_sign(TimeDuration const&); ThrowCompletionOr 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); ThrowCompletionOr> add_durations(VM&, ArithmeticOperation, Duration const&, Value); diff --git a/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp index a747bb167dd..1c9e1bf5b47 100644 --- a/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/DurationPrototype.cpp @@ -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.add, add, 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.toJSON, to_json, 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)); } +// 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(ErrorType::TemporalMissingOptionsObject); + } + + GC::Ptr 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()) { + // 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(); + + // 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()) { + // 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()) { + // 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(ErrorType::TemporalMissingUnits); + } + + auto largest_unit_value = largest_unit.get(); + + // 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(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()) + TRY(validate_temporal_rounding_increment(vm, rounding_increment, maximum.get(), 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(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(ErrorType::TemporalInvalidLargestUnit, temporal_unit_to_string(existing_largest_unit)); + if (is_calendar_unit(largest_unit_value)) + return vm.throw_completion(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 JS_DEFINE_NATIVE_FUNCTION(DurationPrototype::to_string) { diff --git a/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h b/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h index f28c1794ca8..2070d5ea6c3 100644 --- a/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h +++ b/Libraries/LibJS/Runtime/Temporal/DurationPrototype.h @@ -35,6 +35,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(abs); JS_DECLARE_NATIVE_FUNCTION(add); JS_DECLARE_NATIVE_FUNCTION(subtract); + JS_DECLARE_NATIVE_FUNCTION(round); JS_DECLARE_NATIVE_FUNCTION(to_string); JS_DECLARE_NATIVE_FUNCTION(to_json); JS_DECLARE_NATIVE_FUNCTION(to_locale_string); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.round.js b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.round.js new file mode 100644 index 00000000000..24f012784c9 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Duration/Duration.prototype.round.js @@ -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: "" } 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"); + }); +});