diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index 93c0ed97452..0add7e093b9 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -114,6 +114,176 @@ Value get_option(GlobalObject& global_object, Object& options, String const& pro return value; } +// 13.8 ToTemporalRoundingMode ( normalizedOptions, fallback ), https://tc39.es/proposal-temporal/#sec-temporal-totemporalroundingmode +String to_temporal_rounding_mode(GlobalObject& global_object, Object& normalized_options, String const& fallback) +{ + auto& vm = global_object.vm(); + + auto option = get_option(global_object, normalized_options, "roundingMode", { OptionType::String }, { "ceil"sv, "floor"sv, "trunc"sv, "halfExpand"sv }, js_string(vm, fallback)); + if (vm.exception()) + return {}; + + VERIFY(option.is_string()); + return option.as_string().string(); +} + +// 13.14 ToTemporalRoundingIncrement ( normalizedOptions, dividend, inclusive ), https://tc39.es/proposal-temporal/#sec-temporal-totemporalroundingincrement +u64 to_temporal_rounding_increment(GlobalObject& global_object, Object& normalized_options, Optional dividend, bool inclusive) +{ + auto& vm = global_object.vm(); + + double maximum; + // 1. If dividend is undefined, then + if (!dividend.has_value()) { + // a. Let maximum be +∞. + maximum = INFINITY; + } + // 2. Else if inclusive is true, then + else if (inclusive) { + // a. Let maximum be dividend. + maximum = *dividend; + } + // 3. Else if dividend is more than 1, then + else if (*dividend > 1) { + // a. Let maximum be dividend − 1. + maximum = *dividend - 1; + } + // 4. Else, + else { + // a. Let maximum be 1. + maximum = 1; + } + + // 5. Let increment be ? GetOption(normalizedOptions, "roundingIncrement", « Number », empty, 1). + auto increment_value = get_option(global_object, normalized_options, "roundingIncrement", { OptionType::Number }, {}, Value(1)); + if (vm.exception()) + return {}; + VERIFY(increment_value.is_number()); + auto increment = increment_value.as_double(); + + // 6. If increment < 1 or increment > maximum, throw a RangeError exception. + if (increment < 1 || increment > maximum) { + vm.throw_exception(global_object, ErrorType::OptionIsNotValidValue, increment, "roundingIncrement"); + return {}; + } + + // 7. Set increment to floor(ℝ(increment)). + auto floored_increment = static_cast(increment); + + // 8. If dividend is not undefined and dividend modulo increment is not zero, then + if (dividend.has_value() && static_cast(*dividend) % floored_increment != 0) { + // a. Throw a RangeError exception. + vm.throw_exception(global_object, ErrorType::OptionIsNotValidValue, increment, "roundingIncrement"); + return {}; + } + + // 9. Return increment. + return floored_increment; +} + +// https://tc39.es/proposal-temporal/#table-temporal-singular-and-plural-units +static HashMap plural_to_singular_units = { + { "years"sv, "year"sv }, + { "months"sv, "month"sv }, + { "weeks"sv, "week"sv }, + { "days"sv, "day"sv }, + { "hours"sv, "hour"sv }, + { "minutes"sv, "minute"sv }, + { "seconds"sv, "second"sv }, + { "milliseconds"sv, "millisecond"sv }, + { "microseconds"sv, "microsecond"sv }, + { "nanoseconds"sv, "nanosecond"sv } +}; + +// 13.18 ToSmallestTemporalUnit ( normalizedOptions, disallowedUnits, fallback ), https://tc39.es/proposal-temporal/#sec-temporal-tosmallesttemporalunit +Optional to_smallest_temporal_unit(GlobalObject& global_object, Object& normalized_options, Vector const& disallowed_units, Optional fallback) +{ + auto& vm = global_object.vm(); + + // 1. Assert: disallowedUnits does not contain fallback. + + // 2. Let smallestUnit be ? GetOption(normalizedOptions, "smallestUnit", « String », « "year", "years", "month", "months", "week", "weeks", "day", "days", "hour", "hours", "minute", "minutes", "second", "seconds", "millisecond", "milliseconds", "microsecond", "microseconds", "nanosecond", "nanoseconds" », fallback). + auto smallest_unit_value = get_option(global_object, normalized_options, "smallestUnit"sv, { OptionType::String }, { "year"sv, "years"sv, "month"sv, "months"sv, "week"sv, "weeks"sv, "day"sv, "days"sv, "hour"sv, "hours"sv, "minute"sv, "minutes"sv, "second"sv, "seconds"sv, "millisecond"sv, "milliseconds"sv, "microsecond"sv, "microseconds"sv, "nanosecond"sv, "nanoseconds"sv }, fallback.has_value() ? js_string(vm, *fallback) : js_undefined()); + if (vm.exception()) + return {}; + + // OPTIMIZATION: We skip the following string-only checks for the fallback to tidy up the code a bit + if (smallest_unit_value.is_undefined()) + return {}; + VERIFY(smallest_unit_value.is_string()); + auto smallest_unit = smallest_unit_value.as_string().string(); + + // 3. If smallestUnit is in the Plural column of Table 12, then + if (auto singular_unit = plural_to_singular_units.get(smallest_unit); singular_unit.has_value()) { + // a. Set smallestUnit to the corresponding Singular value of the same row. + smallest_unit = singular_unit.value(); + } + + // 4. If disallowedUnits contains smallestUnit, then + if (disallowed_units.contains_slow(smallest_unit)) { + // a. Throw a RangeError exception. + vm.throw_exception(global_object, ErrorType::OptionIsNotValidValue, smallest_unit, "smallestUnit"); + return {}; + } + + // 5. Return smallestUnit. + return smallest_unit; +} + +// 13.32 RoundNumberToIncrement ( x, increment, roundingMode ) +BigInt* round_number_to_increment(GlobalObject& global_object, BigInt const& x, u64 increment, String const& rounding_mode) +{ + auto& heap = global_object.heap(); + + // 1. Assert: x and increment are mathematical values. + // 2. Assert: roundingMode is "ceil", "floor", "trunc", or "halfExpand". + VERIFY(rounding_mode == "ceil" || rounding_mode == "floor" || rounding_mode == "trunc" || rounding_mode == "halfExpand"); + + // OPTIMIZATION: If the increment is 1 the number is always rounded + if (increment == 1) + return js_bigint(heap, x.big_integer()); + + auto increment_big_int = Crypto::UnsignedBigInteger::create_from(increment); + // 3. Let quotient be x / increment. + auto division_result = x.big_integer().divided_by(increment_big_int); + + // OPTIMIZATION: If theres no remainder there number is already rounded + if (division_result.remainder == Crypto::UnsignedBigInteger { 0 }) + return js_bigint(heap, x.big_integer()); + + Crypto::SignedBigInteger rounded = move(division_result.quotient); + // 4. If roundingMode is "ceil", then + if (rounding_mode == "ceil") { + // a. Let rounded be −floor(−quotient). + if (!division_result.remainder.is_negative()) + rounded = rounded.plus(Crypto::UnsignedBigInteger { 1 }); + } + // 5. Else if roundingMode is "floor", then + else if (rounding_mode == "floor") { + // a. Let rounded be floor(quotient). + if (division_result.remainder.is_negative()) + rounded = rounded.minus(Crypto::UnsignedBigInteger { 1 }); + } + // 6. Else if roundingMode is "trunc", then + else if (rounding_mode == "trunc") { + // a. Let rounded be the integral part of quotient, removing any fractional digits. + // NOTE: This is a no-op + } + // 7. Else, + else { + // a. Let rounded be ! RoundHalfAwayFromZero(quotient). + if (division_result.remainder.multiplied_by(Crypto::UnsignedBigInteger { 2 }).unsigned_value() >= increment_big_int) { + if (division_result.remainder.is_negative()) + rounded = rounded.minus(Crypto::UnsignedBigInteger { 1 }); + else + rounded = rounded.plus(Crypto::UnsignedBigInteger { 1 }); + } + } + + // 8. Return rounded × increment. + return js_bigint(heap, rounded.multiplied_by(increment_big_int)); +} + // 13.34 ParseISODateTime ( isoString ), https://tc39.es/proposal-temporal/#sec-temporal-parseisodatetime Optional parse_iso_date_time(GlobalObject& global_object, [[maybe_unused]] String const& iso_string) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index ca368d344b7..9957a3bc2c7 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -19,7 +19,10 @@ enum class OptionType { Number }; Value get_option(GlobalObject&, Object& options, String const& property, Vector const& types, Vector const& values, Value fallback); - +String to_temporal_rounding_mode(GlobalObject&, Object& normalized_options, String const& fallback); +u64 to_temporal_rounding_increment(GlobalObject&, Object& normalized_options, Optional dividend, bool inclusive); +Optional to_smallest_temporal_unit(GlobalObject&, Object& normalized_options, Vector const& disallowed_units, Optional fallback); +BigInt* round_number_to_increment(GlobalObject&, BigInt const&, u64 increment, String const& rounding_mode); struct ISODateTime { i32 year; i32 month; diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/Instant.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/Instant.cpp index f1f87a13c1d..d4b1c3b6e70 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/Instant.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/Instant.cpp @@ -153,4 +153,45 @@ i32 compare_epoch_nanoseconds(BigInt const& epoch_nanoseconds_one, BigInt const& return 0; } +// 8.5.8 RoundTemporalInstant ( ns, increment, unit, roundingMode ), https://tc39.es/proposal-temporal/#sec-temporal-roundtemporalinstant +BigInt* round_temporal_instant(GlobalObject& global_object, BigInt const& nanoseconds, u64 increment, String const& unit, String const& rounding_mode) +{ + // 1. Assert: Type(ns) is BigInt. + + u64 increment_nanoseconds; + // 2. If unit is "hour", then + if (unit == "hour") { + // a. Let incrementNs be increment × 3.6 × 10^12. + increment_nanoseconds = increment * 3600000000000; + } + // 3. Else if unit is "minute", then + else if (unit == "minute") { + // a. Let incrementNs be increment × 6 × 10^10. + increment_nanoseconds = increment * 60000000000; + } + // 4. Else if unit is "second", then + else if (unit == "second") { + // a. Let incrementNs be increment × 10^9. + increment_nanoseconds = increment * 1000000000; + } + // 5. Else if unit is "millisecond", then + else if (unit == "millisecond") { + // a. Let incrementNs be increment × 10^6. + increment_nanoseconds = increment * 1000000; + } + // 6. Else if unit is "microsecond", then + else if (unit == "microsecond") { + // a. Let incrementNs be increment × 10^3. + increment_nanoseconds = increment * 1000; + } + // 7. Else, + else { + // a. Let incrementNs be increment. + increment_nanoseconds = increment; + } + + // 8. Return ! RoundNumberToIncrement(ℝ(ns), incrementNs, roundingMode). + return round_number_to_increment(global_object, nanoseconds, increment_nanoseconds, rounding_mode); +} + } diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/Instant.h b/Userland/Libraries/LibJS/Runtime/Temporal/Instant.h index 631cea06ef9..e34295946a2 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/Instant.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/Instant.h @@ -41,5 +41,6 @@ Instant* create_temporal_instant(GlobalObject&, BigInt& nanoseconds, FunctionObj Instant* to_temporal_instant(GlobalObject&, Value item); BigInt* parse_temporal_instant(GlobalObject&, String const& iso_string); i32 compare_epoch_nanoseconds(BigInt const&, BigInt const&); +BigInt* round_temporal_instant(GlobalObject&, BigInt const& nanoseconds, u64 increment, String const& unit, String const& rounding_mode); } diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/InstantPrototype.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/InstantPrototype.cpp index abd7a9305b5..0c58563a414 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/InstantPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/InstantPrototype.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -33,6 +34,7 @@ void InstantPrototype::initialize(GlobalObject& global_object) u8 attr = Attribute::Writable | Attribute::Configurable; define_native_function(vm.names.valueOf, value_of, 0, attr); + define_native_function(vm.names.round, round, 1, attr); define_native_function(vm.names.equals, equals, 1, attr); } @@ -122,6 +124,86 @@ JS_DEFINE_NATIVE_FUNCTION(InstantPrototype::epoch_nanoseconds_getter) return &ns; } +// 8.3.11 Temporal.Instant.prototype.round ( options ) +JS_DEFINE_NATIVE_FUNCTION(InstantPrototype::round) +{ + // 1. Let instant be the this value. + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + auto* instant = typed_this(global_object); + if (vm.exception()) + return {}; + + // 3. Set options to ? GetOptionsObject(options). + auto* options = get_options_object(global_object, vm.argument(0)); + if (vm.exception()) + return {}; + + // 4. Let smallestUnit be ? ToSmallestTemporalUnit(options, « "year", "month", "week", "day" », undefined). + auto smallest_unit_value = to_smallest_temporal_unit(global_object, *options, { "year"sv, "month"sv, "week"sv, "day"sv }, {}); + if (vm.exception()) + return {}; + + // 5. If smallestUnit is undefined, throw a RangeError exception. + if (!smallest_unit_value.has_value()) { + vm.throw_exception(global_object, ErrorType::OptionIsNotValidValue, vm.names.undefined.as_string(), "smallestUnit"); + return {}; + } + // At this point smallest_unit_value can only be a string + auto& smallest_unit = *smallest_unit_value; + + // 6. Let roundingMode be ? ToTemporalRoundingMode(options, "halfExpand"). + auto rounding_mode = to_temporal_rounding_mode(global_object, *options, "halfExpand"); + if (vm.exception()) + return {}; + + double maximum; + // 7. If smallestUnit is "hour", then + if (smallest_unit == "hour"sv) { + // a. Let maximum be 24. + maximum = 24; + } + // 8. Else if smallestUnit is "minute", then + else if (smallest_unit == "minute"sv) { + // a. Let maximum be 1440. + maximum = 1440; + } + // 9. Else if smallestUnit is "second", then + else if (smallest_unit == "second"sv) { + // a. Let maximum be 86400. + maximum = 86400; + } + // 10. Else if smallestUnit is "millisecond", then + else if (smallest_unit == "millisecond"sv) { + // a. Let maximum be 8.64 × 10^7. + maximum = 86400000; + } + // 11. Else if smallestUnit is "microsecond", then + else if (smallest_unit == "microsecond"sv) { + // a. Let maximum be 8.64 × 10^10. + maximum = 86400000000; + } + // 12. Else, + else { + // a. Assert: smallestUnit is "nanosecond". + VERIFY(smallest_unit == "nanosecond"sv); + // b. Let maximum be 8.64 × 10^13. + maximum = 86400000000000; + } + + // 13. Let roundingIncrement be ? ToTemporalRoundingIncrement(options, maximum, true). + auto rounding_increment = to_temporal_rounding_increment(global_object, *options, maximum, true); + if (vm.exception()) + return {}; + + // 14. Let roundedNs be ? RoundTemporalInstant(instant.[[Nanoseconds]], roundingIncrement, smallestUnit, roundingMode). + auto* rounded_ns = round_temporal_instant(global_object, instant->nanoseconds(), rounding_increment, smallest_unit, rounding_mode); + if (vm.exception()) + return {}; + + // 15. Return ? CreateTemporalInstant(roundedNs). + return create_temporal_instant(global_object, *rounded_ns); +} + // 8.3.12 Temporal.Instant.prototype.equals ( other ), https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.equals JS_DEFINE_NATIVE_FUNCTION(InstantPrototype::equals) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/InstantPrototype.h b/Userland/Libraries/LibJS/Runtime/Temporal/InstantPrototype.h index f6a69e4346e..c5b9d7e64e5 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/InstantPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/InstantPrototype.h @@ -24,6 +24,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(epoch_microseconds_getter); JS_DECLARE_NATIVE_FUNCTION(epoch_nanoseconds_getter); + JS_DECLARE_NATIVE_FUNCTION(round); JS_DECLARE_NATIVE_FUNCTION(equals); JS_DECLARE_NATIVE_FUNCTION(value_of); }; diff --git a/Userland/Libraries/LibJS/Tests/builtins/Temporal/Instant/Instant.prototype.round.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/Instant/Instant.prototype.round.js new file mode 100644 index 00000000000..20a5b4edbe8 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Temporal/Instant/Instant.prototype.round.js @@ -0,0 +1,62 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const instant = new Temporal.Instant(1111111111111n); + expect(instant.round({ smallestUnit: "second" }).epochNanoseconds).toBe(1111000000000n); + expect( + instant.round({ smallestUnit: "second", roundingMode: "ceil" }).epochNanoseconds + ).toBe(1112000000000n); + expect( + instant.round({ smallestUnit: "minute", roundingIncrement: 30, roundingMode: "floor" }) + .epochNanoseconds + ).toBe(0n); + expect( + instant.round({ + smallestUnit: "minute", + roundingIncrement: 30, + roundingMode: "halfExpand", + }).epochNanoseconds + ).toBe(1800000000000n); + }); +}); + +test("errors", () => { + test("this value must be a Temporal.Instant object", () => { + expect(() => { + Temporal.Instant.prototype.round.call("foo", {}); + }).toThrowWithMessage(TypeError, "Not a Temporal.Instant"); + }); + + test("invalid rounding mode", () => { + expect(() => { + const instant = new Temporal.Instant(1n); + instant.round({ smallestUnit: "second", roundingMode: "serenityOS" }); + }).toThrowWithMessage(RangeError, "is not a valid value for option roundingMode"); + }); + + test("invalid smallest unit", () => { + expect(() => { + const instant = new Temporal.Instant(1n); + instant.round({ smallestUnit: "serenityOS" }); + }).toThrowWithMessage(RangeError, "is not a valid value for option smallestUnit"); + }); + + test("increment may not be NaN", () => { + expect(() => { + const instant = new Temporal.Instant(1n); + instant.round({ smallestUnit: "second", roundingIncrement: NaN }); + }).toThrowWithMessage(RangeError, "is not a valid value for option roundingIncrement"); + }); + + test("increment may smaller than 1 or larger than maximum", () => { + const instant = new Temporal.Instant(1n); + expect(() => { + instant.round({ smallestUnit: "second", roundingIncrement: -1 }); + }).toThrowWithMessage(RangeError, "is not a valid value for option roundingIncrement"); + expect(() => { + instant.round({ smallestUnit: "second", roundingIncrement: 0 }); + }).toThrowWithMessage(RangeError, "is not a valid value for option roundingIncrement"); + expect(() => { + instant.round({ smallestUnit: "second", roundingIncrement: Infinity }); + }).toThrowWithMessage(RangeError, "is not a valid value for option roundingIncrement"); + }); +});