From 1d67f28e72dfa7cc77d83d48d32f055737567fb0 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sun, 24 Nov 2024 16:31:17 -0500 Subject: [PATCH] LibJS: Implement Temporal.Instant.prototype.add/subtract/equals --- Libraries/LibJS/Runtime/Temporal/Instant.cpp | 42 ++++++++++++++ Libraries/LibJS/Runtime/Temporal/Instant.h | 2 + .../Runtime/Temporal/InstantPrototype.cpp | 47 ++++++++++++++++ .../LibJS/Runtime/Temporal/InstantPrototype.h | 3 + .../Temporal/Instant/Instant.prototype.add.js | 56 +++++++++++++++++++ .../Instant/Instant.prototype.equals.js | 20 +++++++ .../Instant/Instant.prototype.subtract.js | 56 +++++++++++++++++++ 7 files changed, 226 insertions(+) create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/Instant/Instant.prototype.add.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/Instant/Instant.prototype.equals.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/Instant/Instant.prototype.subtract.js diff --git a/Libraries/LibJS/Runtime/Temporal/Instant.cpp b/Libraries/LibJS/Runtime/Temporal/Instant.cpp index 3e275428aa0..7731a74bb55 100644 --- a/Libraries/LibJS/Runtime/Temporal/Instant.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Instant.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -164,6 +165,20 @@ i8 compare_epoch_nanoseconds(Crypto::SignedBigInteger const& epoch_nanoseconds_o return 0; } +// 8.5.5 AddInstant ( epochNanoseconds, timeDuration ), https://tc39.es/proposal-temporal/#sec-temporal-addinstant +ThrowCompletionOr add_instant(VM& vm, Crypto::SignedBigInteger const& epoch_nanoseconds, TimeDuration const& time_duration) +{ + // 1. Let result be AddTimeDurationToEpochNanoseconds(timeDuration, epochNanoseconds). + auto result = add_time_duration_to_epoch_nanoseconds(time_duration, epoch_nanoseconds); + + // 2. If IsValidEpochNanoseconds(result) is false, throw a RangeError exception. + if (!is_valid_epoch_nanoseconds(result)) + return vm.throw_completion(ErrorType::TemporalInvalidEpochNanoseconds); + + // 3. Return result. + return result; +} + // 8.5.7 RoundTemporalInstant ( ns, increment, unit, roundingMode ), https://tc39.es/proposal-temporal/#sec-temporal-roundtemporalinstant Crypto::SignedBigInteger round_temporal_instant(Crypto::SignedBigInteger const& nanoseconds, u64 increment, Unit unit, RoundingMode rounding_mode) { @@ -213,4 +228,31 @@ String temporal_instant_to_string(Instant const& instant, Optional t return MUST(String::formatted("{}{}", date_time_string, time_zone_string)); } +// 8.5.10 AddDurationToInstant ( operation, instant, temporalDurationLike ), https://tc39.es/proposal-temporal/#sec-temporal-adddurationtoinstant +ThrowCompletionOr> add_duration_to_instant(VM& vm, ArithmeticOperation operation, Instant const& instant, Value temporal_duration_like) +{ + // 1. Let duration be ? ToTemporalDuration(temporalDurationLike). + auto duration = TRY(to_temporal_duration(vm, temporal_duration_like)); + + // 2. If operation is SUBTRACT, set duration to CreateNegatedTemporalDuration(duration). + if (operation == ArithmeticOperation::Subtract) + duration = create_negated_temporal_duration(vm, duration); + + // 3. Let largestUnit be DefaultTemporalLargestUnit(duration). + auto largest_unit = default_temporal_largest_unit(duration); + + // 4. If TemporalUnitCategory(largestUnit) is DATE, throw a RangeError exception. + if (temporal_unit_category(largest_unit) == UnitCategory::Date) + return vm.throw_completion(ErrorType::TemporalInvalidLargestUnit, temporal_unit_to_string(largest_unit)); + + // 5. Let internalDuration be ToInternalDurationRecordWith24HourDays(duration). + auto internal_duration = to_internal_duration_record_with_24_hour_days(vm, duration); + + // 6. Let ns be ? AddInstant(instant.[[EpochNanoseconds]], internalDuration.[[Time]]). + auto nanoseconds = TRY(add_instant(vm, instant.epoch_nanoseconds()->big_integer(), internal_duration.time)); + + // 7. Return ! CreateTemporalInstant(ns). + return MUST(create_temporal_instant(vm, BigInt::create(vm, move(nanoseconds)))); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/Instant.h b/Libraries/LibJS/Runtime/Temporal/Instant.h index 9e89ed08067..e7264ca229a 100644 --- a/Libraries/LibJS/Runtime/Temporal/Instant.h +++ b/Libraries/LibJS/Runtime/Temporal/Instant.h @@ -60,7 +60,9 @@ bool is_valid_epoch_nanoseconds(Crypto::SignedBigInteger const& epoch_nanosecond ThrowCompletionOr> create_temporal_instant(VM&, BigInt const& epoch_nanoseconds, GC::Ptr new_target = {}); ThrowCompletionOr> to_temporal_instant(VM&, Value item); i8 compare_epoch_nanoseconds(Crypto::SignedBigInteger const& epoch_nanoseconds_one, Crypto::SignedBigInteger const& epoch_nanoseconds_two); +ThrowCompletionOr add_instant(VM&, Crypto::SignedBigInteger const& epoch_nanoseconds, TimeDuration const&); Crypto::SignedBigInteger round_temporal_instant(Crypto::SignedBigInteger const& nanoseconds, u64 increment, Unit, RoundingMode); String temporal_instant_to_string(Instant const&, Optional time_zone, SecondsStringPrecision::Precision); +ThrowCompletionOr> add_duration_to_instant(VM&, ArithmeticOperation, Instant const&, Value temporal_duration_like); } diff --git a/Libraries/LibJS/Runtime/Temporal/InstantPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/InstantPrototype.cpp index 75be138ed1b..185483edadf 100644 --- a/Libraries/LibJS/Runtime/Temporal/InstantPrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/InstantPrototype.cpp @@ -32,6 +32,9 @@ void InstantPrototype::initialize(Realm& realm) define_native_accessor(realm, vm.names.epochNanoseconds, epoch_nanoseconds_getter, {}, Attribute::Configurable); u8 attr = Attribute::Writable | Attribute::Configurable; + 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.equals, equals, 1, attr); define_native_function(realm, vm.names.toString, to_string, 0, attr); define_native_function(realm, vm.names.toLocaleString, to_locale_string, 0, attr); define_native_function(realm, vm.names.toJSON, to_json, 0, attr); @@ -66,6 +69,50 @@ JS_DEFINE_NATIVE_FUNCTION(InstantPrototype::epoch_nanoseconds_getter) return instant->epoch_nanoseconds(); } +// 8.3.5 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 = TRY(typed_this_object(vm)); + + // 3. Return ? AddDurationToInstant(ADD, instant, temporalDurationLike). + return TRY(add_duration_to_instant(vm, ArithmeticOperation::Add, instant, temporal_duration_like)); +} + +// 8.3.6 Temporal.Instant.prototype.subtract ( temporalDurationLike ), https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.subtract +JS_DEFINE_NATIVE_FUNCTION(InstantPrototype::subtract) +{ + auto temporal_duration_like = vm.argument(0); + + // 1. Let instant be the this value. + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + auto instant = TRY(typed_this_object(vm)); + + // 3. Return ? AddDurationToInstant(SUBTRACT, instant, temporalDurationLike). + return TRY(add_duration_to_instant(vm, ArithmeticOperation::Subtract, instant, temporal_duration_like)); +} + +// 8.3.10 Temporal.Instant.prototype.equals ( other ), https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.equals +JS_DEFINE_NATIVE_FUNCTION(InstantPrototype::equals) +{ + // 1. Let instant be the this value. + // 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]). + auto instant = TRY(typed_this_object(vm)); + + // 3. Set other to ? ToTemporalInstant(other). + auto other = TRY(to_temporal_instant(vm, vm.argument(0))); + + // 4. If instant.[[EpochNanoseconds]] ≠ other.[[EpochNanoseconds]], return false. + if (instant->epoch_nanoseconds()->big_integer() != other->epoch_nanoseconds()->big_integer()) + return false; + + // 5. Return true. + return true; +} + // 8.3.11 Temporal.Instant.prototype.toString ( [ options ] ), https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.tostring JS_DEFINE_NATIVE_FUNCTION(InstantPrototype::to_string) { diff --git a/Libraries/LibJS/Runtime/Temporal/InstantPrototype.h b/Libraries/LibJS/Runtime/Temporal/InstantPrototype.h index 5ddcc620037..a1283b04a75 100644 --- a/Libraries/LibJS/Runtime/Temporal/InstantPrototype.h +++ b/Libraries/LibJS/Runtime/Temporal/InstantPrototype.h @@ -25,6 +25,9 @@ private: JS_DECLARE_NATIVE_FUNCTION(epoch_milliseconds_getter); JS_DECLARE_NATIVE_FUNCTION(epoch_nanoseconds_getter); + JS_DECLARE_NATIVE_FUNCTION(add); + JS_DECLARE_NATIVE_FUNCTION(subtract); + JS_DECLARE_NATIVE_FUNCTION(equals); JS_DECLARE_NATIVE_FUNCTION(to_string); JS_DECLARE_NATIVE_FUNCTION(to_locale_string); JS_DECLARE_NATIVE_FUNCTION(to_json); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Instant/Instant.prototype.add.js b/Libraries/LibJS/Tests/builtins/Temporal/Instant/Instant.prototype.add.js new file mode 100644 index 00000000000..707767c03d4 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Instant/Instant.prototype.add.js @@ -0,0 +1,56 @@ +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 an object of type 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], "year"], + [[0, 123, 0, 0], "month"], + [[0, 0, 123, 0], "week"], + [[0, 0, 0, 123], "day"], + ]) { + const duration = new Temporal.Duration(...args); + expect(() => { + instant.add(duration); + }).toThrowWithMessage(RangeError, `Largest unit must not be ${property}`); + } + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Instant/Instant.prototype.equals.js b/Libraries/LibJS/Tests/builtins/Temporal/Instant/Instant.prototype.equals.js new file mode 100644 index 00000000000..a1bfc7f4ced --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Instant/Instant.prototype.equals.js @@ -0,0 +1,20 @@ +describe("correct behavior", () => { + test("length is 1", () => { + expect(Temporal.Instant.prototype.equals).toHaveLength(1); + }); + + test("basic functionality", () => { + const instant1 = new Temporal.Instant(111n); + expect(instant1.equals(instant1)); + const instant2 = new Temporal.Instant(999n); + expect(!instant1.equals(instant2)); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.Instant object", () => { + expect(() => { + Temporal.Instant.prototype.equals.call("foo", 1, 2); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.Instant"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/Instant/Instant.prototype.subtract.js b/Libraries/LibJS/Tests/builtins/Temporal/Instant/Instant.prototype.subtract.js new file mode 100644 index 00000000000..cc08822d630 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/Instant/Instant.prototype.subtract.js @@ -0,0 +1,56 @@ +describe("correct behavior", () => { + test("length is 1", () => { + expect(Temporal.Instant.prototype.subtract).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.subtract(duration).epochNanoseconds).toBe(1625611197995994994n); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.Instant object", () => { + expect(() => { + Temporal.Instant.prototype.subtract.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type 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.subtract(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.subtract(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], "year"], + [[0, 123, 0, 0], "month"], + [[0, 0, 123, 0], "week"], + [[0, 0, 0, 123], "day"], + ]) { + const duration = new Temporal.Duration(...args); + expect(() => { + instant.subtract(duration); + }).toThrowWithMessage(RangeError, `Largest unit must not be ${property}`); + } + }); +});