From 2da526423fec330ff642c5443ba9d0c3347b4120 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Thu, 21 Nov 2024 11:39:26 -0500 Subject: [PATCH] LibJS: Implement stringification Temporal.PlainYearMonth prototypes --- .../LibJS/Runtime/Temporal/PlainYearMonth.cpp | 27 ++++++++ .../LibJS/Runtime/Temporal/PlainYearMonth.h | 1 + .../Temporal/PlainYearMonthPrototype.cpp | 46 +++++++++++++ .../Temporal/PlainYearMonthPrototype.h | 3 + .../Temporal/PlainYearMonth/PlainYearMonth.js | 12 ++-- .../PlainYearMonth.prototype.toJSON.js | 23 +++++++ ...PlainYearMonth.prototype.toLocaleString.js | 23 +++++++ .../PlainYearMonth.prototype.toString.js | 64 +++++++++++++++++++ 8 files changed, 193 insertions(+), 6 deletions(-) create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toJSON.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toLocaleString.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toString.js diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp index 2280d3a7212..f737d92a949 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp @@ -149,4 +149,31 @@ ThrowCompletionOr> create_temporal_year_month(VM& vm, IS return object; } +// 9.5.6 TemporalYearMonthToString ( yearMonth, showCalendar ), https://tc39.es/proposal-temporal/#sec-temporal-temporalyearmonthtostring +String temporal_year_month_to_string(PlainYearMonth const& year_month, ShowCalendar show_calendar) +{ + // 1. Let year be PadISOYear(yearMonth.[[ISODate]].[[Year]]). + auto year = pad_iso_year(year_month.iso_date().year); + + // 2. Let month be ToZeroPaddedDecimalString(yearMonth.[[ISODate]].[[Month]], 2). + // 3. Let result be the string-concatenation of year, the code unit 0x002D (HYPHEN-MINUS), and month. + auto result = MUST(String::formatted("{}-{:02}", year, year_month.iso_date().month)); + + // 4. If showCalendar is one of always or critical, or if yearMonth.[[Calendar]] is not "iso8601", then + if (show_calendar == ShowCalendar::Always || show_calendar == ShowCalendar::Critical || year_month.calendar() != "iso8601"sv) { + // a. Let day be ToZeroPaddedDecimalString(yearMonth.[[ISODate]].[[Day]], 2). + // b. Set result to the string-concatenation of result, the code unit 0x002D (HYPHEN-MINUS), and day. + result = MUST(String::formatted("{}-{:02}", result, year_month.iso_date().day)); + } + + // 5. Let calendarString be FormatCalendarAnnotation(yearMonth.[[Calendar]], showCalendar). + auto calendar_string = format_calendar_annotation(year_month.calendar(), show_calendar); + + // 6. Set result to the string-concatenation of result and calendarString. + result = MUST(String::formatted("{}{}", result, calendar_string)); + + // 7. Return result. + return result; +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h index 36884ed74c9..20b876d244a 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h @@ -35,5 +35,6 @@ private: ThrowCompletionOr> to_temporal_year_month(VM&, Value item, Value options = js_undefined()); bool iso_year_month_within_limits(ISODate); ThrowCompletionOr> create_temporal_year_month(VM&, ISODate, String calendar, GC::Ptr new_target = {}); +String temporal_year_month_to_string(PlainYearMonth const&, ShowCalendar); } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp index f9aba3f9c81..b363af9c158 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp @@ -5,6 +5,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include @@ -37,6 +38,11 @@ void PlainYearMonthPrototype::initialize(Realm& realm) define_native_accessor(realm, vm.names.daysInMonth, days_in_month_getter, {}, Attribute::Configurable); define_native_accessor(realm, vm.names.monthsInYear, months_in_year_getter, {}, Attribute::Configurable); define_native_accessor(realm, vm.names.inLeapYear, in_leap_year_getter, {}, Attribute::Configurable); + + u8 attr = Attribute::Writable | Attribute::Configurable; + 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); } // 9.3.3 get Temporal.PlainYearMonth.prototype.calendarId, https://tc39.es/proposal-temporal/#sec-get-temporal.plainyearmonth.prototype.calendarid @@ -123,4 +129,44 @@ JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::month_code_getter) return PrimitiveString::create(vm, move(month_code)); } +// 9.3.19 Temporal.PlainYearMonth.prototype.toString ( [ options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.tostring +JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::to_string) +{ + // 1. Let yearMonth be the this value. + // 2. Perform ? RequireInternalSlot(yearMonth, [[InitializedTemporalYearMonth]]). + auto year_month = TRY(typed_this_object(vm)); + + // 3. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, vm.argument(0))); + + // 4. Let showCalendar be ? GetTemporalShowCalendarNameOption(resolvedOptions). + auto show_calendar = TRY(get_temporal_show_calendar_name_option(vm, resolved_options)); + + // 5. Return TemporalYearMonthToString(yearMonth, showCalendar). + return PrimitiveString::create(vm, temporal_year_month_to_string(year_month, show_calendar)); +} + +// 9.3.20 Temporal.PlainYearMonth.prototype.toLocaleString ( [ locales [ , options ] ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.tolocalestring +// NOTE: This is the minimum toLocaleString implementation for engines without ECMA-402. +JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::to_locale_string) +{ + // 1. Let yearMonth be the this value. + // 2. Perform ? RequireInternalSlot(yearMonth, [[InitializedTemporalYearMonth]]). + auto year_month = TRY(typed_this_object(vm)); + + // 3. Return TemporalYearMonthToString(yearMonth, AUTO). + return PrimitiveString::create(vm, temporal_year_month_to_string(year_month, ShowCalendar::Auto)); +} + +// 9.3.21 Temporal.PlainYearMonth.prototype.toJSON ( ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.tojson +JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::to_json) +{ + // 1. Let yearMonth be the this value. + // 2. Perform ? RequireInternalSlot(yearMonth, [[InitializedTemporalYearMonth]]). + auto year_month = TRY(typed_this_object(vm)); + + // 3. Return TemporalYearMonthToString(yearMonth, AUTO). + return PrimitiveString::create(vm, temporal_year_month_to_string(year_month, ShowCalendar::Auto)); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h index 53a73ff791e..57e288bdee1 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h @@ -33,6 +33,9 @@ private: JS_DECLARE_NATIVE_FUNCTION(days_in_month_getter); JS_DECLARE_NATIVE_FUNCTION(months_in_year_getter); JS_DECLARE_NATIVE_FUNCTION(in_leap_year_getter); + 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/PlainYearMonth/PlainYearMonth.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.js index e6887e86c07..8072eba2b32 100644 --- a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.js +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.js @@ -51,10 +51,10 @@ describe("normal behavior", () => { expect(Object.getPrototypeOf(plainYearMonth)).toBe(Temporal.PlainYearMonth.prototype); }); - // FIXME: Re-implement this test with Temporal.PlainYearMonth.prototype.toString({ calendarName: "always" }). - // test("default reference day is 1", () => { - // const plainYearMonth = new Temporal.PlainYearMonth(2021, 7); - // const fields = plainYearMonth.getISOFields(); - // expect(fields.isoDay).toBe(1); - // }); + test("default reference day is 1", () => { + const plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + const fields = plainYearMonth.toString({ calendarName: "always" }); + const day = fields.split("-")[2].split("[")[0]; + expect(day).toBe("01"); + }); }); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toJSON.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toJSON.js new file mode 100644 index 00000000000..8e98e16218f --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toJSON.js @@ -0,0 +1,23 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.PlainYearMonth.prototype.toJSON).toHaveLength(0); + }); + + test("basic functionality", () => { + let plainYearMonth; + + plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + expect(plainYearMonth.toJSON()).toBe("2021-07"); + + plainYearMonth = new Temporal.PlainYearMonth(2021, 7, "gregory", 6); + expect(plainYearMonth.toJSON()).toBe("2021-07-06[u-ca=gregory]"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Temporal.PlainYearMonth.prototype.toJSON.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toLocaleString.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toLocaleString.js new file mode 100644 index 00000000000..4fb4aa16bf4 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toLocaleString.js @@ -0,0 +1,23 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.PlainYearMonth.prototype.toLocaleString).toHaveLength(0); + }); + + test("basic functionality", () => { + let plainYearMonth; + + plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + expect(plainYearMonth.toLocaleString()).toBe("2021-07"); + + plainYearMonth = new Temporal.PlainYearMonth(2021, 7, "gregory", 6); + expect(plainYearMonth.toLocaleString()).toBe("2021-07-06[u-ca=gregory]"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Temporal.PlainYearMonth.prototype.toLocaleString.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toString.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toString.js new file mode 100644 index 00000000000..99a667c6998 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toString.js @@ -0,0 +1,64 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.PlainYearMonth.prototype.toString).toHaveLength(0); + }); + + test("basic functionality", () => { + let plainYearMonth; + + plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + expect(plainYearMonth.toString()).toBe("2021-07"); + expect(plainYearMonth.toString({ calendarName: "auto" })).toBe("2021-07"); + expect(plainYearMonth.toString({ calendarName: "always" })).toBe( + "2021-07-01[u-ca=iso8601]" + ); + expect(plainYearMonth.toString({ calendarName: "never" })).toBe("2021-07"); + expect(plainYearMonth.toString({ calendarName: "critical" })).toBe( + "2021-07-01[!u-ca=iso8601]" + ); + + plainYearMonth = new Temporal.PlainYearMonth(2021, 7, "gregory", 6); + expect(plainYearMonth.toString()).toBe("2021-07-06[u-ca=gregory]"); + expect(plainYearMonth.toString({ calendarName: "auto" })).toBe("2021-07-06[u-ca=gregory]"); + expect(plainYearMonth.toString({ calendarName: "always" })).toBe( + "2021-07-06[u-ca=gregory]" + ); + expect(plainYearMonth.toString({ calendarName: "never" })).toBe("2021-07-06"); + expect(plainYearMonth.toString({ calendarName: "critical" })).toBe( + "2021-07-06[!u-ca=gregory]" + ); + + plainYearMonth = new Temporal.PlainYearMonth(0, 1); + expect(plainYearMonth.toString()).toBe("0000-01"); + + plainYearMonth = new Temporal.PlainYearMonth(999, 1); + expect(plainYearMonth.toString()).toBe("0999-01"); + + plainYearMonth = new Temporal.PlainYearMonth(9999, 1); + expect(plainYearMonth.toString()).toBe("9999-01"); + + plainYearMonth = new Temporal.PlainYearMonth(12345, 1); + expect(plainYearMonth.toString()).toBe("+012345-01"); + + plainYearMonth = new Temporal.PlainYearMonth(123456, 1); + expect(plainYearMonth.toString()).toBe("+123456-01"); + + plainYearMonth = new Temporal.PlainYearMonth(-12345, 1); + expect(plainYearMonth.toString()).toBe("-012345-01"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Temporal.PlainYearMonth.prototype.toString.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); + + test("calendarName option must be one of 'auto', 'always', 'never', 'critical'", () => { + const plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + expect(() => { + plainYearMonth.toString({ calendarName: "foo" }); + }).toThrowWithMessage(RangeError, "foo is not a valid value for option calendarName"); + }); +});