diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 01c6d2f560..320c1ee618 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -174,6 +174,7 @@ namespace JS { P(findLast) \ P(findLastIndex) \ P(findIndex) \ + P(firstDay) \ P(fixed) \ P(flags) \ P(flat) \ @@ -330,6 +331,7 @@ namespace JS { P(milliseconds) \ P(millisecondsDisplay) \ P(min) \ + P(minimalDays) \ P(minimize) \ P(maximumFractionDigits) \ P(maximumSignificantDigits) \ @@ -531,8 +533,10 @@ namespace JS { P(valueOf) \ P(values) \ P(warn) \ + P(weekInfo) \ P(weekOfYear) \ P(weekday) \ + P(weekend) \ P(weeks) \ P(weeksDisplay) \ P(with) \ diff --git a/Userland/Libraries/LibJS/Runtime/Intl/Locale.cpp b/Userland/Libraries/LibJS/Runtime/Intl/Locale.cpp index c4429f2bce..130fb2eeda 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/Locale.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/Locale.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace JS::Intl { @@ -186,4 +187,64 @@ StringView character_direction_of_locale(Locale const& locale_object) return "ltr"sv; } +static u8 weekday_to_integer(Optional weekday, Unicode::Weekday falllback) +{ + // NOTE: This fallback will be used if LibUnicode data generation is disabled. Its value should + // be that of the default region ("001") in the CLDR. + switch (weekday.value_or(falllback)) { + case Unicode::Weekday::Monday: + return 1; + case Unicode::Weekday::Tuesday: + return 2; + case Unicode::Weekday::Wednesday: + return 3; + case Unicode::Weekday::Thursday: + return 4; + case Unicode::Weekday::Friday: + return 5; + case Unicode::Weekday::Saturday: + return 6; + case Unicode::Weekday::Sunday: + return 7; + } + + VERIFY_NOT_REACHED(); +} + +static Vector weekend_of_locale(StringView locale) +{ + auto weekend_start = weekday_to_integer(Unicode::get_locale_weekend_start(locale), Unicode::Weekday::Saturday); + auto weekend_end = weekday_to_integer(Unicode::get_locale_weekend_end(locale), Unicode::Weekday::Sunday); + + // There currently aren't any regions in the CLDR which wrap around from Sunday (7) to Monday (1). + // If this changes, this logic will need to be updated to handle that. + VERIFY(weekend_start <= weekend_end); + + Vector weekend; + weekend.ensure_capacity(weekend_end - weekend_start + 1); + + for (auto day = weekend_start; day <= weekend_end; ++day) + weekend.unchecked_append(day); + + return weekend; +} + +// 1.1.8 WeekInfoOfLocale ( loc ), https://tc39.es/proposal-intl-locale-info/#sec-week-info-of-locale +WeekInfo week_info_of_locale(Locale const& locale_object) +{ + // 1. Let locale be loc.[[Locale]]. + auto const& locale = locale_object.locale(); + + // 2. Assert: locale matches the unicode_locale_id production. + VERIFY(Unicode::parse_unicode_locale_id(locale).has_value()); + + // 3. Return a record whose fields are defined by Table 1, with values based on locale. + WeekInfo week_info {}; + week_info.minimal_days = Unicode::get_locale_minimum_days(locale).value_or(1); + week_info.first_day = weekday_to_integer(Unicode::get_locale_first_day(locale), Unicode::Weekday::Monday); + week_info.weekend = weekend_of_locale(locale); + + return week_info; +} + } diff --git a/Userland/Libraries/LibJS/Runtime/Intl/Locale.h b/Userland/Libraries/LibJS/Runtime/Intl/Locale.h index 33c6b80321..70017ae1ce 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/Locale.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/Locale.h @@ -74,11 +74,19 @@ private: bool m_numeric { false }; // [[Numeric]] }; +// Table 1: WeekInfo Record Fields, https://tc39.es/proposal-intl-locale-info/#table-locale-weekinfo-record +struct WeekInfo { + u8 minimal_days { 0 }; // [[MinimalDays]] + u8 first_day { 0 }; // [[FirstDay]] + Vector weekend; // [[Weekend]] +}; + Array* calendars_of_locale(GlobalObject& global_object, Locale const& locale); Array* collations_of_locale(GlobalObject& global_object, Locale const& locale); Array* hour_cycles_of_locale(GlobalObject& global_object, Locale const& locale); Array* numbering_systems_of_locale(GlobalObject& global_object, Locale const& locale); Array* time_zones_of_locale(GlobalObject& global_object, StringView region); StringView character_direction_of_locale(Locale const& locale); +WeekInfo week_info_of_locale(Locale const& locale); } diff --git a/Userland/Libraries/LibJS/Runtime/Intl/LocalePrototype.cpp b/Userland/Libraries/LibJS/Runtime/Intl/LocalePrototype.cpp index 02a5c25efa..9fa2c7e734 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/LocalePrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/LocalePrototype.cpp @@ -49,6 +49,7 @@ void LocalePrototype::initialize(GlobalObject& global_object) define_native_accessor(vm.names.region, region, {}, Attribute::Configurable); define_native_accessor(vm.names.timeZones, time_zones, {}, Attribute::Configurable); define_native_accessor(vm.names.textInfo, text_info, {}, Attribute::Configurable); + define_native_accessor(vm.names.weekInfo, week_info, {}, Attribute::Configurable); } // 14.3.3 Intl.Locale.prototype.maximize ( ), https://tc39.es/ecma402/#sec-Intl.Locale.prototype.maximize @@ -263,4 +264,33 @@ JS_DEFINE_NATIVE_FUNCTION(LocalePrototype::text_info) return info; } +// 1.4.22 get Intl.Locale.prototype.weekInfo, https://tc39.es/proposal-intl-locale-info/#sec-Intl.Locale.prototype.weekInfo +JS_DEFINE_NATIVE_FUNCTION(LocalePrototype::week_info) +{ + // 1. Let loc be the this value. + // 2. Perform ? RequireInternalSlot(loc, [[InitializedLocale]]). + [[maybe_unused]] auto* locale_object = TRY(typed_this_object(global_object)); + + // 3. Let info be ! ObjectCreate(%Object.prototype%). + auto* info = Object::create(global_object, global_object.object_prototype()); + + // 4. Let wi be ! WeekInfoOfLocale(loc). + auto week_info = week_info_of_locale(*locale_object); + + // 5. Let we be ! CreateArrayFromList( wi.[[Weekend]] ). + auto weekend = Array::create_from(global_object, week_info.weekend, [](auto day) { return Value(day); }); + + // 6. Perform ! CreateDataPropertyOrThrow(info, "firstDay", wi.[[FirstDay]]). + MUST(info->create_data_property_or_throw(vm.names.firstDay, Value(week_info.first_day))); + + // 7. Perform ! CreateDataPropertyOrThrow(info, "weekend", we). + MUST(info->create_data_property_or_throw(vm.names.weekend, weekend)); + + // 8. Perform ! CreateDataPropertyOrThrow(info, "minimalDays", wi.[[MinimalDays]]). + MUST(info->create_data_property_or_throw(vm.names.minimalDays, Value(week_info.minimal_days))); + + // 9. Return info. + return info; +} + } diff --git a/Userland/Libraries/LibJS/Runtime/Intl/LocalePrototype.h b/Userland/Libraries/LibJS/Runtime/Intl/LocalePrototype.h index 781d22f42f..273ec5d563 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/LocalePrototype.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/LocalePrototype.h @@ -40,6 +40,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(region); JS_DECLARE_NATIVE_FUNCTION(time_zones); JS_DECLARE_NATIVE_FUNCTION(text_info); + JS_DECLARE_NATIVE_FUNCTION(week_info); }; } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/Locale/Locale.prototype.weekInfo.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/Locale/Locale.prototype.weekInfo.js new file mode 100644 index 0000000000..f6a6e12da6 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/Locale/Locale.prototype.weekInfo.js @@ -0,0 +1,68 @@ +describe("errors", () => { + test("called on non-Locale object", () => { + expect(() => { + Intl.Locale.prototype.weekInfo; + }).toThrowWithMessage(TypeError, "Not an object of type Intl.Locale"); + }); +}); + +describe("normal behavior", () => { + test("basic functionality", () => { + const weekInfo = new Intl.Locale("en-US").weekInfo; + + expect(weekInfo).toBeDefined(); + expect(Object.getPrototypeOf(weekInfo)).toBe(Object.prototype); + + expect(weekInfo.firstDay).toBeDefined(); + expect(Object.getPrototypeOf(weekInfo.firstDay)).toBe(Number.prototype); + expect(weekInfo.firstDay).toBe(7); + + expect(weekInfo.weekend).toBeDefined(); + expect(Array.isArray(weekInfo.weekend)).toBeTrue(); + expect(weekInfo.weekend).toEqual([6, 7]); + + expect(weekInfo.minimalDays).toBeDefined(); + expect(Object.getPrototypeOf(weekInfo.minimalDays)).toBe(Number.prototype); + expect(weekInfo.minimalDays).toBe(1); + }); + + test("regions with CLDR-specified firstDay", () => { + expect(new Intl.Locale("en-AG").weekInfo.firstDay).toBe(7); + expect(new Intl.Locale("en-SY").weekInfo.firstDay).toBe(6); + expect(new Intl.Locale("en-MV").weekInfo.firstDay).toBe(5); + }); + + test("firstDay falls back to default region 001", () => { + expect(new Intl.Locale("en-AC").weekInfo.firstDay).toBe(1); + }); + + test("regions with CLDR-specified weekend", () => { + expect(new Intl.Locale("en-AF").weekInfo.weekend).toEqual([4, 5]); + expect(new Intl.Locale("en-IN").weekInfo.weekend).toEqual([7]); + expect(new Intl.Locale("en-YE").weekInfo.weekend).toEqual([5, 6]); + }); + + test("weekend falls back to default region 001", () => { + expect(new Intl.Locale("en-AC").weekInfo.weekend).toEqual([6, 7]); + }); + + test("regions with CLDR-specified minimalDays", () => { + expect(new Intl.Locale("en-AD").weekInfo.minimalDays).toBe(4); + expect(new Intl.Locale("en-CZ").weekInfo.minimalDays).toBe(4); + }); + + test("minimalDays falls back to default region 001", () => { + expect(new Intl.Locale("en-AC").weekInfo.minimalDays).toBe(1); + }); + + test("likely regional subtags are added to locales without a region", () => { + const defaultRegion = new Intl.Locale("en-001").weekInfo; + + // "en" expands to "en-US" when likely subtags are added. + const en = new Intl.Locale("en").weekInfo; + const enUS = new Intl.Locale("en-US").weekInfo; + + expect(en).toEqual(enUS); + expect(en).not.toEqual(defaultRegion); + }); +});