LibJS: Implement Temporal.ZonedDateTime.prototype.add/subtract/equals

This commit is contained in:
Timothy Flynn 2024-11-25 12:34:35 -05:00 committed by Andreas Kling
parent 4ef21614e9
commit 336efa5e3f
Notes: github-actions[bot] 2024-11-26 10:03:47 +00:00
9 changed files with 271 additions and 0 deletions

View file

@ -353,6 +353,48 @@ ThrowCompletionOr<Crypto::SignedBigInteger> get_start_of_day(VM& vm, StringView
TODO();
}
// 11.1.15 TimeZoneEquals ( one, two ), https://tc39.es/proposal-temporal/#sec-temporal-timezoneequals
bool time_zone_equals(StringView one, StringView two)
{
// 1. If one is two, return true.
if (one == two)
return true;
// 2. Let offsetMinutesOne be ! ParseTimeZoneIdentifier(one).[[OffsetMinutes]].
auto offset_minutes_one = parse_time_zone_identifier(one).offset_minutes;
// 3. Let offsetMinutesTwo be ! ParseTimeZoneIdentifier(two).[[OffsetMinutes]].
auto offset_minutes_two = parse_time_zone_identifier(two).offset_minutes;
// 4. If offsetMinutesOne is EMPTY and offsetMinutesTwo is EMPTY, then
if (!offset_minutes_one.has_value() && !offset_minutes_two.has_value()) {
// a. Let recordOne be GetAvailableNamedTimeZoneIdentifier(one).
auto record_one = Intl::get_available_named_time_zone_identifier(one);
// b. Let recordTwo be GetAvailableNamedTimeZoneIdentifier(two).
auto record_two = Intl::get_available_named_time_zone_identifier(two);
// c. If recordOne is not EMPTY and recordTwo is not EMPTY and recordOne.[[PrimaryIdentifier]] is
// recordTwo.[[PrimaryIdentifier]], return true.
if (record_one.has_value() && record_two.has_value()) {
if (record_one->primary_identifier == record_two->primary_identifier)
return true;
}
}
// 5. Else,
else {
// a. If offsetMinutesOne is not EMPTY and offsetMinutesTwo is not EMPTY and offsetMinutesOne = offsetMinutesTwo,
// return true.
if (offset_minutes_one.has_value() && offset_minutes_two.has_value()) {
if (offset_minutes_one == offset_minutes_two)
return true;
}
}
// 6. Return false.
return false;
}
// 11.1.16 ParseTimeZoneIdentifier ( identifier ), https://tc39.es/proposal-temporal/#sec-parsetimezoneidentifier
ThrowCompletionOr<TimeZone> parse_time_zone_identifier(VM& vm, StringView identifier)
{

View file

@ -33,6 +33,7 @@ ThrowCompletionOr<Crypto::SignedBigInteger> get_epoch_nanoseconds_for(VM&, Strin
ThrowCompletionOr<Crypto::SignedBigInteger> disambiguate_possible_epoch_nanoseconds(VM&, Vector<Crypto::SignedBigInteger> possible_epoch_ns, StringView time_zone, ISODateTime const&, Disambiguation);
ThrowCompletionOr<Vector<Crypto::SignedBigInteger>> get_possible_epoch_nanoseconds(VM&, StringView time_zone, ISODateTime const&);
ThrowCompletionOr<Crypto::SignedBigInteger> get_start_of_day(VM&, StringView time_zone, ISODate);
bool time_zone_equals(StringView one, StringView two);
ThrowCompletionOr<TimeZone> parse_time_zone_identifier(VM&, StringView identifier);
TimeZone parse_time_zone_identifier(StringView identifier);
TimeZone parse_time_zone_identifier(ParseResult const&);

View file

@ -524,4 +524,36 @@ ThrowCompletionOr<Crypto::BigFraction> difference_zoned_date_time_with_total(VM&
return TRY(total_relative_duration(vm, difference, nanoseconds2, date_time, time_zone, calendar, unit));
}
// 6.5.10 AddDurationToZonedDateTime ( operation, zonedDateTime, temporalDurationLike, options ), https://tc39.es/proposal-temporal/#sec-temporal-adddurationtozoneddatetime
ThrowCompletionOr<GC::Ref<ZonedDateTime>> add_duration_to_zoned_date_time(VM& vm, ArithmeticOperation operation, ZonedDateTime const& zoned_date_time, Value temporal_duration_like, Value options)
{
// 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 resolvedOptions be ? GetOptionsObject(options).
auto resolved_options = TRY(get_options_object(vm, options));
// 4. Let overflow be ? GetTemporalOverflowOption(resolvedOptions).
auto overflow = TRY(get_temporal_overflow_option(vm, resolved_options));
// 5. Let calendar be zonedDateTime.[[Calendar]].
auto const& calendar = zoned_date_time.calendar();
// 6. Let timeZone be zonedDateTime.[[TimeZone]].
auto const& time_zone = zoned_date_time.time_zone();
// 7. Let internalDuration be ToInternalDurationRecord(duration).
auto internal_duration = to_internal_duration_record(vm, duration);
// 8. Let epochNanoseconds be ? AddZonedDateTime(zonedDateTime.[[EpochNanoseconds]], timeZone, calendar, internalDuration, overflow).
auto epoch_nanoseconds = TRY(add_zoned_date_time(vm, zoned_date_time.epoch_nanoseconds()->big_integer(), time_zone, calendar, internal_duration, overflow));
// 9. Return ! CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar).
return MUST(create_temporal_zoned_date_time(vm, BigInt::create(vm, move(epoch_nanoseconds)), time_zone, calendar));
}
}

View file

@ -56,5 +56,6 @@ ThrowCompletionOr<Crypto::SignedBigInteger> add_zoned_date_time(VM&, Crypto::Sig
ThrowCompletionOr<InternalDuration> difference_zoned_date_time(VM&, Crypto::SignedBigInteger const& nanoseconds1, Crypto::SignedBigInteger const& nanoseconds2, StringView time_zone, StringView calendar, Unit largest_unit);
ThrowCompletionOr<InternalDuration> difference_zoned_date_time_with_rounding(VM&, Crypto::SignedBigInteger const& nanoseconds1, Crypto::SignedBigInteger const& nanoseconds2, StringView time_zone, StringView calendar, Unit largest_unit, u64 rounding_increment, Unit smallest_unit, RoundingMode);
ThrowCompletionOr<Crypto::BigFraction> difference_zoned_date_time_with_total(VM&, Crypto::SignedBigInteger const& nanoseconds1, Crypto::SignedBigInteger const& nanoseconds2, StringView time_zone, StringView calendar, Unit);
ThrowCompletionOr<GC::Ref<ZonedDateTime>> add_duration_to_zoned_date_time(VM&, ArithmeticOperation, ZonedDateTime const&, Value temporal_duration_like, Value options);
}

View file

@ -62,6 +62,9 @@ void ZonedDateTimePrototype::initialize(Realm& realm)
define_native_accessor(realm, vm.names.offset, offset_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);
@ -342,6 +345,56 @@ JS_DEFINE_NATIVE_FUNCTION(ZonedDateTimePrototype::offset_getter)
return PrimitiveString::create(vm, format_utc_offset_nanoseconds(offset_nanoseconds));
}
// 6.3.35 Temporal.ZonedDateTime.prototype.add ( temporalDurationLike [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.zoneddatetime.prototype.add
JS_DEFINE_NATIVE_FUNCTION(ZonedDateTimePrototype::add)
{
auto temporal_duration_like = vm.argument(0);
auto options = vm.argument(1);
// 1. Let zonedDateTime be the this value.
// 2. Perform ? RequireInternalSlot(zonedDateTime, [[InitializedTemporalZonedDateTime]]).
auto zoned_date_time = TRY(typed_this_object(vm));
// 3. Return ? AddDurationToZonedDateTime(ADD, zonedDateTime, temporalDurationLike, options).
return TRY(add_duration_to_zoned_date_time(vm, ArithmeticOperation::Add, zoned_date_time, temporal_duration_like, options));
}
// 6.3.36 Temporal.ZonedDateTime.prototype.subtract ( temporalDurationLike [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.zoneddatetime.prototype.subtract
JS_DEFINE_NATIVE_FUNCTION(ZonedDateTimePrototype::subtract)
{
auto temporal_duration_like = vm.argument(0);
auto options = vm.argument(1);
// 1. Let zonedDateTime be the this value.
// 2. Perform ? RequireInternalSlot(zonedDateTime, [[InitializedTemporalZonedDateTime]]).
auto zoned_date_time = TRY(typed_this_object(vm));
// 3. Return ? AddDurationToZonedDateTime(SUBTRACT, zonedDateTime, temporalDurationLike, options).
return TRY(add_duration_to_zoned_date_time(vm, ArithmeticOperation::Subtract, zoned_date_time, temporal_duration_like, options));
}
// 6.3.40 Temporal.ZonedDateTime.prototype.equals ( other ), https://tc39.es/proposal-temporal/#sec-temporal.zoneddatetime.prototype.equals
JS_DEFINE_NATIVE_FUNCTION(ZonedDateTimePrototype::equals)
{
// 1. Let zonedDateTime be the this value.
// 2. Perform ? RequireInternalSlot(zonedDateTime, [[InitializedTemporalZonedDateTime]]).
auto zoned_date_time = TRY(typed_this_object(vm));
// 3. Set other to ? ToTemporalZonedDateTime(other).
auto other = TRY(to_temporal_zoned_date_time(vm, vm.argument(0)));
// 4. If zonedDateTime.[[EpochNanoseconds]] ≠ other.[[EpochNanoseconds]], return false.
if (zoned_date_time->epoch_nanoseconds()->big_integer() != other->epoch_nanoseconds()->big_integer())
return false;
// 5. If TimeZoneEquals(zonedDateTime.[[TimeZone]], other.[[TimeZone]]) is false, return false.
if (!time_zone_equals(zoned_date_time->time_zone(), other->time_zone()))
return false;
// 6. Return CalendarEquals(zonedDateTime.[[Calendar]], other.[[Calendar]]).
return calendar_equals(zoned_date_time->calendar(), other->calendar());
}
// 6.3.41 Temporal.ZonedDateTime.prototype.toString ( [ options ] ), https://tc39.es/proposal-temporal/#sec-temporal.zoneddatetime.prototype.tostring
JS_DEFINE_NATIVE_FUNCTION(ZonedDateTimePrototype::to_string)
{

View file

@ -51,6 +51,9 @@ private:
JS_DECLARE_NATIVE_FUNCTION(in_leap_year_getter);
JS_DECLARE_NATIVE_FUNCTION(offset_nanoseconds_getter);
JS_DECLARE_NATIVE_FUNCTION(offset_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);

View file

@ -0,0 +1,58 @@
describe("correct behavior", () => {
test("length is 1", () => {
expect(Temporal.ZonedDateTime.prototype.add).toHaveLength(1);
});
test("basic functionality", () => {
const plainDateTime = new Temporal.PlainDateTime(2021, 11, 12, 0, 22, 30, 100, 200, 300);
const zonedDateTime = plainDateTime.toZonedDateTime("UTC");
const dayDuration = new Temporal.Duration(0, 0, 0, 1);
const result = zonedDateTime.add(dayDuration);
expect(zonedDateTime.day).toBe(12);
expect(zonedDateTime.epochNanoseconds).toBe(1636676550100200300n);
expect(result.day).toBe(13);
expect(result.epochNanoseconds).toBe(1636762950100200300n);
});
test("duration-like object", () => {
const plainDateTime = new Temporal.PlainDateTime(2021, 11, 12, 0, 22, 30, 100, 200, 300);
const zonedDateTime = plainDateTime.toZonedDateTime("UTC");
const dayDuration = { days: 1 };
const result = zonedDateTime.add(dayDuration);
expect(zonedDateTime.day).toBe(12);
expect(zonedDateTime.epochNanoseconds).toBe(1636676550100200300n);
expect(result.day).toBe(13);
expect(result.epochNanoseconds).toBe(1636762950100200300n);
});
test("duration string", () => {
const plainDateTime = new Temporal.PlainDateTime(2021, 11, 12, 0, 22, 30, 100, 200, 300);
const zonedDateTime = plainDateTime.toZonedDateTime("UTC");
const dayDuration = "P1D";
const result = zonedDateTime.add(dayDuration);
expect(zonedDateTime.day).toBe(12);
expect(zonedDateTime.epochNanoseconds).toBe(1636676550100200300n);
expect(result.day).toBe(13);
expect(result.epochNanoseconds).toBe(1636762950100200300n);
});
});
describe("errors", () => {
test("this value must be a Temporal.ZonedDateTime object", () => {
expect(() => {
Temporal.ZonedDateTime.prototype.add.call("foo");
}).toThrowWithMessage(TypeError, "Not an object of type Temporal.ZonedDateTime");
});
test("invalid duration-like object", () => {
expect(() => {
new Temporal.ZonedDateTime(1n, "UTC").add({});
}).toThrowWithMessage(TypeError, "Invalid duration-like object");
});
});

View file

@ -0,0 +1,23 @@
describe("correct behavior", () => {
test("length is 1", () => {
expect(Temporal.ZonedDateTime.prototype.equals).toHaveLength(1);
});
test("basic functionality", () => {
const zonedDateTimeOne = new Temporal.ZonedDateTime(1n, "UTC");
const zonedDateTimeTwo = new Temporal.ZonedDateTime(2n, "UTC");
expect(zonedDateTimeOne.equals(zonedDateTimeOne)).toBeTrue();
expect(zonedDateTimeTwo.equals(zonedDateTimeTwo)).toBeTrue();
expect(zonedDateTimeOne.equals(zonedDateTimeTwo)).toBeFalse();
expect(zonedDateTimeTwo.equals(zonedDateTimeOne)).toBeFalse();
});
});
describe("errors", () => {
test("this value must be a Temporal.ZonedDateTime object", () => {
expect(() => {
Temporal.ZonedDateTime.prototype.equals.call("foo");
}).toThrowWithMessage(TypeError, "Not an object of type Temporal.ZonedDateTime");
});
});

View file

@ -0,0 +1,58 @@
describe("correct behavior", () => {
test("length is 1", () => {
expect(Temporal.ZonedDateTime.prototype.subtract).toHaveLength(1);
});
test("basic functionality", () => {
const plainDateTime = new Temporal.PlainDateTime(2021, 11, 12, 0, 22, 30, 100, 200, 300);
const zonedDateTime = plainDateTime.toZonedDateTime("UTC");
const dayDuration = new Temporal.Duration(0, 0, 0, 1);
const result = zonedDateTime.subtract(dayDuration);
expect(zonedDateTime.day).toBe(12);
expect(zonedDateTime.epochNanoseconds).toBe(1636676550100200300n);
expect(result.day).toBe(11);
expect(result.epochNanoseconds).toBe(1636590150100200300n);
});
test("duration-like object", () => {
const plainDateTime = new Temporal.PlainDateTime(2021, 11, 12, 0, 22, 30, 100, 200, 300);
const zonedDateTime = plainDateTime.toZonedDateTime("UTC");
const dayDuration = { days: 1 };
const result = zonedDateTime.subtract(dayDuration);
expect(zonedDateTime.day).toBe(12);
expect(zonedDateTime.epochNanoseconds).toBe(1636676550100200300n);
expect(result.day).toBe(11);
expect(result.epochNanoseconds).toBe(1636590150100200300n);
});
test("duration string", () => {
const plainDateTime = new Temporal.PlainDateTime(2021, 11, 12, 0, 22, 30, 100, 200, 300);
const zonedDateTime = plainDateTime.toZonedDateTime("UTC");
const dayDuration = "P1D";
const result = zonedDateTime.subtract(dayDuration);
expect(zonedDateTime.day).toBe(12);
expect(zonedDateTime.epochNanoseconds).toBe(1636676550100200300n);
expect(result.day).toBe(11);
expect(result.epochNanoseconds).toBe(1636590150100200300n);
});
});
describe("errors", () => {
test("this value must be a Temporal.ZonedDateTime object", () => {
expect(() => {
Temporal.ZonedDateTime.prototype.subtract.call("foo");
}).toThrowWithMessage(TypeError, "Not an object of type Temporal.ZonedDateTime");
});
test("invalid duration-like object", () => {
expect(() => {
new Temporal.ZonedDateTime(1n, "UTC").subtract({});
}).toThrowWithMessage(TypeError, "Invalid duration-like object");
});
});