ladybird/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp
Timothy Flynn 4fc0fba646 LibCore+LibJS+LibUnicode: Port retrieving available time zones to ICU
This required updating some LibJS spec steps to their latest versions,
as the data expected by the old steps does not quite match the APIs that
are available with the ICU. The new spec steps are much more aligned.
2024-06-26 10:14:02 +02:00

718 lines
30 KiB
C++

/*
* Copyright (c) 2021-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/AllOf.h>
#include <AK/CharacterTypes.h>
#include <AK/Find.h>
#include <AK/QuickSort.h>
#include <AK/TypeCasts.h>
#include <LibJS/Runtime/Array.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/Intl/AbstractOperations.h>
#include <LibJS/Runtime/Intl/Locale.h>
#include <LibJS/Runtime/Intl/SingleUnitIdentifiers.h>
#include <LibJS/Runtime/VM.h>
#include <LibJS/Runtime/ValueInlines.h>
#include <LibUnicode/TimeZone.h>
#include <LibUnicode/UnicodeKeywords.h>
namespace JS::Intl {
Optional<LocaleKey> locale_key_from_value(Value value)
{
if (value.is_undefined())
return OptionalNone {};
if (value.is_null())
return Empty {};
if (value.is_string())
return value.as_string().utf8_string();
VERIFY_NOT_REACHED();
}
// 6.2.1 IsStructurallyValidLanguageTag ( locale ), https://tc39.es/ecma402/#sec-isstructurallyvalidlanguagetag
bool is_structurally_valid_language_tag(StringView locale)
{
auto contains_duplicate_variant = [&](auto& variants) {
if (variants.is_empty())
return false;
quick_sort(variants);
for (size_t i = 0; i < variants.size() - 1; ++i) {
if (variants[i].equals_ignoring_case(variants[i + 1]))
return true;
}
return false;
};
// 1. Let lowerLocale be the ASCII-lowercase of locale.
// NOTE: LibUnicode's parsing is case-insensitive.
// 2. If lowerLocale cannot be matched by the unicode_locale_id Unicode locale nonterminal, return false.
auto locale_id = Unicode::parse_unicode_locale_id(locale);
if (!locale_id.has_value())
return false;
// 3. If lowerLocale uses any of the backwards compatibility syntax described in Unicode Technical Standard #35 Part 1 Core,
// Section 3.3 BCP 47 Conformance, return false.
// https://unicode.org/reports/tr35/#BCP_47_Conformance
if (locale.contains('_') || locale_id->language_id.is_root || !locale_id->language_id.language.has_value())
return false;
// 4. Let languageId be the longest prefix of lowerLocale matched by the unicode_language_id Unicode locale nonterminal.
auto& language_id = locale_id->language_id;
// 5. Let variants be GetLocaleVariants(languageId).
// 6. If variants is not undefined, then
if (auto& variants = language_id.variants; !variants.is_empty()) {
// a. If variants contains any duplicate subtags, return false.
if (contains_duplicate_variant(variants))
return false;
}
HashTable<char> unique_keys;
// 7. Let allExtensions be the suffix of lowerLocale following languageId.
// 8. If allExtensions contains a substring matched by the pu_extensions Unicode locale nonterminal, let extensions be
// the prefix of allExtensions preceding the longest such substring. Otherwise, let extensions be allExtensions.
// 9. If extensions is not the empty String, then
for (auto& extension : locale_id->extensions) {
char key = extension.visit(
[](Unicode::LocaleExtension const&) { return 'u'; },
[](Unicode::TransformedExtension const&) { return 't'; },
[](Unicode::OtherExtension const& ext) { return static_cast<char>(to_ascii_lowercase(ext.key)); });
// a. If extensions contains any duplicate singleton subtags, return false.
if (unique_keys.set(key) != HashSetResult::InsertedNewEntry)
return false;
// b. Let transformExtension be the longest substring of extensions matched by the transformed_extensions Unicode
// locale nonterminal. If there is no such substring, return true.
if (auto* transformed = extension.get_pointer<Unicode::TransformedExtension>()) {
// c. Assert: The substring of transformExtension from 0 to 3 is "-t-".
// d. Let tPrefix be the substring of transformExtension from 3.
// e. Let tlang be the longest prefix of tPrefix matched by the tlang Unicode locale nonterminal. If there is
// no such prefix, return true.
auto& transformed_language = transformed->language;
if (!transformed_language.has_value())
continue;
// f. Let tlangRefinements be the longest suffix of tlang following a non-empty prefix matched by the
// unicode_language_subtag Unicode locale nonterminal.
auto& transformed_refinements = transformed_language->variants;
// g. If tlangRefinements contains any duplicate substrings matched greedily by the unicode_variant_subtag
// Unicode locale nonterminal, return false.
if (contains_duplicate_variant(transformed_refinements))
return false;
}
}
// 10. Return true.
return true;
}
// 6.2.2 CanonicalizeUnicodeLocaleId ( locale ), https://tc39.es/ecma402/#sec-canonicalizeunicodelocaleid
String canonicalize_unicode_locale_id(StringView locale)
{
return Unicode::canonicalize_unicode_locale_id(locale);
}
// 6.3.1 IsWellFormedCurrencyCode ( currency ), https://tc39.es/ecma402/#sec-iswellformedcurrencycode
bool is_well_formed_currency_code(StringView currency)
{
// 1. If the length of currency is not 3, return false.
if (currency.length() != 3)
return false;
// 2. Let normalized be the ASCII-uppercase of currency.
// 3. If normalized contains any code unit outside of 0x0041 through 0x005A (corresponding to Unicode characters LATIN CAPITAL LETTER A through LATIN CAPITAL LETTER Z), return false.
if (!all_of(currency, is_ascii_alpha))
return false;
// 4. Return true.
return true;
}
// 6.5.1 AvailableNamedTimeZoneIdentifiers ( ), https://tc39.es/ecma402/#sup-availablenamedtimezoneidentifiers
Vector<TimeZoneIdentifier> const& available_named_time_zone_identifiers()
{
// It is recommended that the result of AvailableNamedTimeZoneIdentifiers remains the same for the lifetime of the surrounding agent.
static auto named_time_zone_identifiers = []() {
// 1. Let identifiers be a List containing the String value of each Zone or Link name in the IANA Time Zone Database.
auto const& identifiers = Unicode::available_time_zones();
// 2. Assert: No element of identifiers is an ASCII-case-insensitive match for any other element.
// 3. Assert: Every element of identifiers identifies a Zone or Link name in the IANA Time Zone Database.
// 4. Sort identifiers according to lexicographic code unit order.
// NOTE: All of the above is handled by LibUnicode.
// 5. Let result be a new empty List.
Vector<TimeZoneIdentifier> result;
result.ensure_capacity(identifiers.size());
bool found_utc = false;
// 6. For each element identifier of identifiers, do
for (auto const& identifier : identifiers) {
// a. Let primary be identifier.
auto primary = identifier;
// b. If identifier is a Link name and identifier is not "UTC", then
if (identifier != "UTC"sv) {
if (auto resolved = Unicode::resolve_primary_time_zone(identifier); resolved.has_value() && identifier != resolved) {
// i. Set primary to the Zone name that identifier resolves to, according to the rules for resolving Link
// names in the IANA Time Zone Database.
primary = resolved.release_value();
// ii. NOTE: An implementation may need to resolve identifier iteratively.
}
}
// c. If primary is one of "Etc/UTC", "Etc/GMT", or "GMT", set primary to "UTC".
if (primary.is_one_of("Etc/UTC"sv, "Etc/GMT"sv, "GMT"sv))
primary = "UTC"_string;
// d. Let record be the Time Zone Identifier Record { [[Identifier]]: identifier, [[PrimaryIdentifier]]: primary }.
TimeZoneIdentifier record { .identifier = identifier, .primary_identifier = primary };
// e. Append record to result.
result.unchecked_append(move(record));
if (!found_utc && identifier == "UTC"sv && primary == "UTC"sv)
found_utc = true;
}
// 7. Assert: result contains a Time Zone Identifier Record r such that r.[[Identifier]] is "UTC" and r.[[PrimaryIdentifier]] is "UTC".
VERIFY(found_utc);
// 8. Return result.
return result;
}();
return named_time_zone_identifiers;
}
// 6.5.2 GetAvailableNamedTimeZoneIdentifier ( timeZoneIdentifier ), https://tc39.es/ecma402/#sec-getavailablenamedtimezoneidentifier
Optional<TimeZoneIdentifier const&> get_available_named_time_zone_identifier(StringView time_zone_identifier)
{
// 1. For each element record of AvailableNamedTimeZoneIdentifiers(), do
for (auto const& record : available_named_time_zone_identifiers()) {
// a. If record.[[Identifier]] is an ASCII-case-insensitive match for timeZoneIdentifier, return record.
if (record.identifier.equals_ignoring_ascii_case(time_zone_identifier))
return record;
}
// 2. Return EMPTY.
return {};
}
// 6.6.1 IsWellFormedUnitIdentifier ( unitIdentifier ), https://tc39.es/ecma402/#sec-iswellformedunitidentifier
bool is_well_formed_unit_identifier(StringView unit_identifier)
{
// 6.6.2 IsSanctionedSingleUnitIdentifier ( unitIdentifier ), https://tc39.es/ecma402/#sec-issanctionedsingleunitidentifier
constexpr auto is_sanctioned_single_unit_identifier = [](StringView unit_identifier) {
// 1. If unitIdentifier is listed in Table 2 below, return true.
// 2. Else, return false.
static constexpr auto sanctioned_units = sanctioned_single_unit_identifiers();
return find(sanctioned_units.begin(), sanctioned_units.end(), unit_identifier) != sanctioned_units.end();
};
// 1. If ! IsSanctionedSingleUnitIdentifier(unitIdentifier) is true, then
if (is_sanctioned_single_unit_identifier(unit_identifier)) {
// a. Return true.
return true;
}
// 2. Let i be StringIndexOf(unitIdentifier, "-per-", 0).
auto indices = unit_identifier.find_all("-per-"sv);
// 3. If i is -1 or StringIndexOf(unitIdentifier, "-per-", i + 1) is not -1, then
if (indices.size() != 1) {
// a. Return false.
return false;
}
// 4. Assert: The five-character substring "-per-" occurs exactly once in unitIdentifier, at index i.
// NOTE: We skip this because the indices vector being of size 1 already verifies this invariant.
// 5. Let numerator be the substring of unitIdentifier from 0 to i.
auto numerator = unit_identifier.substring_view(0, indices[0]);
// 6. Let denominator be the substring of unitIdentifier from i + 5.
auto denominator = unit_identifier.substring_view(indices[0] + 5);
// 7. If ! IsSanctionedSingleUnitIdentifier(numerator) and ! IsSanctionedSingleUnitIdentifier(denominator) are both true, then
if (is_sanctioned_single_unit_identifier(numerator) && is_sanctioned_single_unit_identifier(denominator)) {
// a. Return true.
return true;
}
// 8. Return false.
return false;
}
// 9.2.1 CanonicalizeLocaleList ( locales ), https://tc39.es/ecma402/#sec-canonicalizelocalelist
ThrowCompletionOr<Vector<String>> canonicalize_locale_list(VM& vm, Value locales)
{
auto& realm = *vm.current_realm();
// 1. If locales is undefined, then
if (locales.is_undefined()) {
// a. Return a new empty List.
return Vector<String> {};
}
// 2. Let seen be a new empty List.
Vector<String> seen;
Object* object = nullptr;
// 3. If Type(locales) is String or Type(locales) is Object and locales has an [[InitializedLocale]] internal slot, then
if (locales.is_string() || (locales.is_object() && is<Locale>(locales.as_object()))) {
// a. Let O be CreateArrayFromList(« locales »).
object = Array::create_from(realm, { locales });
}
// 4. Else,
else {
// a. Let O be ? ToObject(locales).
object = TRY(locales.to_object(vm));
}
// 5. Let len be ? ToLength(? Get(O, "length")).
auto length_value = TRY(object->get(vm.names.length));
auto length = TRY(length_value.to_length(vm));
// 6. Let k be 0.
// 7. Repeat, while k < len,
for (size_t k = 0; k < length; ++k) {
// a. Let Pk be ToString(k).
auto property_key = PropertyKey { k };
// b. Let kPresent be ? HasProperty(O, Pk).
auto key_present = TRY(object->has_property(property_key));
// c. If kPresent is true, then
if (key_present) {
// i. Let kValue be ? Get(O, Pk).
auto key_value = TRY(object->get(property_key));
// ii. If Type(kValue) is not String or Object, throw a TypeError exception.
if (!key_value.is_string() && !key_value.is_object())
return vm.throw_completion<TypeError>(ErrorType::NotAnObjectOrString, key_value);
String tag;
// iii. If Type(kValue) is Object and kValue has an [[InitializedLocale]] internal slot, then
if (key_value.is_object() && is<Locale>(key_value.as_object())) {
// 1. Let tag be kValue.[[Locale]].
tag = static_cast<Locale const&>(key_value.as_object()).locale();
}
// iv. Else,
else {
// 1. Let tag be ? ToString(kValue).
tag = TRY(key_value.to_string(vm));
}
// v. If ! IsStructurallyValidLanguageTag(tag) is false, throw a RangeError exception.
if (!is_structurally_valid_language_tag(tag))
return vm.throw_completion<RangeError>(ErrorType::IntlInvalidLanguageTag, tag);
// vi. Let canonicalizedTag be ! CanonicalizeUnicodeLocaleId(tag).
auto canonicalized_tag = canonicalize_unicode_locale_id(tag);
// vii. If canonicalizedTag is not an element of seen, append canonicalizedTag as the last element of seen.
if (!seen.contains_slow(canonicalized_tag))
seen.append(move(canonicalized_tag));
}
// d. Increase k by 1.
}
return seen;
}
// 9.2.3 LookupMatchingLocaleByPrefix ( availableLocales, requestedLocales ), https://tc39.es/ecma402/#sec-lookupmatchinglocalebyprefix
Optional<MatchedLocale> lookup_matching_locale_by_prefix(ReadonlySpan<String> requested_locales)
{
// 1. For each element locale of requestedLocales, do
for (auto locale : requested_locales) {
auto locale_id = Unicode::parse_unicode_locale_id(locale);
VERIFY(locale_id.has_value());
// a. Let extension be empty.
Optional<Unicode::Extension> extension;
String locale_without_extension;
// b. If locale contains a Unicode locale extension sequence, then
if (auto extensions = locale_id->remove_extension_type<Unicode::LocaleExtension>(); !extensions.is_empty()) {
VERIFY(extensions.size() == 1);
// i. Set extension to the Unicode locale extension sequence of locale.
extension = extensions.take_first();
// ii. Set locale to the String value that is locale with any Unicode locale extension sequences removed.
locale = locale_id->to_string();
}
// c. Let prefix be locale.
StringView prefix { locale };
// d. Repeat, while prefix is not the empty String,
while (!prefix.is_empty()) {
// i. If availableLocales contains prefix, return the Record { [[locale]]: prefix, [[extension]]: extension }.
if (Unicode::is_locale_available(prefix))
return MatchedLocale { MUST(String::from_utf8(prefix)), move(extension) };
// ii. If prefix contains "-" (code unit 0x002D HYPHEN-MINUS), let pos be the index into prefix of the last
// occurrence of "-"; else let pos be 0.
auto position = prefix.find_last('-').value_or(0);
// iii. Repeat, while pos ≥ 2 and the substring of prefix from pos - 2 to pos - 1 is "-",
while (position >= 2 && prefix.substring_view(position - 2, 1) == '-') {
// 1. Set pos to pos - 2.
position -= 2;
}
// iv. Set prefix to the substring of prefix from 0 to pos.
prefix = prefix.substring_view(0, position);
}
}
// 2. Return undefined.
return {};
}
// 9.2.4 LookupMatchingLocaleByBestFit ( availableLocales, requestedLocales ), https://tc39.es/ecma402/#sec-lookupmatchinglocalebybestfit
Optional<MatchedLocale> lookup_matching_locale_by_best_fit(ReadonlySpan<String> requested_locales)
{
// The algorithm is implementation dependent, but should produce results that a typical user of the requested locales
// would consider at least as good as those produced by the LookupMatchingLocaleByPrefix algorithm.
return lookup_matching_locale_by_prefix(requested_locales);
}
// 9.2.6 InsertUnicodeExtensionAndCanonicalize ( locale, attributes, keywords ), https://tc39.es/ecma402/#sec-insert-unicode-extension-and-canonicalize
String insert_unicode_extension_and_canonicalize(Unicode::LocaleID locale, Vector<String> attributes, Vector<Unicode::Keyword> keywords)
{
// Note: This implementation differs from the spec in how the extension is inserted. The spec assumes
// the input to this method is a string, and is written such that operations are performed on parts
// of that string. LibUnicode gives us the parsed locale in a structure, so we can mutate that
// structure directly.
locale.extensions.append(Unicode::LocaleExtension { move(attributes), move(keywords) });
// 10. Return CanonicalizeUnicodeLocaleId(newLocale).
return JS::Intl::canonicalize_unicode_locale_id(locale.to_string());
}
template<typename T>
static auto& find_key_in_value(T& value, StringView key)
{
if (key == "ca"sv)
return value.ca;
if (key == "co"sv)
return value.co;
if (key == "hc"sv)
return value.hc;
if (key == "kf"sv)
return value.kf;
if (key == "kn"sv)
return value.kn;
if (key == "nu"sv)
return value.nu;
// If you hit this point, you must add any missing keys from [[RelevantExtensionKeys]] to LocaleOptions and ResolvedLocale.
VERIFY_NOT_REACHED();
}
static Vector<LocaleKey> available_keyword_values(StringView locale, StringView key)
{
auto key_locale_data = Unicode::available_keyword_values(locale, key);
Vector<LocaleKey> result;
result.ensure_capacity(key_locale_data.size());
for (auto& keyword : key_locale_data)
result.unchecked_append(move(keyword));
if (key == "hc"sv) {
// https://tc39.es/ecma402/#sec-intl.datetimeformat-internal-slots
// [[LocaleData]].[[<locale>]].[[hc]] must be « null, "h11", "h12", "h23", "h24" ».
result.prepend(Empty {});
}
return result;
}
// 9.2.7 ResolveLocale ( availableLocales, requestedLocales, options, relevantExtensionKeys, localeData ), https://tc39.es/ecma402/#sec-resolvelocale
ResolvedLocale resolve_locale(ReadonlySpan<String> requested_locales, LocaleOptions const& options, ReadonlySpan<StringView> relevant_extension_keys)
{
static auto true_string = "true"_string;
// 1. Let matcher be options.[[localeMatcher]].
auto const& matcher = options.locale_matcher;
Optional<MatchedLocale> matcher_result;
// 2. If matcher is "lookup", then
if (matcher.is_string() && matcher.as_string().utf8_string_view() == "lookup"sv) {
// a. Let r be LookupMatchingLocaleByPrefix(availableLocales, requestedLocales).
matcher_result = lookup_matching_locale_by_prefix(requested_locales);
}
// 3. Else,
else {
// a. Let r be LookupMatchingLocaleByBestFit(availableLocales, requestedLocales).
matcher_result = lookup_matching_locale_by_best_fit(requested_locales);
}
// 4. If r is undefined, set r to the Record { [[locale]]: DefaultLocale(), [[extension]]: empty }.
if (!matcher_result.has_value())
matcher_result = MatchedLocale { MUST(String::from_utf8(Unicode::default_locale())), {} };
// 5. Let foundLocale be r.[[locale]].
auto found_locale = move(matcher_result->locale);
// 6. Let foundLocaleData be localeData.[[<foundLocale>]].
// 7. Assert: Type(foundLocaleData) is Record.
// 8. Let result be a new Record.
// 9. Set result.[[LocaleData]] to foundLocaleData.
ResolvedLocale result {};
Vector<Unicode::Keyword> keywords;
// 10. If r.[[extension]] is not empty, then
if (matcher_result->extension.has_value()) {
// a. Let components be UnicodeExtensionComponents(r.[[extension]]).
auto& components = matcher_result->extension->get<Unicode::LocaleExtension>();
// b. Let keywords be components.[[Keywords]].
keywords = move(components.keywords);
}
// 11. Else,
// a. Let keywords be a new empty List.
// 12. Let supportedKeywords be a new empty List.
Vector<Unicode::Keyword> supported_keywords;
// 13. For each element key of relevantExtensionKeys, do
for (auto const& key : relevant_extension_keys) {
// a. Let keyLocaleData be foundLocaleData.[[<key>]].
// b. Assert: keyLocaleData is a List.
auto key_locale_data = available_keyword_values(found_locale, key);
// c. Let value be keyLocaleData[0].
// d. Assert: value is a String or value is null.
auto value = key_locale_data[0];
// e. Let supportedKeyword be empty.
Optional<Unicode::Keyword> supported_keyword;
// f. If keywords contains an element whose [[Key]] is key, then
if (auto entry = keywords.find_if([&](auto const& entry) { return entry.key == key; }); entry != keywords.end()) {
// i. Let entry be the element of keywords whose [[Key]] is key.
// ii. Let requestedValue be entry.[[Value]].
auto requested_value = entry->value;
// iii. If requestedValue is not the empty String, then
if (!requested_value.is_empty()) {
// 1. If keyLocaleData contains requestedValue, then
if (key_locale_data.contains_slow(requested_value)) {
// a. Set value to requestedValue.
value = move(requested_value);
// b. Set supportedKeyword to the Record { [[Key]]: key, [[Value]]: value }.
supported_keyword = Unicode::Keyword { MUST(String::from_utf8(key)), move(entry->value) };
}
}
// iv. Else if keyLocaleData contains "true", then
else if (key_locale_data.contains_slow(true_string)) {
// 1. Set value to "true".
value = true_string;
// 2. Set supportedKeyword to the Record { [[Key]]: key, [[Value]]: "" }.
supported_keyword = Unicode::Keyword { MUST(String::from_utf8(key)), {} };
}
}
// g. Assert: options has a field [[<key>]].
// h. Let optionsValue be options.[[<key>]].
// i. Assert: optionsValue is a String, or optionsValue is either undefined or null.
auto options_value = find_key_in_value(options, key);
// j. If optionsValue is a String, then
if (auto* options_string = options_value.has_value() ? options_value->get_pointer<String>() : nullptr) {
// i. Let ukey be the ASCII-lowercase of key.
// NOTE: `key` is always lowercase, and this step is likely to be removed:
// https://github.com/tc39/ecma402/pull/846#discussion_r1428263375
// ii. Set optionsValue to CanonicalizeUValue(ukey, optionsValue).
*options_string = Unicode::canonicalize_unicode_extension_values(key, *options_string);
// iii. If optionsValue is the empty String, then
if (options_string->is_empty()) {
// 1. Set optionsValue to "true".
*options_string = true_string;
}
}
// k. If SameValue(optionsValue, value) is false and keyLocaleData contains optionsValue, then
if (options_value.has_value() && (options_value != value) && key_locale_data.contains_slow(*options_value)) {
// i. Set value to optionsValue.
value = options_value.release_value();
// ii. Set supportedKeyword to empty.
supported_keyword.clear();
}
// l. If supportedKeyword is not empty, append supportedKeyword to supportedKeywords.
if (supported_keyword.has_value())
supported_keywords.append(supported_keyword.release_value());
// m. Set result.[[<key>]] to value.
find_key_in_value(result, key) = move(value);
}
// 14. If supportedKeywords is not empty, then
if (!supported_keywords.is_empty()) {
auto locale_id = Unicode::parse_unicode_locale_id(found_locale);
VERIFY(locale_id.has_value());
// a. Let supportedAttributes be a new empty List.
// b. Set foundLocale to InsertUnicodeExtensionAndCanonicalize(foundLocale, supportedAttributes, supportedKeywords).
found_locale = insert_unicode_extension_and_canonicalize(locale_id.release_value(), {}, move(supported_keywords));
}
// 15. Set result.[[locale]] to foundLocale.
result.locale = move(found_locale);
// 16. Return result.
return result;
}
// 9.2.8 FilterLocales ( availableLocales, requestedLocales, options ), https://tc39.es/ecma402/#sec-lookupsupportedlocales
ThrowCompletionOr<Array*> filter_locales(VM& vm, ReadonlySpan<String> requested_locales, Value options)
{
auto& realm = *vm.current_realm();
// 1. Set options to ? CoerceOptionsToObject(options).
auto* options_object = TRY(coerce_options_to_object(vm, options));
// 2. Let matcher be ? GetOption(options, "localeMatcher", string, « "lookup", "best fit" », "best fit").
auto matcher = TRY(get_option(vm, *options_object, vm.names.localeMatcher, OptionType::String, { "lookup"sv, "best fit"sv }, "best fit"sv));
// 3. Let subset be a new empty List.
Vector<String> subset;
// 4. For each element locale of requestedLocales, do
for (auto const& locale : requested_locales) {
auto locale_id = Unicode::parse_unicode_locale_id(locale);
VERIFY(locale_id.has_value());
// a. Let noExtensionsLocale be the String value that is locale with any Unicode locale extension sequences removed.
locale_id->remove_extension_type<Unicode::LocaleExtension>();
auto no_extensions_locale = locale_id->to_string();
Optional<MatchedLocale> match;
// b. If matcher is "lookup", then
if (matcher.as_string().utf8_string_view() == "lookup"sv) {
// i. Let match be LookupMatchingLocaleByPrefix(availableLocales, noExtensionsLocale).
match = lookup_matching_locale_by_prefix({ { no_extensions_locale } });
}
// c. Else,
else {
// i. Let match be LookupMatchingLocaleByBestFit(availableLocales, noExtensionsLocale).
match = lookup_matching_locale_by_best_fit({ { no_extensions_locale } });
}
// d. If match is not undefined, append locale to subset.
if (match.has_value())
subset.append(locale);
}
// 5. Return CreateArrayFromList(subset).
return Array::create_from<String>(realm, subset, [&vm](auto& locale) { return PrimitiveString::create(vm, move(locale)); }).ptr();
}
// 9.2.10 CoerceOptionsToObject ( options ), https://tc39.es/ecma402/#sec-coerceoptionstoobject
ThrowCompletionOr<Object*> coerce_options_to_object(VM& vm, Value options)
{
auto& realm = *vm.current_realm();
// 1. If options is undefined, then
if (options.is_undefined()) {
// a. Return OrdinaryObjectCreate(null).
return Object::create(realm, nullptr).ptr();
}
// 2. Return ? ToObject(options).
return TRY(options.to_object(vm)).ptr();
}
// NOTE: 9.2.11 GetOption has been removed and is being pulled in from ECMA-262 in the Temporal proposal.
// 9.2.12 GetBooleanOrStringNumberFormatOption ( options, property, stringValues, fallback ), https://tc39.es/ecma402/#sec-getbooleanorstringnumberformatoption
ThrowCompletionOr<StringOrBoolean> get_boolean_or_string_number_format_option(VM& vm, Object const& options, PropertyKey const& property, ReadonlySpan<StringView> string_values, StringOrBoolean fallback)
{
// 1. Let value be ? Get(options, property).
auto value = TRY(options.get(property));
// 2. If value is undefined, return fallback.
if (value.is_undefined())
return fallback;
// 3. If value is true, return true.
if (value.is_boolean() && value.as_bool())
return StringOrBoolean { true };
// 4. If ToBoolean(value) is false, return false.
if (!value.to_boolean())
return StringOrBoolean { false };
// 5. Let value be ? ToString(value).
auto value_string = TRY(value.to_string(vm));
// 6. If stringValues does not contain value, throw a RangeError exception.
auto it = find(string_values.begin(), string_values.end(), value_string.bytes_as_string_view());
if (it == string_values.end())
return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, value_string, property.as_string());
// 7. Return value.
return StringOrBoolean { *it };
}
// 9.2.13 DefaultNumberOption ( value, minimum, maximum, fallback ), https://tc39.es/ecma402/#sec-defaultnumberoption
ThrowCompletionOr<Optional<int>> default_number_option(VM& vm, Value value, int minimum, int maximum, Optional<int> fallback)
{
// 1. If value is undefined, return fallback.
if (value.is_undefined())
return fallback;
// 2. Set value to ? ToNumber(value).
value = TRY(value.to_number(vm));
// 3. If value is NaN or less than minimum or greater than maximum, throw a RangeError exception.
if (value.is_nan() || (value.as_double() < minimum) || (value.as_double() > maximum))
return vm.throw_completion<RangeError>(ErrorType::IntlNumberIsNaNOrOutOfRange, value, minimum, maximum);
// 4. Return floor(value).
return floor(value.as_double());
}
// 9.2.14 GetNumberOption ( options, property, minimum, maximum, fallback ), https://tc39.es/ecma402/#sec-getnumberoption
ThrowCompletionOr<Optional<int>> get_number_option(VM& vm, Object const& options, PropertyKey const& property, int minimum, int maximum, Optional<int> fallback)
{
// 1. Assert: Type(options) is Object.
// 2. Let value be ? Get(options, property).
auto value = TRY(options.get(property));
// 3. Return ? DefaultNumberOption(value, minimum, maximum, fallback).
return default_number_option(vm, value, minimum, maximum, move(fallback));
}
}