LibJS: Implement Temporal.Instant.prototype.add()

This commit is contained in:
Linus Groh 2021-08-07 00:31:08 +01:00
parent 8ffad70504
commit b38f1fb071
Notes: sideshowbarker 2024-07-18 07:19:08 +09:00
8 changed files with 173 additions and 3 deletions

View file

@ -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") \

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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