diff options
author | Linus Groh <mail@linusgroh.de> | 2021-08-07 00:31:08 +0100 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2021-08-07 13:10:35 +0100 |
commit | b38f1fb0718cd3d9f58854064b0c538b27aa4a5a (patch) | |
tree | fe6ac4cbd6152c7993684fb494802609740a98a2 /Userland/Libraries | |
parent | 8ffad70504a2d882b28137a7cf6c27571173314b (diff) | |
download | serenity-b38f1fb0718cd3d9f58854064b0c538b27aa4a5a.zip |
LibJS: Implement Temporal.Instant.prototype.add()
Diffstat (limited to 'Userland/Libraries')
8 files changed, 173 insertions, 3 deletions
diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 6086108a80..f704f8fbc5 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -171,7 +171,8 @@ M(TemporalInvalidCalendarIdentifier, "Invalid calendar identifier '{}'") \ M(TemporalInvalidDuration, "Invalid duration") \ M(TemporalInvalidDurationLikeObject, "Invalid duration-like object") \ - M(TemporalInvalidDurationPropertyValue, "Invalid value for duration property '{}': must be an integer, got {}") \ + M(TemporalInvalidDurationPropertyValueNonIntegral, "Invalid value for duration property '{}': must be an integer, got {}") \ + M(TemporalInvalidDurationPropertyValueNonZero, "Invalid value for duration property '{}': must be zero, got {}") \ M(TemporalInvalidEpochNanoseconds, "Invalid epoch nanoseconds value, must be in range -86400 * 10^17 to 86400 * 10^17") \ M(TemporalInvalidISODate, "Invalid ISO date") \ M(TemporalInvalidMonthCode, "Invalid month code") \ diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp index 5d5cec2d0c..2a84daa75f 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/Duration.cpp @@ -118,7 +118,7 @@ TemporalDuration to_temporal_duration_record(GlobalObject& global_object, Object // e. If floor(val) ≠ val, then if (floor(value.as_double()) != value.as_double()) { // i. Throw a RangeError exception. - vm.throw_exception<RangeError>(global_object, ErrorType::TemporalInvalidDurationPropertyValue, property.as_string(), value.to_string_without_side_effects()); + vm.throw_exception<RangeError>(global_object, ErrorType::TemporalInvalidDurationPropertyValueNonIntegral, property.as_string(), value.to_string_without_side_effects()); return {}; } @@ -266,4 +266,56 @@ Duration* create_temporal_duration(GlobalObject& global_object, double years, do return object; } +// 7.5.19 ToLimitedTemporalDuration ( temporalDurationLike, disallowedFields ),https://tc39.es/proposal-temporal/#sec-temporal-tolimitedtemporalduration +Optional<TemporalDuration> to_limited_temporal_duration(GlobalObject& global_object, Value temporal_duration_like, Vector<StringView> const& disallowed_fields) +{ + auto& vm = global_object.vm(); + + Optional<TemporalDuration> duration; + + // 1. If Type(temporalDurationLike) is not Object, then + if (!temporal_duration_like.is_object()) { + // a. Let str be ? ToString(temporalDurationLike). + auto str = temporal_duration_like.to_string(global_object); + if (vm.exception()) + return {}; + + // b. Let duration be ? ParseTemporalDurationString(str). + duration = parse_temporal_duration_string(global_object, str); + if (vm.exception()) + return {}; + } + // 2. Else, + else { + // a. Let duration be ? ToTemporalDurationRecord(temporalDurationLike). + duration = to_temporal_duration_record(global_object, temporal_duration_like.as_object()); + if (vm.exception()) + return {}; + } + + // 3. If ! IsValidDuration(duration.[[Years]], duration.[[Months]], duration.[[Weeks]], duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]]) is false, throw a RangeError exception. + if (!is_valid_duration(duration->years, duration->months, duration->weeks, duration->days, duration->hours, duration->minutes, duration->seconds, duration->milliseconds, duration->microseconds, duration->nanoseconds)) { + vm.throw_exception<RangeError>(global_object, ErrorType::TemporalInvalidDuration); + return {}; + } + + // 4. For each row of Table 7, except the header row, in table order, do + for (auto& [internal_slot, property] : temporal_duration_like_properties<TemporalDuration, double>(vm)) { + // a. Let prop be the Property value of the current row. + + // b. Let value be duration's internal slot whose name is the Internal Slot value of the current row. + auto value = (*duration).*internal_slot; + + // If value is not 0 and disallowedFields contains prop, then + if (value != 0 && disallowed_fields.contains_slow(property.as_string())) { + // i. Throw a RangeError exception. + vm.throw_exception<RangeError>(global_object, ErrorType::TemporalInvalidDurationPropertyValueNonZero, property.as_string(), value); + return {}; + } + } + + // 5. Return duration. + return duration; +} + } diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/Duration.h b/Userland/Libraries/LibJS/Runtime/Temporal/Duration.h index ac3489a8bf..e5e9e094fc 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/Duration.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/Duration.h @@ -102,5 +102,6 @@ i8 duration_sign(double years, double months, double weeks, double days, double bool is_valid_duration(double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds); PartialDuration to_partial_duration(GlobalObject&, Value temporal_duration_like); Duration* create_temporal_duration(GlobalObject&, double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds, FunctionObject* new_target = nullptr); +Optional<TemporalDuration> to_limited_temporal_duration(GlobalObject&, Value temporal_duration_like, Vector<StringView> const& disallowed_fields); } diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/Instant.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/Instant.cpp index 02f88828de..834d3a7cbe 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/Instant.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/Instant.cpp @@ -160,6 +160,35 @@ i32 compare_epoch_nanoseconds(BigInt const& epoch_nanoseconds_one, BigInt const& return 0; } +// 8.5.6 AddInstant ( epochNanoseconds, hours, minutes, seconds, milliseconds, microseconds, nanoseconds ), https://tc39.es/proposal-temporal/#sec-temporal-addinstant +BigInt* add_instant(GlobalObject& global_object, BigInt const& epoch_nanoseconds, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds) +{ + auto& vm = global_object.vm(); + + // 1. Assert: hours, minutes, seconds, milliseconds, microseconds, and nanoseconds are integer Number values. + VERIFY(hours == trunc(hours) && minutes == trunc(minutes) && seconds == trunc(seconds) && milliseconds == trunc(milliseconds) && microseconds == trunc(microseconds) && nanoseconds == trunc(nanoseconds)); + + // 2. Let result be epochNanoseconds + ℤ(nanoseconds) + ℤ(microseconds) × 1000ℤ + ℤ(milliseconds) × 10^6ℤ + ℤ(seconds) × 10^9ℤ + ℤ(minutes) × 60ℤ × 10^9ℤ + ℤ(hours) × 3600ℤ × 10^9ℤ. + // FIXME: Pretty sure i64's are not sufficient for the extreme cases. + auto* result = js_bigint(vm, + epoch_nanoseconds.big_integer() + .plus(Crypto::SignedBigInteger::create_from((i64)nanoseconds)) + .plus(Crypto::SignedBigInteger::create_from((i64)microseconds).multiplied_by(Crypto::SignedBigInteger { 1'000 })) + .plus(Crypto::SignedBigInteger::create_from((i64)milliseconds).multiplied_by(Crypto::SignedBigInteger { 1'000'000 })) + .plus(Crypto::SignedBigInteger::create_from((i64)seconds).multiplied_by(Crypto::SignedBigInteger { 1'000'000'000 })) + .plus(Crypto::SignedBigInteger::create_from((i64)minutes).multiplied_by(Crypto::SignedBigInteger { 60 }).multiplied_by(Crypto::SignedBigInteger { 1'000'000'000 })) + .plus(Crypto::SignedBigInteger::create_from((i64)hours).multiplied_by(Crypto::SignedBigInteger { 3600 }).multiplied_by(Crypto::SignedBigInteger { 1'000'000'000 }))); + + // If ! IsValidEpochNanoseconds(result) is false, throw a RangeError exception. + if (!is_valid_epoch_nanoseconds(*result)) { + vm.throw_exception<RangeError>(global_object, ErrorType::TemporalInvalidEpochNanoseconds); + return {}; + } + + // 4. Return result. + return result; +} + // 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) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/Instant.h b/Userland/Libraries/LibJS/Runtime/Temporal/Instant.h index 65a6f252a5..2ea0efafc2 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/Instant.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/Instant.h @@ -39,6 +39,7 @@ 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* add_instant(GlobalObject&, BigInt const& epoch_nanoseconds, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds); 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 e395710f43..d749a8182c 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/InstantPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/InstantPrototype.cpp @@ -8,6 +8,7 @@ #include <LibCrypto/BigInt/UnsignedBigInteger.h> #include <LibJS/Runtime/GlobalObject.h> #include <LibJS/Runtime/Temporal/AbstractOperations.h> +#include <LibJS/Runtime/Temporal/Duration.h> #include <LibJS/Runtime/Temporal/Instant.h> #include <LibJS/Runtime/Temporal/InstantPrototype.h> @@ -34,6 +35,7 @@ void InstantPrototype::initialize(GlobalObject& global_object) define_native_accessor(vm.names.epochNanoseconds, epoch_nanoseconds_getter, {}, Attribute::Configurable); u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(vm.names.add, add, 1, attr); define_native_function(vm.names.round, round, 1, attr); define_native_function(vm.names.equals, equals, 1, attr); define_native_function(vm.names.valueOf, value_of, 0, attr); @@ -125,6 +127,31 @@ JS_DEFINE_NATIVE_FUNCTION(InstantPrototype::epoch_nanoseconds_getter) return &ns; } +// 8.3.7 Temporal.Instant.prototype.add ( temporalDurationLike ), https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.add +JS_DEFINE_NATIVE_FUNCTION(InstantPrototype::add) +{ + auto temporal_duration_like = vm.argument(0); + + // 1. Let instant be the this value. + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + auto* instant = typed_this(global_object); + if (vm.exception()) + return {}; + + // 3. Let duration be ? ToLimitedTemporalDuration(temporalDurationLike, « "years", "months", "weeks", "days" »). + auto duration = to_limited_temporal_duration(global_object, temporal_duration_like, { "years"sv, "months"sv, "weeks"sv, "days"sv }); + if (vm.exception()) + return {}; + + // 4. Let ns be ? AddInstant(instant.[[Nanoseconds]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]]). + auto* ns = add_instant(global_object, instant->nanoseconds(), duration->hours, duration->minutes, duration->seconds, duration->milliseconds, duration->microseconds, duration->nanoseconds); + if (vm.exception()) + return {}; + + // 5. Return ! CreateTemporalInstant(ns). + return create_temporal_instant(global_object, *ns); +} + // 8.3.11 Temporal.Instant.prototype.round ( options ), https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.round JS_DEFINE_NATIVE_FUNCTION(InstantPrototype::round) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/InstantPrototype.h b/Userland/Libraries/LibJS/Runtime/Temporal/InstantPrototype.h index c5b9d7e64e..55360e6dd6 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/InstantPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/InstantPrototype.h @@ -23,7 +23,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(epoch_milliseconds_getter); JS_DECLARE_NATIVE_FUNCTION(epoch_microseconds_getter); JS_DECLARE_NATIVE_FUNCTION(epoch_nanoseconds_getter); - + JS_DECLARE_NATIVE_FUNCTION(add); 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.add.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/Instant/Instant.prototype.add.js new file mode 100644 index 0000000000..015c94dc6b --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Temporal/Instant/Instant.prototype.add.js @@ -0,0 +1,59 @@ +describe("correct behavior", () => { + test("length is 1", () => { + expect(Temporal.Instant.prototype.add).toHaveLength(1); + }); + + test("basic functionality", () => { + const instant = new Temporal.Instant(1625614921000000000n); + const duration = new Temporal.Duration(0, 0, 0, 0, 1, 2, 3, 4, 5, 6); + expect(instant.add(duration).epochNanoseconds).toBe(1625618644004005006n); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.Instant object", () => { + expect(() => { + Temporal.Instant.prototype.add.call("foo"); + }).toThrowWithMessage(TypeError, "Not a Temporal.Instant"); + }); + + test("invalid nanoseconds value, positive", () => { + const instant = new Temporal.Instant(8_640_000_000_000_000_000_000n); + const duration = new Temporal.Duration(0, 0, 0, 0, 0, 0, 0, 0, 0, 1); + expect(() => { + instant.add(duration); + }).toThrowWithMessage( + RangeError, + "Invalid epoch nanoseconds value, must be in range -86400 * 10^17 to 86400 * 10^17" + ); + }); + + test("invalid nanoseconds value, negative", () => { + const instant = new Temporal.Instant(-8_640_000_000_000_000_000_000n); + const duration = new Temporal.Duration(0, 0, 0, 0, 0, 0, 0, 0, 0, -1); + expect(() => { + instant.add(duration); + }).toThrowWithMessage( + RangeError, + "Invalid epoch nanoseconds value, must be in range -86400 * 10^17 to 86400 * 10^17" + ); + }); + + test("disallowed fields", () => { + const instant = new Temporal.Instant(1625614921000000000n); + for (const [args, property] of [ + [[123, 0, 0, 0], "years"], + [[0, 123, 0, 0], "months"], + [[0, 0, 123, 0], "weeks"], + [[0, 0, 0, 123], "days"], + ]) { + const duration = new Temporal.Duration(...args); + expect(() => { + instant.add(duration); + }).toThrowWithMessage( + RangeError, + `Invalid value for duration property '${property}': must be zero, got 123` + ); + } + }); +}); |