From 4a08fd2be2ad8f9cacf79eccad8d59185fa96848 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Mon, 29 Nov 2021 07:55:34 -0500 Subject: [PATCH] LibJS: Implement Intl.DateTimeFormat.prototype.resolvedOptions --- .../Runtime/Intl/DateTimeFormatPrototype.cpp | 77 +++++++++++ .../Runtime/Intl/DateTimeFormatPrototype.h | 3 + ...ateTimeFormat.prototype.resolvedOptions.js | 123 ++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 Userland/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.resolvedOptions.js diff --git a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatPrototype.cpp b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatPrototype.cpp index d828d6bac6..48dfd1b775 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatPrototype.cpp @@ -6,6 +6,7 @@ #include #include +#include namespace JS::Intl { @@ -23,6 +24,82 @@ void DateTimeFormatPrototype::initialize(GlobalObject& global_object) // 11.4.2 Intl.DateTimeFormat.prototype [ @@toStringTag ], https://tc39.es/ecma402/#sec-intl.datetimeformat.prototype-@@tostringtag define_direct_property(*vm.well_known_symbol_to_string_tag(), js_string(vm, "Intl.DateTimeFormat"), Attribute::Configurable); + + u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(vm.names.resolvedOptions, resolved_options, 0, attr); +} + +// 11.4.7 Intl.DateTimeFormat.prototype.resolvedOptions ( ), https://tc39.es/ecma402/#sec-intl.datetimeformat.prototype.resolvedoptions +JS_DEFINE_NATIVE_FUNCTION(DateTimeFormatPrototype::resolved_options) +{ + // 1. Let dtf be the this value. + // 2. If the implementation supports the normative optional constructor mode of 4.3 Note 1, then + // a. Set dtf to ? UnwrapDateTimeFormat(dtf). + // 3. Perform ? RequireInternalSlot(dtf, [[InitializedDateTimeFormat]]). + auto* date_time_format = TRY(typed_this_object(global_object)); + + // 4. Let options be ! OrdinaryObjectCreate(%Object.prototype%). + auto* options = Object::create(global_object, global_object.object_prototype()); + + // 5. For each row of Table 7, except the header row, in table order, do + // a. Let p be the Property value of the current row. + // b. If p is "hour12", then + // i. Let hc be dtf.[[HourCycle]]. + // ii. If hc is "h11" or "h12", let v be true. + // iii. Else if, hc is "h23" or "h24", let v be false. + // iv. Else, let v be undefined. + // c. Else, + // i. Let v be the value of dtf's internal slot whose name is the Internal Slot value of the current row. + // d. If the Internal Slot value of the current row is an Internal Slot value in Table 4, then + // i. If dtf.[[DateStyle]] is not undefined or dtf.[[TimeStyle]] is not undefined, then + // 1. Let v be undefined. + // e. If v is not undefined, then + // i. Perform ! CreateDataPropertyOrThrow(options, p, v). + MUST(options->create_data_property_or_throw(vm.names.locale, js_string(vm, date_time_format->locale()))); + MUST(options->create_data_property_or_throw(vm.names.calendar, js_string(vm, date_time_format->calendar()))); + MUST(options->create_data_property_or_throw(vm.names.numberingSystem, js_string(vm, date_time_format->numbering_system()))); + MUST(options->create_data_property_or_throw(vm.names.timeZone, js_string(vm, date_time_format->time_zone()))); + + if (date_time_format->has_hour_cycle()) { + MUST(options->create_data_property_or_throw(vm.names.hourCycle, js_string(vm, date_time_format->hour_cycle_string()))); + + switch (date_time_format->hour_cycle()) { + case Unicode::HourCycle::H11: + case Unicode::HourCycle::H12: + MUST(options->create_data_property_or_throw(vm.names.hour12, Value(true))); + break; + case Unicode::HourCycle::H23: + case Unicode::HourCycle::H24: + MUST(options->create_data_property_or_throw(vm.names.hour12, Value(false))); + break; + } + } + + if (!date_time_format->has_date_style() && !date_time_format->has_time_style()) { + MUST(for_each_calendar_field(global_object, *date_time_format, [&](auto& option, auto const& property, auto const&) -> ThrowCompletionOr { + using ValueType = typename RemoveReference::ValueType; + + if (!option.has_value()) + return {}; + + if constexpr (IsIntegral) { + TRY(options->create_data_property_or_throw(property, Value(*option))); + } else { + auto name = Unicode::calendar_pattern_style_to_string(*option); + TRY(options->create_data_property_or_throw(property, js_string(vm, name))); + } + + return {}; + })); + } + + if (date_time_format->has_date_style()) + MUST(options->create_data_property_or_throw(vm.names.dateStyle, js_string(vm, date_time_format->date_style_string()))); + if (date_time_format->has_time_style()) + MUST(options->create_data_property_or_throw(vm.names.timeStyle, js_string(vm, date_time_format->time_style_string()))); + + // 6. Return options. + return options; } } diff --git a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatPrototype.h b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatPrototype.h index 494ea76978..c141d700e7 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormatPrototype.h @@ -18,6 +18,9 @@ public: explicit DateTimeFormatPrototype(GlobalObject&); virtual void initialize(GlobalObject&) override; virtual ~DateTimeFormatPrototype() override = default; + +private: + JS_DECLARE_NATIVE_FUNCTION(resolved_options); }; } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.resolvedOptions.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.resolvedOptions.js new file mode 100644 index 0000000000..466311a615 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.resolvedOptions.js @@ -0,0 +1,123 @@ +// NOTE: We cannot yet test the fields of ECMA-402's Table 4 (week, day, etc.) because those fields +// won't be copied into the Intl.DateTimeFormat object until the date-time pattern generator +// actually parses the CLDR patterns (see parse_date_time_pattern). +describe("correct behavior", () => { + test("length is 0", () => { + expect(Intl.DateTimeFormat.prototype.resolvedOptions).toHaveLength(0); + }); + + test("locale only contains relevant extension keys", () => { + const en1 = Intl.NumberFormat("en-u-ca-islamicc"); + expect(en1.resolvedOptions().locale).toBe("en"); + + const en2 = Intl.NumberFormat("en-u-nu-latn"); + expect(en2.resolvedOptions().locale).toBe("en-u-nu-latn"); + + const en3 = Intl.NumberFormat("en-u-ca-islamicc-nu-latn"); + expect(en3.resolvedOptions().locale).toBe("en-u-nu-latn"); + }); + + test("calendar may be set by option", () => { + const en = Intl.DateTimeFormat("en", { calendar: "gregory" }); + expect(en.resolvedOptions().calendar).toBe("gregory"); + + const el = Intl.DateTimeFormat("el", { calendar: "generic" }); + expect(el.resolvedOptions().calendar).toBe("generic"); + }); + + test("calendar may be set by locale extension", () => { + const en = Intl.DateTimeFormat("en-u-ca-gregory"); + expect(en.resolvedOptions().calendar).toBe("gregory"); + + const el = Intl.DateTimeFormat("el-u-ca-generic"); + expect(el.resolvedOptions().calendar).toBe("generic"); + }); + + test("calendar option overrides locale extension", () => { + const el = Intl.DateTimeFormat("el-u-ca-generic", { calendar: "gregory" }); + expect(el.resolvedOptions().calendar).toBe("gregory"); + }); + + test("calendar option limited to known 'ca' values", () => { + ["generic", "hello"].forEach(calendar => { + const en = Intl.DateTimeFormat("en", { calendar: calendar }); + expect(en.resolvedOptions().calendar).toBe("generic"); + }); + + ["generic", "hello"].forEach(calendar => { + const en = Intl.DateTimeFormat(`en-u-ca-${calendar}`); + expect(en.resolvedOptions().calendar).toBe("generic"); + }); + }); + + test("numberingSystem may be set by option", () => { + const en = Intl.DateTimeFormat("en", { numberingSystem: "latn" }); + expect(en.resolvedOptions().numberingSystem).toBe("latn"); + + const el = Intl.DateTimeFormat("el", { numberingSystem: "latn" }); + expect(el.resolvedOptions().numberingSystem).toBe("latn"); + }); + + test("numberingSystem may be set by locale extension", () => { + const en = Intl.DateTimeFormat("en-u-nu-latn"); + expect(en.resolvedOptions().numberingSystem).toBe("latn"); + + const el = Intl.DateTimeFormat("el-u-nu-latn"); + expect(el.resolvedOptions().numberingSystem).toBe("latn"); + }); + + test("numberingSystem option overrides locale extension", () => { + const el = Intl.DateTimeFormat("el-u-nu-latn", { numberingSystem: "grek" }); + expect(el.resolvedOptions().numberingSystem).toBe("grek"); + }); + + test("numberingSystem option limited to known 'nu' values", () => { + ["latn", "arab"].forEach(numberingSystem => { + const en = Intl.DateTimeFormat("en", { numberingSystem: numberingSystem }); + expect(en.resolvedOptions().numberingSystem).toBe("latn"); + }); + + ["latn", "arab"].forEach(numberingSystem => { + const en = Intl.DateTimeFormat(`en-u-nu-${numberingSystem}`); + expect(en.resolvedOptions().numberingSystem).toBe("latn"); + }); + + ["latn", "grek"].forEach(numberingSystem => { + const el = Intl.DateTimeFormat("el", { numberingSystem: numberingSystem }); + expect(el.resolvedOptions().numberingSystem).toBe(numberingSystem); + }); + + ["latn", "grek"].forEach(numberingSystem => { + const el = Intl.DateTimeFormat(`el-u-nu-${numberingSystem}`); + expect(el.resolvedOptions().numberingSystem).toBe(numberingSystem); + }); + }); + + test("style", () => { + const en = new Intl.DateTimeFormat("en"); + expect(en.resolvedOptions().timeZone).toBe("UTC"); + + const el = new Intl.DateTimeFormat("el", { timeZone: "UTC" }); + expect(el.resolvedOptions().timeZone).toBe("UTC"); + }); + + test("dateStyle", () => { + const en = new Intl.DateTimeFormat("en"); + expect(en.resolvedOptions().dateStyle).toBeUndefined(); + + ["full", "long", "medium", "short"].forEach(style => { + const el = new Intl.DateTimeFormat("el", { dateStyle: style }); + expect(el.resolvedOptions().dateStyle).toBe(style); + }); + }); + + test("timeStyle", () => { + const en = new Intl.DateTimeFormat("en"); + expect(en.resolvedOptions().timeStyle).toBeUndefined(); + + ["full", "long", "medium", "short"].forEach(style => { + const el = new Intl.DateTimeFormat("el", { timeStyle: style }); + expect(el.resolvedOptions().timeStyle).toBe(style); + }); + }); +});