LibJS: Implement Intl.Locale.prototype.firstDayOfWeek

This is a normative change in the Intl Locale Info spec. See:
https://github.com/tc39/proposal-intl-locale-info/commit/f03a814
This commit is contained in:
Timothy Flynn 2023-10-03 10:18:18 -04:00 committed by Andreas Kling
parent b5875700e2
commit a357874c77
7 changed files with 227 additions and 31 deletions

View file

@ -191,6 +191,7 @@ namespace JS {
P(findLastIndex) \
P(findIndex) \
P(firstDay) \
P(firstDayOfWeek) \
P(fixed) \
P(flags) \
P(flat) \

View file

@ -182,6 +182,93 @@ StringView character_direction_of_locale(Locale const& locale_object)
return "ltr"sv;
}
// 1.1.8 WeekdayToNumber ( fw ), https://tc39.es/proposal-intl-locale-info/#sec-weekday-to-number
// FIXME: Spec issue: The spec definitions of WeekdayToNumber and WeekdayToString are backwards.
// https://github.com/tc39/proposal-intl-locale-info/issues/78
Optional<u8> weekday_to_number(StringView weekday)
{
struct WeekdayToNumber {
StringView type;
u8 number { 0 };
};
// Table 2: First Day Type and Number, https://tc39.es/proposal-intl-locale-info/#table-locale-first-day-type-number
static constexpr auto weekday_to_number_table = AK::Array {
WeekdayToNumber { "mon"sv, 1 },
WeekdayToNumber { "tue"sv, 2 },
WeekdayToNumber { "wed"sv, 3 },
WeekdayToNumber { "thu"sv, 4 },
WeekdayToNumber { "fri"sv, 5 },
WeekdayToNumber { "sat"sv, 6 },
WeekdayToNumber { "sun"sv, 7 },
};
// 1. For each row of Table 2, except the header row, in table order, do
for (auto const& row : weekday_to_number_table) {
// a. Let t be the name given in the Type column of the row.
auto type = row.type;
// b. Let n be the name given in the Number column of the row.
auto number = row.number;
// c. If fw is equal to t, return n.
if (weekday == type)
return number;
}
// 2. Assert: Should not reach here.
// FIXME: Spec issue: This is currently reachable if an invalid value is provided as a locale extension,
// for example "en-u-fw-100". We return "undefined" for now to avoid crashing.
// https://github.com/tc39/proposal-intl-locale-info/issues/78
return {};
}
// 1.1.9 WeekdayToString ( fw ), https://tc39.es/proposal-intl-locale-info/#sec-weekday-to-string
// FIXME: Spec issue: The spec definitions of WeekdayToNumber and WeekdayToString are backwards.
// https://github.com/tc39/proposal-intl-locale-info/issues/78
StringView weekday_to_string(StringView weekday)
{
struct WeekdayToString {
StringView value;
StringView type;
};
// Table 1: First Day Value and Type, https://tc39.es/proposal-intl-locale-info/#table-locale-first-day-option-type
static constexpr auto weekday_to_string_table = AK::Array {
WeekdayToString { "mon"sv, "mon"sv },
WeekdayToString { "tue"sv, "tue"sv },
WeekdayToString { "wed"sv, "wed"sv },
WeekdayToString { "thu"sv, "thu"sv },
WeekdayToString { "fri"sv, "fri"sv },
WeekdayToString { "sat"sv, "sat"sv },
WeekdayToString { "sun"sv, "sun"sv },
WeekdayToString { "0"sv, "sun"sv },
WeekdayToString { "1"sv, "mon"sv },
WeekdayToString { "2"sv, "tue"sv },
WeekdayToString { "3"sv, "wed"sv },
WeekdayToString { "4"sv, "thu"sv },
WeekdayToString { "5"sv, "fri"sv },
WeekdayToString { "6"sv, "sat"sv },
WeekdayToString { "7"sv, "sun"sv },
};
// 1. For each row of Table 1, except the header row, in table order, do
for (auto const& row : weekday_to_string_table) {
// a. Let v be the name given in the Value column of the row.
auto value = row.value;
// b. Let t be the name given in the Type column of the row.
auto type = row.type;
// c. If fw is equal to v, return t.
if (weekday == value)
return type;
}
// 2. Assert: Should not reach here.
VERIFY_NOT_REACHED();
}
static u8 weekday_to_integer(Optional<::Locale::Weekday> weekday, ::Locale::Weekday falllback)
{
// NOTE: This fallback will be used if LibUnicode data generation is disabled. Its value should
@ -224,7 +311,7 @@ static Vector<u8> weekend_of_locale(StringView locale)
return weekend;
}
// 1.1.8 WeekInfoOfLocale ( loc ), https://tc39.es/proposal-intl-locale-info/#sec-week-info-of-locale
// 1.1.10 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]].
@ -233,12 +320,20 @@ WeekInfo week_info_of_locale(Locale const& locale_object)
// 2. Assert: locale matches the unicode_locale_id production.
VERIFY(::Locale::parse_unicode_locale_id(locale).has_value());
// 3. Return a record whose fields are defined by Table 1, with values based on locale.
// 3. Let r be a record whose fields are defined by Table 3, with values based on locale.
WeekInfo week_info {};
week_info.minimal_days = ::Locale::get_locale_minimum_days(locale).value_or(1);
week_info.first_day = weekday_to_integer(::Locale::get_locale_first_day(locale), ::Locale::Weekday::Monday);
week_info.weekend = weekend_of_locale(locale);
// 4. Let fw be loc.[[FirstDayOfWeek]].
// 5. If fw is not undefined, then
if (locale_object.has_first_day_of_week()) {
// a. Set r.[[FirstDay]] to fw.
week_info.first_day = locale_object.first_day_of_week();
}
// 6. Return r.
return week_info;
}

