diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index f84c1c9fa76..e41c31f82e6 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -84,6 +84,28 @@ ThrowCompletionOr get_temporal_overflow_option(VM& vm, Object const& o return Overflow::Reject; } +// 13.10 GetTemporalShowCalendarNameOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporalshowcalendarnameoption +ThrowCompletionOr get_temporal_show_calendar_name_option(VM& vm, Object const& options) +{ + // 1. Let stringValue be ? GetOption(options, "calendarName", STRING, « "auto", "always", "never", "critical" », "auto"). + auto string_value = TRY(get_option(vm, options, vm.names.calendarName, OptionType::String, { "auto"sv, "always"sv, "never"sv, "critical"sv }, "auto"sv)); + + // 2. If stringValue is "always", return ALWAYS. + if (string_value.as_string().utf8_string_view() == "always"sv) + return ShowCalendar::Always; + + // 3. If stringValue is "never", return NEVER. + if (string_value.as_string().utf8_string_view() == "never"sv) + return ShowCalendar::Never; + + // 4. If stringValue is "critical", return CRITICAL. + if (string_value.as_string().utf8_string_view() == "critical"sv) + return ShowCalendar::Critical; + + // 5. Return AUTO. + return ShowCalendar::Auto; +} + // 13.14 ValidateTemporalRoundingIncrement ( increment, dividend, inclusive ), https://tc39.es/proposal-temporal/#sec-validatetemporalroundingincrement ThrowCompletionOr validate_temporal_rounding_increment(VM& vm, u64 increment, u64 dividend, bool inclusive) { diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index 101f757206c..16a1df2aaca 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -38,6 +38,13 @@ enum class Overflow { Reject, }; +enum class ShowCalendar { + Auto, + Always, + Never, + Critical, +}; + enum class TimeStyle { Separated, Unseparated, @@ -144,6 +151,7 @@ struct ParsedISODateTime { double iso_date_to_epoch_days(double year, double month, double date); double epoch_days_to_epoch_ms(double day, double time); ThrowCompletionOr get_temporal_overflow_option(VM&, Object const& options); +ThrowCompletionOr get_temporal_show_calendar_name_option(VM&, Object const& options); ThrowCompletionOr validate_temporal_rounding_increment(VM&, u64 increment, u64 dividend, bool inclusive); ThrowCompletionOr get_temporal_fractional_second_digits_option(VM&, Object const& options); SecondsStringPrecision to_seconds_string_precision_record(UnitValue, Precision); diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.cpp b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp index 93fb7f8a96e..fcb5a6a4cfe 100644 --- a/Libraries/LibJS/Runtime/Temporal/Calendar.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp @@ -310,6 +310,24 @@ ThrowCompletionOr calendar_month_day_from_fields(VM& vm, StringView cal return result; } +// 12.2.13 FormatCalendarAnnotation ( id, showCalendar ), https://tc39.es/proposal-temporal/#sec-temporal-formatcalendarannotation +String format_calendar_annotation(StringView id, ShowCalendar show_calendar) +{ + // 1. If showCalendar is NEVER, return the empty String. + if (show_calendar == ShowCalendar::Never) + return String {}; + + // 2. If showCalendar is AUTO and id is "iso8601", return the empty String. + if (show_calendar == ShowCalendar::Auto && id == "iso8601"sv) + return String {}; + + // 3. If showCalendar is CRITICAL, let flag be "!"; else, let flag be the empty String. + auto flag = show_calendar == ShowCalendar::Critical ? "!"sv : ""sv; + + // 4. Return the string-concatenation of "[", flag, "u-ca=", id, and "]". + return MUST(String::formatted("[{}u-ca={}]", flag, id)); +} + // 12.2.15 ISODaysInMonth ( year, month ), https://tc39.es/proposal-temporal/#sec-temporal-isodaysinmonth u8 iso_days_in_month(double year, double month) { diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.h b/Libraries/LibJS/Runtime/Temporal/Calendar.h index 7289d1e3c78..c54b5b22d29 100644 --- a/Libraries/LibJS/Runtime/Temporal/Calendar.h +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.h @@ -100,6 +100,7 @@ ThrowCompletionOr canonicalize_calendar(VM&, StringView id); 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); u8 iso_days_in_month(double year, double month); YearWeek iso_week_of_year(ISODate const&); u16 iso_day_of_year(ISODate const&); diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp b/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp index f523d6539ab..5fe270d0169 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp @@ -84,6 +84,23 @@ bool is_valid_iso_date(double year, double month, double day) return true; } +// 3.5.9 PadISOYear ( y ), https://tc39.es/proposal-temporal/#sec-temporal-padisoyear +String pad_iso_year(i32 year) +{ + // 1. If y ≥ 0 and y ≤ 9999, then + if (year >= 0 && year <= 9999) { + // a. Return ToZeroPaddedDecimalString(y, 4). + return MUST(String::formatted("{:04}", year)); + } + + // 2. If y > 0, let yearSign be "+"; otherwise, let yearSign be "-". + auto year_sign = year > 0 ? '+' : '-'; + + // 3. Let year be ToZeroPaddedDecimalString(abs(y), 6). + // 4. Return the string-concatenation of yearSign and year. + return MUST(String::formatted("{}{:06}", year_sign, abs(year))); +} + // 3.5.11 ISODateWithinLimits ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isodatewithinlimits bool iso_date_within_limits(ISODate iso_date) { diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDate.h b/Libraries/LibJS/Runtime/Temporal/PlainDate.h index 67d38b33753..310510fc23d 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainDate.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainDate.h @@ -25,6 +25,7 @@ struct ISODate { ISODate create_iso_date_record(double year, double month, double day); ThrowCompletionOr regulate_iso_date(VM& vm, double year, double month, double day, Overflow overflow); bool is_valid_iso_date(double year, double month, double day); +String pad_iso_year(i32 year); bool iso_date_within_limits(ISODate); } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.cpp b/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.cpp index e5efdc04c35..1cdf1225633 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -141,4 +140,31 @@ ThrowCompletionOr> create_temporal_month_day(VM& vm, ISOD return object; } +// 10.5.3 TemporalMonthDayToString ( monthDay, showCalendar ), https://tc39.es/proposal-temporal/#sec-temporal-temporalmonthdaytostring +String temporal_month_day_to_string(PlainMonthDay const& month_day, ShowCalendar show_calendar) +{ + // 1. Let month be ToZeroPaddedDecimalString(monthDay.[[ISODate]].[[Month]], 2). + // 2. Let day be ToZeroPaddedDecimalString(monthDay.[[ISODate]].[[Day]], 2). + // 3. Let result be the string-concatenation of month, the code unit 0x002D (HYPHEN-MINUS), and day. + auto result = MUST(String::formatted("{:02}-{:02}", month_day.iso_date().month, month_day.iso_date().day)); + + // 4. If showCalendar is one of ALWAYS or CRITICAL, or if monthDay.[[Calendar]] is not "iso8601", then + if (show_calendar == ShowCalendar::Always || show_calendar == ShowCalendar::Critical || month_day.calendar() != "iso8601"sv) { + // a. Let year be PadISOYear(monthDay.[[ISODate]].[[Year]]). + auto year = pad_iso_year(month_day.iso_date().year); + + // b. Set result to the string-concatenation of year, the code unit 0x002D (HYPHEN-MINUS), and result. + result = MUST(String::formatted("{}-{}", year, result)); + } + + // 5. Let calendarString be FormatCalendarAnnotation(monthDay.[[Calendar]], showCalendar). + auto calendar_string = format_calendar_annotation(month_day.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/PlainMonthDay.h b/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.h index 3a6eeb6ab80..0f2592240bc 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDay.h @@ -10,6 +10,7 @@ #include #include #include +#include #include namespace JS::Temporal { @@ -33,5 +34,6 @@ private: ThrowCompletionOr> to_temporal_month_day(VM&, Value item, Value options = js_undefined()); ThrowCompletionOr> create_temporal_month_day(VM&, ISODate, String calendar, GC::Ptr new_target = {}); +String temporal_month_day_to_string(PlainMonthDay const&, ShowCalendar); } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp index 4987eb706af..2d4a68a5b59 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp @@ -32,6 +32,11 @@ void PlainMonthDayPrototype::initialize(Realm& realm) define_native_accessor(realm, vm.names.calendarId, calendar_id_getter, {}, Attribute::Configurable); define_native_accessor(realm, vm.names.monthCode, month_code_getter, {}, Attribute::Configurable); define_native_accessor(realm, vm.names.day, day_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); } // 10.3.3 get Temporal.PlainMonthDay.prototype.calendarId, https://tc39.es/proposal-temporal/#sec-get-temporal.plainmonthday.prototype.calendarid @@ -67,4 +72,44 @@ JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::day_getter) return calendar_iso_to_date(month_day->calendar(), month_day->iso_date()).day; } +// 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) +{ + // 1. Let monthDay be the this value. + // 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]). + auto month_day = 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 TemporalMonthDayToString(monthDay, showCalendar). + return PrimitiveString::create(vm, temporal_month_day_to_string(month_day, show_calendar)); +} + +// 10.3.9 Temporal.PlainMonthDay.prototype.toLocaleString ( [ locales [ , options ] ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.tolocalestring +// NOTE: This is the minimum toLocaleString implementation for engines without ECMA-402. +JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::to_locale_string) +{ + // 1. Let monthDay be the this value. + // 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]). + auto month_day = TRY(typed_this_object(vm)); + + // 3. Return TemporalMonthDayToString(monthDay, auto). + return PrimitiveString::create(vm, temporal_month_day_to_string(month_day, ShowCalendar::Auto)); +} + +// 10.3.10 Temporal.PlainMonthDay.prototype.toJSON ( ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.tolocalestring +JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::to_json) +{ + // 1. Let monthDay be the this value. + // 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]). + auto month_day = TRY(typed_this_object(vm)); + + // 3. Return TemporalMonthDayToString(monthDay, auto). + return PrimitiveString::create(vm, temporal_month_day_to_string(month_day, ShowCalendar::Auto)); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h index 33a00aedee9..6eb48a84078 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h @@ -26,6 +26,9 @@ 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(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.toJSON.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.toJSON.js new file mode 100644 index 00000000000..0e9ac4e3a2e --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.toJSON.js @@ -0,0 +1,23 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.PlainMonthDay.prototype.toJSON).toHaveLength(0); + }); + + test("basic functionality", () => { + let plainMonthDay; + + plainMonthDay = new Temporal.PlainMonthDay(7, 6); + expect(plainMonthDay.toJSON()).toBe("07-06"); + + plainMonthDay = new Temporal.PlainMonthDay(7, 6, "gregory", 2021); + expect(plainMonthDay.toJSON()).toBe("2021-07-06[u-ca=gregory]"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainMonthDay object", () => { + expect(() => { + Temporal.PlainMonthDay.prototype.toJSON.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.toLocaleString.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.toLocaleString.js new file mode 100644 index 00000000000..f5ea5e9cc2f --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.toLocaleString.js @@ -0,0 +1,23 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.PlainMonthDay.prototype.toLocaleString).toHaveLength(0); + }); + + test("basic functionality", () => { + let plainMonthDay; + + plainMonthDay = new Temporal.PlainMonthDay(7, 6); + expect(plainMonthDay.toLocaleString()).toBe("07-06"); + + plainMonthDay = new Temporal.PlainMonthDay(7, 6, "gregory", 2021); + expect(plainMonthDay.toLocaleString()).toBe("2021-07-06[u-ca=gregory]"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainMonthDay object", () => { + expect(() => { + Temporal.PlainMonthDay.prototype.toLocaleString.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.toString.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.toString.js new file mode 100644 index 00000000000..b296f8b0716 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.toString.js @@ -0,0 +1,42 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.PlainMonthDay.prototype.toString).toHaveLength(0); + }); + + test("basic functionality", () => { + let plainMonthDay; + + plainMonthDay = new Temporal.PlainMonthDay(7, 6); + expect(plainMonthDay.toString()).toBe("07-06"); + expect(plainMonthDay.toString({ calendarName: "auto" })).toBe("07-06"); + expect(plainMonthDay.toString({ calendarName: "always" })).toBe("1972-07-06[u-ca=iso8601]"); + expect(plainMonthDay.toString({ calendarName: "never" })).toBe("07-06"); + expect(plainMonthDay.toString({ calendarName: "critical" })).toBe( + "1972-07-06[!u-ca=iso8601]" + ); + + plainMonthDay = new Temporal.PlainMonthDay(7, 6, "gregory", 2021); + expect(plainMonthDay.toString()).toBe("2021-07-06[u-ca=gregory]"); + expect(plainMonthDay.toString({ calendarName: "auto" })).toBe("2021-07-06[u-ca=gregory]"); + expect(plainMonthDay.toString({ calendarName: "always" })).toBe("2021-07-06[u-ca=gregory]"); + expect(plainMonthDay.toString({ calendarName: "never" })).toBe("2021-07-06"); + expect(plainMonthDay.toString({ calendarName: "critical" })).toBe( + "2021-07-06[!u-ca=gregory]" + ); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainMonthDay object", () => { + expect(() => { + Temporal.PlainMonthDay.prototype.toString.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay"); + }); + + test("calendarName option must be one of 'auto', 'always', 'never', 'critical'", () => { + const plainMonthDay = new Temporal.PlainMonthDay(7, 6); + expect(() => { + plainMonthDay.toString({ calendarName: "foo" }); + }).toThrowWithMessage(RangeError, "foo is not a valid value for option calendarName"); + }); +});