diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 47a2130986..1b9f254ef1 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -80,6 +80,7 @@ namespace JS { P(byteLength) \ P(byteOffset) \ P(calendar) \ + P(calendarName) \ P(call) \ P(callee) \ P(caller) \ diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index 2a7b357da9..cf347132b1 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -195,6 +195,20 @@ Optional to_temporal_rounding_mode(GlobalObject& global_object, Object& return option.as_string().string(); } +// 13.11 ToShowCalendarOption ( normalizedOptions ), https://tc39.es/proposal-temporal/#sec-temporal-toshowcalendaroption +Optional to_show_calendar_option(GlobalObject& global_object, Object& normalized_options) +{ + auto& vm = global_object.vm(); + + // 1. Return ? GetOption(normalizedOptions, "calendarName", « String », « "auto", "always", "never" », "auto"). + auto option = get_option(global_object, normalized_options, vm.names.calendarName, { OptionType::String }, { "auto"sv, "always"sv, "never"sv }, js_string(vm, "auto"sv)); + if (vm.exception()) + return {}; + + VERIFY(option.is_string()); + return option.as_string().string(); +} + // 13.14 ToTemporalRoundingIncrement ( normalizedOptions, dividend, inclusive ), https://tc39.es/proposal-temporal/#sec-temporal-totemporalroundingincrement u64 to_temporal_rounding_increment(GlobalObject& global_object, Object& normalized_options, Optional dividend, bool inclusive) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index c74f36c7e7..212631edd5 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -64,6 +64,7 @@ Object* get_options_object(GlobalObject&, Value options); Value get_option(GlobalObject&, Object& options, PropertyName const& property, Vector const& types, Vector const& values, Value fallback); Optional to_temporal_overflow(GlobalObject&, Object& normalized_options); Optional to_temporal_rounding_mode(GlobalObject&, Object& normalized_options, String const& fallback); +Optional to_show_calendar_option(GlobalObject&, Object& normalized_options); u64 to_temporal_rounding_increment(GlobalObject&, Object& normalized_options, Optional dividend, bool inclusive); Optional to_smallest_temporal_unit(GlobalObject&, Object& normalized_options, Vector const& disallowed_units, Optional fallback); double constrain_to_range(double x, double minimum, double maximum); diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.cpp index c7d0d4763a..7216391c05 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.cpp @@ -477,6 +477,24 @@ PlainMonthDay* month_day_from_fields(GlobalObject& global_object, Object& calend return static_cast(month_day_object); } +// 12.1.27 FormatCalendarAnnotation ( id, showCalendar ), https://tc39.es/proposal-temporal/#sec-temporal-formatcalendarannotation +String format_calendar_annotation(StringView id, StringView show_calendar) +{ + // 1. Assert: showCalendar is "auto", "always", or "never". + VERIFY(show_calendar == "auto"sv || show_calendar == "always"sv || show_calendar == "never"sv); + + // 2. If showCalendar is "never", return the empty String. + if (show_calendar == "never"sv) + return String::empty(); + + // 3. If showCalendar is "auto" and id is "iso8601", return the empty String. + if (show_calendar == "auto"sv && id == "iso8601"sv) + return String::empty(); + + // 4. Return the string-concatenation of "[u-ca=", id, and "]". + return String::formatted("[u-ca={}]", id); +} + // 12.1.28 CalendarEquals ( one, two ), https://tc39.es/proposal-temporal/#sec-temporal-calendarequals bool calendar_equals(GlobalObject& global_object, Object& one, Object& two) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.h b/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.h index f6823de4c2..7e7d96082e 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/Calendar.h @@ -52,6 +52,7 @@ Object* get_temporal_calendar_with_iso_default(GlobalObject&, Object&); PlainDate* date_from_fields(GlobalObject&, Object& calendar, Object& fields, Object& options); PlainYearMonth* year_month_from_fields(GlobalObject&, Object& calendar, Object& fields, Object* options = nullptr); PlainMonthDay* month_day_from_fields(GlobalObject& global_object, Object& calendar, Object& fields, Object* options = nullptr); +String format_calendar_annotation(StringView id, StringView show_calendar); bool calendar_equals(GlobalObject&, Object& one, Object& two); Object* consolidate_calendars(GlobalObject&, Object& one, Object& two); bool is_iso_leap_year(i32 year); diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp index 9dce0cfa62..d3446f8888 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp @@ -332,6 +332,53 @@ ISODate balance_iso_date(double year_, double month_, double day) return ISODate { .year = year, .month = static_cast(month), .day = static_cast(day) }; } +// 3.5.7 PadISOYear ( y ), https://tc39.es/proposal-temporal/#sec-temporal-padisoyear +String pad_iso_year(i32 y) +{ + // 1. Assert: y is an integer. + + // 2. If y > 999 and y ≤ 9999, then + if (y > 999 && y <= 9999) { + // a. Return y formatted as a four-digit decimal number. + return String::number(y); + } + // 3. If y ≥ 0, let yearSign be "+"; otherwise, let yearSign be "-". + auto year_sign = y >= 0 ? '+' : '-'; + + // 4. Let year be abs(y), formatted as a six-digit decimal number, padded to the left with zeroes as necessary. + // 5. Return the string-concatenation of yearSign and year. + return String::formatted("{}{:06}", year_sign, abs(y)); +} + +// 3.5.8 TemporalDateToString ( temporalDate, showCalendar ), https://tc39.es/proposal-temporal/#sec-temporal-temporaldatetostring +Optional temporal_date_to_string(GlobalObject& global_object, PlainDate& temporal_date, StringView show_calendar) +{ + auto& vm = global_object.vm(); + + // 1. Assert: Type(temporalDate) is Object. + // 2. Assert: temporalDate has an [[InitializedTemporalDate]] internal slot. + + // 3. Let year be ! PadISOYear(temporalDate.[[ISOYear]]). + auto year = pad_iso_year(temporal_date.iso_year()); + + // 4. Let month be temporalDate.[[ISOMonth]] formatted as a two-digit decimal number, padded to the left with a zero if necessary. + auto month = String::formatted("{:02}", temporal_date.iso_month()); + + // 5. Let day be temporalDate.[[ISODay]] formatted as a two-digit decimal number, padded to the left with a zero if necessary. + auto day = String::formatted("{:02}", temporal_date.iso_day()); + + // 6. Let calendarID be ? ToString(temporalDate.[[Calendar]]). + auto calendar_id = Value(&temporal_date.calendar()).to_string(global_object); + if (vm.exception()) + return {}; + + // 7. Let calendar be ! FormatCalendarAnnotation(calendarID, showCalendar). + auto calendar = format_calendar_annotation(calendar_id, show_calendar); + + // 8. Return the string-concatenation of year, the code unit 0x002D (HYPHEN-MINUS), month, the code unit 0x002D (HYPHEN-MINUS), day, and calendar. + return String::formatted("{}-{}-{}{}", year, month, day, calendar); +} + // 3.5.10 CompareISODate ( y1, m1, d1, y2, m2, d2 ), https://tc39.es/proposal-temporal/#sec-temporal-compareisodate i8 compare_iso_date(i32 year1, u8 month1, u8 day1, i32 year2, u8 month2, u8 day2) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.h b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.h index 37489ee3b7..03497bc29b 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDate.h @@ -45,6 +45,8 @@ PlainDate* to_temporal_date(GlobalObject&, Value item, Object* options = nullptr Optional regulate_iso_date(GlobalObject&, double year, double month, double day, String const& overflow); bool is_valid_iso_date(i32 year, u8 month, u8 day); ISODate balance_iso_date(double year, double month, double day); +String pad_iso_year(i32 y); +Optional temporal_date_to_string(GlobalObject&, PlainDate&, StringView show_calendar); i8 compare_iso_date(i32 year1, u8 month1, u8 day1, i32 year2, u8 month2, u8 day2); } diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDatePrototype.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDatePrototype.cpp index 2d4fa05d55..e0e448502d 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDatePrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDatePrototype.cpp @@ -51,6 +51,7 @@ void PlainDatePrototype::initialize(GlobalObject& global_object) define_native_function(vm.names.getISOFields, get_iso_fields, 0, attr); define_native_function(vm.names.withCalendar, with_calendar, 1, attr); define_native_function(vm.names.equals, equals, 1, attr); + define_native_function(vm.names.toString, to_string, 0, attr); define_native_function(vm.names.valueOf, value_of, 0, attr); } @@ -397,6 +398,33 @@ JS_DEFINE_NATIVE_FUNCTION(PlainDatePrototype::equals) return Value(calendar_equals(global_object, temporal_date->calendar(), other->calendar())); } +// 3.3.28 Temporal.PlainDate.prototype.toString ( [ options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.tostring +JS_DEFINE_NATIVE_FUNCTION(PlainDatePrototype::to_string) +{ + // 1. Let temporalDate be the this value. + // 2. Perform ? RequireInternalSlot(temporalDate, [[InitializedTemporalDate]]). + auto* temporal_date = typed_this(global_object); + if (vm.exception()) + return {}; + + // 3. Set options to ? GetOptionsObject(options). + auto* options = get_options_object(global_object, vm.argument(0)); + if (vm.exception()) + return {}; + + // 4. Let showCalendar be ? ToShowCalendarOption(options). + auto show_calendar = to_show_calendar_option(global_object, *options); + if (vm.exception()) + return {}; + + // 5. Return ? TemporalDateToString(temporalDate, showCalendar). + auto string = temporal_date_to_string(global_object, *temporal_date, *show_calendar); + if (vm.exception()) + return {}; + + return js_string(vm, *string); +} + // 3.3.31 Temporal.PlainDate.prototype.valueOf ( ), https://tc39.es/proposal-temporal/#sec-temporal.plaindate.prototype.valueof JS_DEFINE_NATIVE_FUNCTION(PlainDatePrototype::value_of) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDatePrototype.h b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDatePrototype.h index a5b3eaf3e1..6a217700d7 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainDatePrototype.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainDatePrototype.h @@ -37,6 +37,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(get_iso_fields); JS_DECLARE_NATIVE_FUNCTION(with_calendar); JS_DECLARE_NATIVE_FUNCTION(equals); + JS_DECLARE_NATIVE_FUNCTION(to_string); JS_DECLARE_NATIVE_FUNCTION(value_of); }; diff --git a/Userland/Libraries/LibJS/Tests/builtins/Temporal/PlainDate/PlainDate.prototype.toString.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/PlainDate/PlainDate.prototype.toString.js new file mode 100644 index 0000000000..b34a91c7b2 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Temporal/PlainDate/PlainDate.prototype.toString.js @@ -0,0 +1,51 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.PlainDate.prototype.toString).toHaveLength(0); + }); + + test("basic functionality", () => { + let plainDate; + + plainDate = new Temporal.PlainDate(2021, 7, 6); + expect(plainDate.toString()).toBe("2021-07-06"); + expect(plainDate.toString({ calendarName: "auto" })).toBe("2021-07-06"); + expect(plainDate.toString({ calendarName: "always" })).toBe("2021-07-06[u-ca=iso8601]"); + expect(plainDate.toString({ calendarName: "never" })).toBe("2021-07-06"); + + plainDate = new Temporal.PlainDate(2021, 7, 6, { toString: () => "foo" }); + expect(plainDate.toString()).toBe("2021-07-06[u-ca=foo]"); + expect(plainDate.toString({ calendarName: "auto" })).toBe("2021-07-06[u-ca=foo]"); + expect(plainDate.toString({ calendarName: "always" })).toBe("2021-07-06[u-ca=foo]"); + expect(plainDate.toString({ calendarName: "never" })).toBe("2021-07-06"); + + plainDate = new Temporal.PlainDate(0, 1, 1); + expect(plainDate.toString()).toBe("+000000-01-01"); + + plainDate = new Temporal.PlainDate(999, 1, 1); + expect(plainDate.toString()).toBe("+000999-01-01"); + + plainDate = new Temporal.PlainDate(12345, 1, 1); + expect(plainDate.toString()).toBe("+012345-01-01"); + + plainDate = new Temporal.PlainDate(123456, 1, 1); + expect(plainDate.toString()).toBe("+123456-01-01"); + + plainDate = new Temporal.PlainDate(-12345, 1, 1); + expect(plainDate.toString()).toBe("-012345-01-01"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainDate object", () => { + expect(() => { + Temporal.PlainDate.prototype.toString.call("foo"); + }).toThrowWithMessage(TypeError, "Not a Temporal.PlainDate"); + }); + + test("calendarName option must be one of 'auto', 'always', 'never'", () => { + const plainDate = new Temporal.PlainDate(2021, 7, 6); + expect(() => { + plainDate.toString({ calendarName: "foo" }); + }).toThrowWithMessage(RangeError, "foo is not a valid value for option calendarName"); + }); +});