diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index e41c31f82e6..e838f97d469 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include namespace JS::Temporal { @@ -451,6 +452,40 @@ Crypto::UnsignedBigInteger const& temporal_unit_length_in_nanoseconds(Unit unit) } } +// 13.23 IsPartialTemporalObject ( value ), https://tc39.es/proposal-temporal/#sec-temporal-ispartialtemporalobject +ThrowCompletionOr is_partial_temporal_object(VM& vm, Value value) +{ + // 1. If value is not an Object, return false. + if (!value.is_object()) + return false; + + auto const& object = value.as_object(); + + // 2. If value has an [[InitializedTemporalDate]], [[InitializedTemporalDateTime]], [[InitializedTemporalMonthDay]], + // [[InitializedTemporalTime]], [[InitializedTemporalYearMonth]], or [[InitializedTemporalZonedDateTime]] internal + // slot, return false. + // FIXME: Add the other types as we define them. + if (is(object)) + return false; + + // 3. Let calendarProperty be ? Get(value, "calendar"). + auto calendar_property = TRY(object.get(vm.names.calendar)); + + // 4. If calendarProperty is not undefined, return false. + if (!calendar_property.is_undefined()) + return false; + + // 5. Let timeZoneProperty be ? Get(value, "timeZone"). + auto time_zone_property = TRY(object.get(vm.names.timeZone)); + + // 6. If timeZoneProperty is not undefined, return false. + if (!time_zone_property.is_undefined()) + return false; + + // 7. Return true. + return true; +} + // 13.24 FormatFractionalSeconds ( subSecondNanoseconds, precision ), https://tc39.es/proposal-temporal/#sec-temporal-formatfractionalseconds String format_fractional_seconds(u64 sub_second_nanoseconds, Precision precision) { diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index 16a1df2aaca..f31a269e55f 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -162,6 +162,7 @@ bool is_calendar_unit(Unit); UnitCategory temporal_unit_category(Unit); RoundingIncrement maximum_temporal_duration_rounding_increment(Unit); Crypto::UnsignedBigInteger const& temporal_unit_length_in_nanoseconds(Unit); +ThrowCompletionOr is_partial_temporal_object(VM&, Value); String format_fractional_seconds(u64, Precision); String format_time_string(u8 hour, u8 minute, u8 second, u16 sub_second_nanoseconds, SecondsStringPrecision::Precision, Optional = {}); UnsignedRoundingMode get_unsigned_rounding_mode(RoundingMode, Sign); diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.cpp b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp index fcb5a6a4cfe..529e588bc93 100644 --- a/Libraries/LibJS/Runtime/Temporal/Calendar.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp @@ -243,6 +243,73 @@ ThrowCompletionOr prepare_calendar_fields(VM& vm, StringView cal return result; } +// 12.2.4 CalendarFieldKeysPresent ( fields ), https://tc39.es/proposal-temporal/#sec-temporal-calendarfieldkeyspresent +Vector calendar_field_keys_present(CalendarFields const& fields) +{ + // 1. Let list be « ». + Vector list; + + auto handle_field = [&](auto enumeration_key, auto const& value) { + // a. Let value be fields' field whose name is given in the Field Name column of the row. + // b. Let enumerationKey be the value in the Enumeration Key column of the row. + // c. If value is not unset, append enumerationKey to list. + if (value.has_value()) + list.append(enumeration_key); + }; + + // 2. For each row of Table 19, except the header row, do +#define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \ + handle_field(enumeration, fields.field_name); + JS_ENUMERATE_CALENDAR_FIELDS +#undef __JS_ENUMERATE + + // 3. Return list. + return list; +} + +// 12.2.5 CalendarMergeFields ( calendar, fields, additionalFields ), https://tc39.es/proposal-temporal/#sec-temporal-calendarmergefields +CalendarFields calendar_merge_fields(StringView calendar, CalendarFields const& fields, CalendarFields const& additional_fields) +{ + // 1. Let additionalKeys be CalendarFieldKeysPresent(additionalFields). + auto additional_keys = calendar_field_keys_present(additional_fields); + + // 2. Let overriddenKeys be CalendarFieldKeysToIgnore(calendar, additionalKeys). + auto overridden_keys = calendar_field_keys_to_ignore(calendar, additional_keys); + + // 3. Let merged be a Calendar Fields Record with all fields set to unset. + auto merged = CalendarFields::unset(); + + // 4. Let fieldsKeys be CalendarFieldKeysPresent(fields). + auto fields_keys = calendar_field_keys_present(fields); + + auto merge_field = [&](auto key, auto& merged_field, auto const& fields_field, auto const& additional_fields_field) { + // a. Let key be the value in the Enumeration Key column of the row. + + // b. If fieldsKeys contains key and overriddenKeys does not contain key, then + if (fields_keys.contains_slow(key) && !overridden_keys.contains_slow(key)) { + // i. Let propValue be fields' field whose name is given in the Field Name column of the row. + // ii. Set merged's field whose name is given in the Field Name column of the row to propValue. + merged_field = fields_field; + } + + // c. If additionalKeys contains key, then + if (additional_keys.contains_slow(key)) { + // i. Let propValue be additionalFields' field whose name is given in the Field Name column of the row. + // ii. Set merged's field whose name is given in the Field Name column of the row to propValue. + merged_field = additional_fields_field; + } + }; + + // 5. For each row of Table 19, except the header row, do +#define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \ + merge_field(enumeration, merged.field_name, fields.field_name, additional_fields.field_name); + JS_ENUMERATE_CALENDAR_FIELDS +#undef __JS_ENUMERATE + + // 6. Return merged. + return merged; +} + // 12.2.8 ToTemporalCalendarIdentifier ( temporalCalendarLike ), https://tc39.es/proposal-temporal/#sec-temporal-totemporalcalendaridentifier ThrowCompletionOr to_temporal_calendar_identifier(VM& vm, Value temporal_calendar_like) { @@ -328,6 +395,15 @@ String format_calendar_annotation(StringView id, ShowCalendar show_calendar) return MUST(String::formatted("[{}u-ca={}]", flag, id)); } +// 12.2.14 CalendarEquals ( one, two ), https://tc39.es/proposal-temporal/#sec-temporal-calendarequals +bool calendar_equals(StringView one, StringView two) +{ + // 1. If CanonicalizeUValue("ca", one) is CanonicalizeUValue("ca", two), return true. + // 2. Return false. + return Unicode::canonicalize_unicode_extension_values("ca"sv, one) + == Unicode::canonicalize_unicode_extension_values("ca"sv, two); +} + // 12.2.15 ISODaysInMonth ( year, month ), https://tc39.es/proposal-temporal/#sec-temporal-isodaysinmonth u8 iso_days_in_month(double year, double month) { @@ -532,6 +608,39 @@ Vector calendar_extra_fields(StringView calendar, CalendarFieldLi return {}; } +// 12.2.23 CalendarFieldKeysToIgnore ( calendar, keys ), https://tc39.es/proposal-temporal/#sec-temporal-calendarfieldkeystoignore +Vector calendar_field_keys_to_ignore(StringView calendar, ReadonlySpan keys) +{ + // 1. If calendar is "iso8601", then + if (calendar == "iso8601"sv) { + // a. Let ignoredKeys be an empty List. + Vector ignored_keys; + + // b. For each element key of keys, do + for (auto key : keys) { + // i. Append key to ignoredKeys. + ignored_keys.append(key); + + // ii. If key is MONTH, append MONTH-CODE to ignoredKeys. + if (key == CalendarField::Month) + ignored_keys.append(CalendarField::MonthCode); + // iii. Else if key is MONTH-CODE, append MONTH to ignoredKeys. + else if (key == CalendarField::MonthCode) + ignored_keys.append(CalendarField::Month); + } + + // c. NOTE: While ignoredKeys can have duplicate elements, this is not intended to be meaningful. This specification + // only checks whether particular keys are or are not members of the list. + + // d. Return ignoredKeys. + return ignored_keys; + } + + // 2. Return an implementation-defined List as described below. + // FIXME: Return keys for an ISO8601 calendar for now. + return calendar_field_keys_to_ignore("iso8601"sv, keys); +} + // 12.2.24 CalendarResolveFields ( calendar, fields, type ), https://tc39.es/proposal-temporal/#sec-temporal-calendarresolvefields ThrowCompletionOr calendar_resolve_fields(VM& vm, StringView calendar, CalendarFields& fields, DateType type) { diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.h b/Libraries/LibJS/Runtime/Temporal/Calendar.h index c54b5b22d29..b7a620ce354 100644 --- a/Libraries/LibJS/Runtime/Temporal/Calendar.h +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.h @@ -101,15 +101,19 @@ Vector const& available_calendars(); ThrowCompletionOr prepare_calendar_fields(VM&, StringView calendar, Object const& fields, CalendarFieldList calendar_field_names, CalendarFieldList non_calendar_field_names, CalendarFieldListOrPartial required_field_names); ThrowCompletionOr calendar_month_day_from_fields(VM&, StringView calendar, CalendarFields, Overflow); String format_calendar_annotation(StringView id, ShowCalendar); +bool calendar_equals(StringView one, StringView two); u8 iso_days_in_month(double year, double month); YearWeek iso_week_of_year(ISODate const&); u16 iso_day_of_year(ISODate const&); u8 iso_day_of_week(ISODate const&); +Vector calendar_field_keys_present(CalendarFields const&); +CalendarFields calendar_merge_fields(StringView calendar, CalendarFields const& fields, CalendarFields const& additional_fields); ThrowCompletionOr to_temporal_calendar_identifier(VM&, Value temporal_calendar_like); ThrowCompletionOr get_temporal_calendar_identifier_with_iso_default(VM&, Object const& item); ThrowCompletionOr calendar_month_day_to_iso_reference_date(VM&, StringView calendar, CalendarFields const&, Overflow); CalendarDate calendar_iso_to_date(StringView calendar, ISODate const&); Vector calendar_extra_fields(StringView calendar, CalendarFieldList); +Vector calendar_field_keys_to_ignore(StringView calendar, ReadonlySpan); ThrowCompletionOr calendar_resolve_fields(VM&, StringView calendar, CalendarFields&, DateType); } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp b/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp index 58586e2ffda..cb87ee86f79 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp @@ -113,4 +113,35 @@ bool iso_date_within_limits(ISODate iso_date) return iso_date_time_within_limits(iso_date_time); } +// 3.5.12 CompareISODate ( isoDate1, isoDate2 ), https://tc39.es/proposal-temporal/#sec-temporal-compareisodate +i8 compare_iso_date(ISODate iso_date1, ISODate iso_date2) +{ + // 1. If isoDate1.[[Year]] > isoDate2.[[Year]], return 1. + if (iso_date1.year > iso_date2.year) + return 1; + + // 2. If isoDate1.[[Year]] < isoDate2.[[Year]], return -1. + if (iso_date1.year < iso_date2.year) + return -1; + + // 3. If isoDate1.[[Month]] > isoDate2.[[Month]], return 1. + if (iso_date1.month > iso_date2.month) + return 1; + + // 4. If isoDate1.[[Month]] < isoDate2.[[Month]], return -1. + if (iso_date1.month < iso_date2.month) + return -1; + + // 5. If isoDate1.[[Day]] > isoDate2.[[Day]], return 1. + if (iso_date1.day > iso_date2.day) + return 1; + + // 6. If isoDate1.[[Day]] < isoDate2.[[Day]], return -1. + if (iso_date1.day < iso_date2.day) + return -1; + + // 7. Return 0. + return 0; +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDate.h b/Libraries/LibJS/Runtime/Temporal/PlainDate.h index 310510fc23d..7a235c9d7fa 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainDate.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainDate.h @@ -27,5 +27,6 @@ ThrowCompletionOr regulate_iso_date(VM& vm, double year, double month, bool is_valid_iso_date(double year, double month, double day); String pad_iso_year(i32 year); bool iso_date_within_limits(ISODate); +i8 compare_iso_date(ISODate, ISODate); } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp index 2d4a68a5b59..f8f92146ac6 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp @@ -6,6 +6,7 @@ */ #include +#include #include #include #include @@ -34,6 +35,8 @@ void PlainMonthDayPrototype::initialize(Realm& realm) define_native_accessor(realm, vm.names.day, day_getter, {}, Attribute::Configurable); u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(realm, vm.names.with, with, 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); @@ -72,6 +75,63 @@ JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::day_getter) return calendar_iso_to_date(month_day->calendar(), month_day->iso_date()).day; } +// 10.3.6 Temporal.PlainMonthDay.prototype.with ( temporalMonthDayLike [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.with +JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::with) +{ + auto temporal_month_day_like = vm.argument(0); + auto options = vm.argument(1); + + // 1. Let monthDay be the this value. + // 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]). + auto month_day = TRY(typed_this_object(vm)); + + // 3. If ? IsPartialTemporalObject(temporalMonthDayLike) is false, throw a TypeError exception. + if (!TRY(is_partial_temporal_object(vm, temporal_month_day_like))) + return vm.throw_completion(ErrorType::TemporalObjectMustBePartialTemporalObject); + + // 4. Let calendar be monthDay.[[Calendar]]. + auto const& calendar = month_day->calendar(); + + // 5. Let fields be ISODateToFields(calendar, monthDay.[[ISODate]], MONTH-DAY). + auto fields = iso_date_to_fields(calendar, month_day->iso_date(), DateType::MonthDay); + + // 6. Let partialMonthDay be ? PrepareCalendarFields(calendar, temporalMonthDayLike, « YEAR, MONTH, MONTH-CODE, DAY », « », PARTIAL). + auto partial_month_day = TRY(prepare_calendar_fields(vm, calendar, temporal_month_day_like.as_object(), { { CalendarField::Year, CalendarField::Month, CalendarField::MonthCode, CalendarField::Day } }, {}, Partial {})); + + // 7. Set fields to CalendarMergeFields(calendar, fields, partialMonthDay). + fields = calendar_merge_fields(calendar, fields, partial_month_day); + + // 8. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, options)); + + // 9. Let overflow be ? GetTemporalOverflowOption(resolvedOptions). + auto overflow = TRY(get_temporal_overflow_option(vm, resolved_options)); + + // 10. Let isoDate be ? CalendarMonthDayFromFields(calendar, fields, overflow). + auto iso_date = TRY(calendar_month_day_from_fields(vm, calendar, fields, overflow)); + + // 11. Return ! CreateTemporalMonthDay(isoDate, calendar). + return MUST(create_temporal_month_day(vm, iso_date, calendar)); +} + +// 10.3.7 Temporal.PlainMonthDay.prototype.equals ( other ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.equals +JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::equals) +{ + // 1. Let monthDay be the this value. + // 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]). + auto month_day = TRY(typed_this_object(vm)); + + // 3. Set other to ? ToTemporalMonthDay(other). + auto other = TRY(to_temporal_month_day(vm, vm.argument(0))); + + // 4. If CompareISODate(monthDay.[[ISODate]], other.[[ISODate]]) ≠ 0, return false. + if (compare_iso_date(month_day->iso_date(), other->iso_date()) != 0) + return false; + + // 5. Return CalendarEquals(monthDay.[[Calendar]], other.[[Calendar]]). + return calendar_equals(month_day->calendar(), other->calendar()); +} + // 10.3.8 Temporal.PlainMonthDay.prototype.toString ( [ options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.tostring JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::to_string) { diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h index 6eb48a84078..ace7fbc8005 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h @@ -26,6 +26,8 @@ private: JS_DECLARE_NATIVE_FUNCTION(calendar_id_getter); JS_DECLARE_NATIVE_FUNCTION(month_code_getter); JS_DECLARE_NATIVE_FUNCTION(day_getter); + JS_DECLARE_NATIVE_FUNCTION(with); + 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/PlainMonthDay/PlainMonthDay.prototype.equals.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.equals.js new file mode 100644 index 00000000000..cff03493229 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.equals.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("length is 1", () => { + expect(Temporal.PlainMonthDay.prototype.equals).toHaveLength(1); + }); + + test("basic functionality", () => { + const firstPlainMonthDay = new Temporal.PlainMonthDay(2, 1, "iso8601"); + const secondPlainMonthDay = new Temporal.PlainMonthDay(1, 1, "iso8601"); + expect(firstPlainMonthDay.equals(firstPlainMonthDay)).toBeTrue(); + expect(secondPlainMonthDay.equals(secondPlainMonthDay)).toBeTrue(); + expect(firstPlainMonthDay.equals(secondPlainMonthDay)).toBeFalse(); + expect(secondPlainMonthDay.equals(firstPlainMonthDay)).toBeFalse(); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.with.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.with.js new file mode 100644 index 00000000000..596dc0cf3bc --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.with.js @@ -0,0 +1,57 @@ +describe("correct behavior", () => { + test("length is 1", () => { + expect(Temporal.PlainMonthDay.prototype.with).toHaveLength(1); + }); + + test("basic functionality", () => { + const plainMonthDay = new Temporal.PlainMonthDay(1, 1); + const values = [ + [{ monthCode: "M07" }, new Temporal.PlainMonthDay(7, 1)], + [{ monthCode: "M07", day: 6 }, new Temporal.PlainMonthDay(7, 6)], + [{ year: 0, month: 7, day: 6 }, new Temporal.PlainMonthDay(7, 6)], + ]; + for (const [arg, expected] of values) { + expect(plainMonthDay.with(arg).equals(expected)).toBeTrue(); + } + + // Supplying the same values doesn't change the month/day, but still creates a new object + const plainMonthDayLike = { + month: plainMonthDay.month, + day: plainMonthDay.day, + }; + expect(plainMonthDay.with(plainMonthDayLike)).not.toBe(plainMonthDay); + expect(plainMonthDay.with(plainMonthDayLike).equals(plainMonthDay)).toBeTrue(); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainMonthDay object", () => { + expect(() => { + Temporal.PlainMonthDay.prototype.with.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay"); + }); + + test("argument must be an object", () => { + expect(() => { + new Temporal.PlainMonthDay(1, 1).with("foo"); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + expect(() => { + new Temporal.PlainMonthDay(1, 1).with(42); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + }); + + test("argument must have at least one Temporal property", () => { + expect(() => { + new Temporal.PlainMonthDay(1, 1).with({}); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + }); + + test("argument must not have 'calendar' or 'timeZone'", () => { + expect(() => { + new Temporal.PlainMonthDay(1, 1).with({ calendar: {} }); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + expect(() => { + new Temporal.PlainMonthDay(1, 1).with({ timeZone: {} }); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + }); +});