View file

@ -27,12 +27,13 @@ public:
static constexpr auto relevant_extension_keys()
{
// 14.2.2 Internal slots, https://tc39.es/ecma402/#sec-intl.locale-internal-slots
// The value of the [[RelevantExtensionKeys]] internal slot is « "ca", "co", "hc", "kf", "kn", "nu" ».
// 1.3.2 Internal slots, https://tc39.es/proposal-intl-locale-info/#sec-intl.locale-internal-slots
// The value of the [[RelevantExtensionKeys]] internal slot is « "ca", "co", "fw", "hc", "kf", "kn", "nu" ».
// If %Collator%.[[RelevantExtensionKeys]] does not contain "kf", then remove "kf" from %Locale%.[[RelevantExtensionKeys]].
// If %Collator%.[[RelevantExtensionKeys]] does not contain "kn", then remove "kn" from %Locale%.[[RelevantExtensionKeys]].
// FIXME: We do not yet have an Intl.Collator object. For now, we behave as if "kf" and "kn" exist, as test262 depends on it.
return AK::Array { "ca"sv, "co"sv, "hc"sv, "kf"sv, "kn"sv, "nu"sv };
return AK::Array { "ca"sv, "co"sv, "fw"sv, "hc"sv, "kf"sv, "kn"sv, "nu"sv };
}
virtual ~Locale() override = default;
@ -52,6 +53,10 @@ public:
String const& collation() const { return m_collation.value(); }
void set_collation(String collation) { m_collation = move(collation); }
bool has_first_day_of_week() const { return m_first_day_of_week.has_value(); }
u8 first_day_of_week() const { return m_first_day_of_week.value(); }
void set_first_day_of_week(u8 first_day_of_week) { m_first_day_of_week = first_day_of_week; }
bool has_hour_cycle() const { return m_hour_cycle.has_value(); }
String const& hour_cycle() const { return m_hour_cycle.value(); }
void set_hour_cycle(String hour_cycle) { m_hour_cycle = move(hour_cycle); }
@ -70,6 +75,7 @@ private:
Optional<String> m_calendar; // [[Calendar]]
Optional<String> m_case_first; // [[CaseFirst]]
Optional<String> m_collation; // [[Collation]]
Optional<u8> m_first_day_of_week; // [[FirstDayOfWeek]]
Optional<String> m_hour_cycle; // [[HourCycle]]
Optional<String> m_numbering_system; // [[NumberingSystem]]
bool m_numeric { false }; // [[Numeric]]
@ -88,6 +94,8 @@ NonnullGCPtr<Array> hour_cycles_of_locale(VM&, Locale const& locale);
NonnullGCPtr<Array> numbering_systems_of_locale(VM&, Locale const&);
NonnullGCPtr<Array> time_zones_of_locale(VM&, StringView region);
StringView character_direction_of_locale(Locale const&);
Optional<u8> weekday_to_number(StringView);
StringView weekday_to_string(StringView);
WeekInfo week_info_of_locale(Locale const&);
}

