mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-22 07:30:19 +00:00
LibJS: Add Temporal.Instant.prototype.round()
As well as the required Abstract Operations.
This commit is contained in:
parent
75d1ffea00
commit
84b028bd71
Notes:
sideshowbarker
2024-07-18 09:09:56 +09:00
Author: https://github.com/IdanHo Commit: https://github.com/SerenityOS/serenity/commit/84b028bd71b Pull-request: https://github.com/SerenityOS/serenity/pull/8663 Reviewed-by: https://github.com/Hendiadyoin1 Reviewed-by: https://github.com/linusg
7 changed files with 361 additions and 1 deletions
|
@ -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<double> 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<RangeError>(global_object, ErrorType::OptionIsNotValidValue, increment, "roundingIncrement");
|
||||
return {};
|
||||
}
|
||||
|
||||
// 7. Set increment to floor(ℝ(increment)).
|
||||
auto floored_increment = static_cast<u64>(increment);
|
||||
|
||||
// 8. If dividend is not undefined and dividend modulo increment is not zero, then
|
||||
if (dividend.has_value() && static_cast<u64>(*dividend) % floored_increment != 0) {
|
||||
// a. Throw a RangeError exception.
|
||||
vm.throw_exception<RangeError>(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<StringView, StringView> 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<String> to_smallest_temporal_unit(GlobalObject& global_object, Object& normalized_options, Vector<StringView> const& disallowed_units, Optional<String> 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<RangeError>(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<ISODateTime> parse_iso_date_time(GlobalObject& global_object, [[maybe_unused]] String const& iso_string)
|
||||
{
|
||||
|
|
|
@ -19,7 +19,10 @@ enum class OptionType {
|
|||
Number
|
||||
};
|
||||
Value get_option(GlobalObject&, Object& options, String const& property, Vector<OptionType> const& types, Vector<StringView> 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<double> dividend, bool inclusive);
|
||||
Optional<String> to_smallest_temporal_unit(GlobalObject&, Object& normalized_options, Vector<StringView> const& disallowed_units, Optional<String> fallback);
|
||||
BigInt* round_number_to_increment(GlobalObject&, BigInt const&, u64 increment, String const& rounding_mode);
|
||||
struct ISODateTime {
|
||||
i32 year;
|
||||
i32 month;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#include <LibCrypto/BigInt/UnsignedBigInteger.h>
|
||||
#include <LibJS/Runtime/GlobalObject.h>
|
||||
#include <LibJS/Runtime/Temporal/AbstractOperations.h>
|
||||
#include <LibJS/Runtime/Temporal/Instant.h>
|
||||
#include <LibJS/Runtime/Temporal/InstantPrototype.h>
|
||||
|
||||
|
@ -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<RangeError>(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)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue