diff --git a/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp b/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp index fb9e58159bc..e451d31d859 100644 --- a/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp +++ b/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp @@ -353,6 +353,48 @@ ThrowCompletionOr 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 parse_time_zone_identifier(VM& vm, StringView identifier) { diff --git a/Libraries/LibJS/Runtime/Temporal/TimeZone.h b/Libraries/LibJS/Runtime/Temporal/TimeZone.h index 1952ca05954..45584334140 100644 --- a/Libraries/LibJS/Runtime/Temporal/TimeZone.h +++ b/Libraries/LibJS/Runtime/Temporal/TimeZone.h @@ -33,6 +33,7 @@ ThrowCompletionOr get_epoch_nanoseconds_for(VM&, Strin ThrowCompletionOr disambiguate_possible_epoch_nanoseconds(VM&, Vector possible_epoch_ns, StringView time_zone, ISODateTime const&, Disambiguation); ThrowCompletionOr> get_possible_epoch_nanoseconds(VM&, StringView time_zone, ISODateTime const&); ThrowCompletionOr get_start_of_day(VM&, StringView time_zone, ISODate); +bool time_zone_equals(StringView one, StringView two); ThrowCompletionOr parse_time_zone_identifier(VM&, StringView identifier); TimeZone parse_time_zone_identifier(StringView identifier); TimeZone parse_time_zone_identifier(ParseResult const&); diff --git a/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.cpp b/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.cpp index c2394f406b5..54a60906661 100644 --- a/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.cpp +++ b/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.cpp @@ -524,4 +524,36 @@ ThrowCompletionOr 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> 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)); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.h b/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.h index 27370714e09..0a692bdbac5 100644 --- a/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.h +++ b/Libraries/LibJS/Runtime/Temporal/ZonedDateTime.h @@ -56,5 +56,6 @@ ThrowCompletionOr add_zoned_date_time(VM&, Crypto::Sig ThrowCompletionOr difference_zoned_date_time(VM&, Crypto::SignedBigInteger const& nanoseconds1, Crypto::SignedBigInteger const& nanoseconds2, StringView time_zone, StringView calendar, Unit largest_unit); ThrowCompletionOr 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 difference_zoned_date_time_with_total(VM&, Crypto::SignedBigInteger const& nanoseconds1, Crypto::SignedBigInteger const& nanoseconds2, StringView time_zone, StringView calendar, Unit); +ThrowCompletionOr> add_duration_to_zoned_date_time(VM&, ArithmeticOperation, ZonedDateTime const&, Value temporal_duration_like, Value options); } diff --git a/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.cpp b/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.cpp index c06b9f10b7f..8ddead5b57d 100644 --- a/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.cpp @@ -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) { diff --git a/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.h b/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.h index a47c32c28c8..6c1fc6a681c 100644 --- a/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.h +++ b/Libraries/LibJS/Runtime/Temporal/ZonedDateTimePrototype.h @@ -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); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.add.js b/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.add.js new file mode 100644 index 00000000000..7d0ca714de9 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.add.js @@ -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"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.equals.js b/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.equals.js new file mode 100644 index 00000000000..e3edb0f1698 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.equals.js @@ -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"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.subtract.js b/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.subtract.js new file mode 100644 index 00000000000..e037177bc0b --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/ZonedDateTime/ZonedDateTime.prototype.subtract.js @@ -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"); + }); +});