View file

@ -20,6 +20,7 @@ struct LocaleAndKeys {
String locale;
Optional<String> ca;
Optional<String> co;
Optional<String> fw;
Optional<String> hc;
Optional<String> kf;
Optional<String> kn;
@ -139,6 +140,8 @@ static LocaleAndKeys apply_unicode_extension_to_tag(StringView tag, LocaleAndKey
return value.ca;
if (key == "co"sv)
return value.co;
if (key == "fw"sv)
return value.fw;
if (key == "hc"sv)
return value.hc;
if (key == "kf"sv)
@ -241,6 +244,7 @@ ThrowCompletionOr<Value> LocaleConstructor::call()
}
// 14.1.1 Intl.Locale ( tag [ , options ] ), https://tc39.es/ecma402/#sec-Intl.Locale
// 1.2.3 Intl.Locale ( tag [ , options ] ), https://tc39.es/proposal-intl-locale-info/#sec-Intl.Locale
ThrowCompletionOr<NonnullGCPtr<Object>> LocaleConstructor::construct(FunctionObject& new_target)
{
auto& vm = this->vm();
@ -251,7 +255,7 @@ ThrowCompletionOr<NonnullGCPtr<Object>> LocaleConstructor::construct(FunctionObj
// 2. Let relevantExtensionKeys be %Locale%.[[RelevantExtensionKeys]].
auto relevant_extension_keys = Locale::relevant_extension_keys();
// 3. Let internalSlotsList be « [[InitializedLocale]], [[Locale]], [[Calendar]], [[Collation]], [[HourCycle]], [[NumberingSystem]] ».
// 3. Let internalSlotsList be « [[InitializedLocale]], [[Locale]], [[Calendar]], [[Collation]], [[FirstDayOfWeek]], [[HourCycle]], [[NumberingSystem]] ».
// 4. If relevantExtensionKeys contains "kf", then
// a. Append [[CaseFirst]] as the last element of internalSlotsList.
// 5. If relevantExtensionKeys contains "kn", then
@ -299,51 +303,82 @@ ThrowCompletionOr<NonnullGCPtr<Object>> LocaleConstructor::construct(FunctionObj
// 18. Set opt.[[co]] to collation.
opt.co = TRY(get_string_option(vm, *options, vm.names.collation, ::Locale::is_type_identifier));
// 19. Let hc be ? GetOption(options, "hourCycle", string, « "h11", "h12", "h23", "h24" », undefined).
// 20. Set opt.[[hc]] to hc.
// 19. Let fw be ? GetOption(options, "firstDayOfWeek", "string", « "mon", "tue", "wed", "thu", "fri", "sat", "sun", "0", "1", "2", "3", "4", "5", "6", "7" », undefined).
auto first_day_of_week = TRY(get_string_option(vm, *options, vm.names.firstDayOfWeek, nullptr, AK::Array { "mon"sv, "tue"sv, "wed"sv, "thu"sv, "fri"sv, "sat"sv, "sun"sv, "0"sv, "1"sv, "2"sv, "3"sv, "4"sv, "5"sv, "6"sv, "7"sv }));
// 20. Let firstDay be undefined.
Optional<String> first_day_string;
// 21. If fw is not undefined, then
if (first_day_of_week.has_value()) {
// a. Set firstDay to !WeekdayToString(fw).
first_day_string = MUST(String::from_utf8(weekday_to_string(*first_day_of_week)));
}
// 22. Set opt.[[fw]] to firstDay.
opt.fw = move(first_day_string);
// 23. Let hc be ? GetOption(options, "hourCycle", string, « "h11", "h12", "h23", "h24" », undefined).
// 24. Set opt.[[hc]] to hc.
opt.hc = TRY(get_string_option(vm, *options, vm.names.hourCycle, nullptr, AK::Array { "h11"sv, "h12"sv, "h23"sv, "h24"sv }));
// 21. Let kf be ? GetOption(options, "caseFirst", string, « "upper", "lower", "false" », undefined).
// 22. Set opt.[[kf]] to kf.
// 25. Let kf be ? GetOption(options, "caseFirst", string, « "upper", "lower", "false" », undefined).
// 26. Set opt.[[kf]] to kf.
opt.kf = TRY(get_string_option(vm, *options, vm.names.caseFirst, nullptr, AK::Array { "upper"sv, "lower"sv, "false"sv }));
// 23. Let kn be ? GetOption(options, "numeric", boolean, empty, undefined).
// 27. Let kn be ? GetOption(options, "numeric", boolean, empty, undefined).
auto kn = TRY(get_option(vm, *options, vm.names.numeric, OptionType::Boolean, {}, Empty {}));
// 24. If kn is not undefined, set kn to ! ToString(kn).
// 25. Set opt.[[kn]] to kn.
// 28. If kn is not undefined, set kn to ! ToString(kn).
// 29. Set opt.[[kn]] to kn.
if (!kn.is_undefined())
opt.kn = TRY(kn.to_string(vm));
// 26. Let numberingSystem be ? GetOption(options, "numberingSystem", string, empty, undefined).
// 27. If numberingSystem is not undefined, then
// 30. Let numberingSystem be ? GetOption(options, "numberingSystem", string, empty, undefined).
// 31. If numberingSystem is not undefined, then
// a. If numberingSystem does not match the Unicode Locale Identifier type nonterminal, throw a RangeError exception.
// 28. Set opt.[[nu]] to numberingSystem.
// 32. Set opt.[[nu]] to numberingSystem.
opt.nu = TRY(get_string_option(vm, *options, vm.names.numberingSystem, ::Locale::is_type_identifier));
// 29. Let r be ! ApplyUnicodeExtensionToTag(tag, opt, relevantExtensionKeys).
// 33. Let r be ! ApplyUnicodeExtensionToTag(tag, opt, relevantExtensionKeys).
auto result = apply_unicode_extension_to_tag(tag, move(opt), relevant_extension_keys);
// 30. Set locale.[[Locale]] to r.[[locale]].
// 34. Set locale.[[Locale]] to r.[[locale]].
locale->set_locale(move(result.locale));
// 31. Set locale.[[Calendar]] to r.[[ca]].
// 35. Set locale.[[Calendar]] to r.[[ca]].
if (result.ca.has_value())
locale->set_calendar(result.ca.release_value());
// 32. Set locale.[[Collation]] to r.[[co]].
// 36. Set locale.[[Collation]] to r.[[co]].
if (result.co.has_value())
locale->set_collation(result.co.release_value());
// 33. Set locale.[[HourCycle]] to r.[[hc]].
// 37. Let firstDay be undefined.
Optional<u8> first_day_numeric;
// 38. If r.[[fw]] is not undefined, then
if (result.fw.has_value()) {
// a. Set firstDay to ! WeekdayToNumber(r.[[fw]]).
first_day_numeric = weekday_to_number(*result.fw);
}
// 39. Set locale.[[FirstDayOfWeek]] to firstDay.
if (first_day_numeric.has_value())
locale->set_first_day_of_week(*first_day_numeric);
// 40. Set locale.[[HourCycle]] to r.[[hc]].
if (result.hc.has_value())
locale->set_hour_cycle(result.hc.release_value());
// 34. If relevantExtensionKeys contains "kf", then
// 41. If relevantExtensionKeys contains "kf", then
if (relevant_extension_keys.span().contains_slow("kf"sv)) {
// a. Set locale.[[CaseFirst]] to r.[[kf]].
if (result.kf.has_value())
locale->set_case_first(result.kf.release_value());
}
// 35. If relevantExtensionKeys contains "kn", then
// 42. If relevantExtensionKeys contains "kn", then
if (relevant_extension_keys.span().contains_slow("kn"sv)) {
// a. If SameValue(r.[[kn]], "true") is true or r.[[kn]] is the empty String, then
if (result.kn.has_value() && (result.kn == "true"sv || result.kn->is_empty())) {
@ -357,11 +392,11 @@ ThrowCompletionOr<NonnullGCPtr<Object>> LocaleConstructor::construct(FunctionObj
}
}
// 36. Set locale.[[NumberingSystem]] to r.[[nu]].
// 43. Set locale.[[NumberingSystem]] to r.[[nu]].
if (result.nu.has_value())
locale->set_numbering_system(result.nu.release_value());
// 37. Return locale.
// 44. Return locale.
return locale;
}

View file

@ -39,6 +39,7 @@ void LocalePrototype::initialize(Realm& realm)
define_native_accessor(realm, vm.names.caseFirst, case_first, {}, Attribute::Configurable);
define_native_accessor(realm, vm.names.collation, collation, {}, Attribute::Configurable);
define_native_accessor(realm, vm.names.collations, collations, {}, Attribute::Configurable);
define_native_accessor(realm, vm.names.firstDayOfWeek, first_day_of_week, {}, Attribute::Configurable);
define_native_accessor(realm, vm.names.hourCycle, hour_cycle, {}, Attribute::Configurable);
define_native_accessor(realm, vm.names.hourCycles, hour_cycles, {}, Attribute::Configurable);
define_native_accessor(realm, vm.names.numberingSystem, numbering_system, {}, Attribute::Configurable);
@ -141,6 +142,17 @@ JS_DEFINE_NATIVE_FUNCTION(LocalePrototype::base_name)
JS_ENUMERATE_LOCALE_KEYWORD_PROPERTIES
#undef __JS_ENUMERATE
// 1.4.10 get Intl.Locale.prototype.firstDayOfWeek, https://tc39.es/proposal-intl-locale-info/#sec-Intl.Locale.prototype.firstDayOfWeek
JS_DEFINE_NATIVE_FUNCTION(LocalePrototype::first_day_of_week)
{
// 1. Let loc be the this value.
// 2. Perform ? RequireInternalSlot(loc, [[InitializedLocale]]).
auto locale_object = TRY(typed_this_object(vm));
// 3. Return loc.[[FirstDayOfWeek]].
return locale_object->has_first_day_of_week() ? Value { locale_object->first_day_of_week() } : js_undefined();
}
// 14.3.11 get Intl.Locale.prototype.numeric, https://tc39.es/ecma402/#sec-Intl.Locale.prototype.numeric
JS_DEFINE_NATIVE_FUNCTION(LocalePrototype::numeric)
{
@ -217,10 +229,10 @@ JS_DEFINE_NATIVE_FUNCTION(LocalePrototype::region)
__JS_ENUMERATE(hour_cycles) \
__JS_ENUMERATE(numbering_systems)
// 1.4.16 get Intl.Locale.prototype.calendars, https://tc39.es/proposal-intl-locale-info/#sec-Intl.Locale.prototype.calendars
// 1.4.17 get Intl.Locale.prototype.collations, https://tc39.es/proposal-intl-locale-info/#sec-Intl.Locale.prototype.collations
// 1.4.18 get Intl.Locale.prototype.hourCycles, https://tc39.es/proposal-intl-locale-info/#sec-Intl.Locale.prototype.hourCycles
// 1.4.19 get Intl.Locale.prototype.numberingSystems, https://tc39.es/proposal-intl-locale-info/#sec-Intl.Locale.prototype.numberingSystems
// 1.4.17 get Intl.Locale.prototype.calendars, https://tc39.es/proposal-intl-locale-info/#sec-Intl.Locale.prototype.calendars
// 1.4.18 get Intl.Locale.prototype.collations, https://tc39.es/proposal-intl-locale-info/#sec-Intl.Locale.prototype.collations
// 1.4.19 get Intl.Locale.prototype.hourCycles, https://tc39.es/proposal-intl-locale-info/#sec-Intl.Locale.prototype.hourCycles
// 1.4.20 get Intl.Locale.prototype.numberingSystems, https://tc39.es/proposal-intl-locale-info/#sec-Intl.Locale.prototype.numberingSystems
#define __JS_ENUMERATE(keyword) \
JS_DEFINE_NATIVE_FUNCTION(LocalePrototype::keyword) \
{ \
@ -230,7 +242,7 @@ JS_DEFINE_NATIVE_FUNCTION(LocalePrototype::region)
JS_ENUMERATE_LOCALE_INFO_PROPERTIES
#undef __JS_ENUMERATE
// 1.4.20 get Intl.Locale.prototype.timeZones, https://tc39.es/proposal-intl-locale-info/#sec-Intl.Locale.prototype.timeZones
// 1.4.21 get Intl.Locale.prototype.timeZones, https://tc39.es/proposal-intl-locale-info/#sec-Intl.Locale.prototype.timeZones
JS_DEFINE_NATIVE_FUNCTION(LocalePrototype::time_zones)
{
// 1. Let loc be the this value.
@ -248,7 +260,7 @@ JS_DEFINE_NATIVE_FUNCTION(LocalePrototype::time_zones)
return time_zones_of_locale(vm, locale->language_id.region.value());
}
// 1.4.21 get Intl.Locale.prototype.textInfo, https://tc39.es/proposal-intl-locale-info/#sec-Intl.Locale.prototype.textInfo
// 1.4.22 get Intl.Locale.prototype.textInfo, https://tc39.es/proposal-intl-locale-info/#sec-Intl.Locale.prototype.textInfo
JS_DEFINE_NATIVE_FUNCTION(LocalePrototype::text_info)
{
auto& realm = *vm.current_realm();
@ -270,7 +282,7 @@ 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
// 1.4.23 get Intl.Locale.prototype.weekInfo, https://tc39.es/proposal-intl-locale-info/#sec-Intl.Locale.prototype.weekInfo
JS_DEFINE_NATIVE_FUNCTION(LocalePrototype::week_info)
{
auto& realm = *vm.current_realm();

View file

@ -31,6 +31,7 @@ private:
JS_DECLARE_NATIVE_FUNCTION(case_first);
JS_DECLARE_NATIVE_FUNCTION(collation);
JS_DECLARE_NATIVE_FUNCTION(collations);
JS_DECLARE_NATIVE_FUNCTION(first_day_of_week);
JS_DECLARE_NATIVE_FUNCTION(hour_cycle);
JS_DECLARE_NATIVE_FUNCTION(hour_cycles);
JS_DECLARE_NATIVE_FUNCTION(numbering_system);

View file

@ -0,0 +1,44 @@
describe("errors", () => {
test("called on non-Locale object", () => {
expect(() => {
Intl.Locale.prototype.firstDayOfWeek;
}).toThrowWithMessage(TypeError, "Not an object of type Intl.Locale");
});
test("invalid options", () => {
[100, Infinity, NaN, "hello", 152n, true].forEach(value => {
expect(() => {
new Intl.Locale("en", { firstDayOfWeek: value }).firstDayOfWeek;
}).toThrowWithMessage(
RangeError,
`${value} is not a valid value for option firstDayOfWeek`
);
});
});
// FIXME: Spec issue: It is not yet clear if the following invalid values should throw. For now, this tests our
// existing workaround behavior, which returns "undefined" for invalid extensions.
// https://github.com/tc39/proposal-intl-locale-info/issues/78
test("invalid extensions", () => {
[100, Infinity, NaN, "hello", 152n, true].forEach(value => {
expect(new Intl.Locale(`en-u-fw-${value}`).firstDayOfWeek).toBeUndefined();
});
});
});
describe("normal behavior", () => {
test("valid options", () => {
expect(new Intl.Locale("en").firstDayOfWeek).toBeUndefined();
["mon", "tue", "wed", "thu", "fri", "sat", "sun"].forEach((day, index) => {
expect(new Intl.Locale(`en-u-fw-${day}`).firstDayOfWeek).toBe(index + 1);
expect(new Intl.Locale("en", { firstDayOfWeek: day }).firstDayOfWeek).toBe(index + 1);
expect(new Intl.Locale("en", { firstDayOfWeek: index + 1 }).firstDayOfWeek).toBe(
index + 1
);
expect(new Intl.Locale("en-u-fw-mon", { firstDayOfWeek: day }).firstDayOfWeek).toBe(
index + 1
);
});
});
});