LibJS: Add Temporal.Instant.prototype.round()

As well as the required Abstract Operations.
This commit is contained in:
Idan Horowitz 2021-07-11 23:49:05 +03:00 committed by Linus Groh
parent 75d1ffea00
commit 84b028bd71
Notes: sideshowbarker 2024-07-18 09:09:56 +09:00
7 changed files with 361 additions and 1 deletions

View file

@ -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)
{

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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)
{

View file

@ -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);
};

View file

@ -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");
});
});