Coverage Report

Created: 2026-09-26 06:49

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2021-2023, Tim Flynn <trflynn89@serenityos.org>
3
 *
4
 * SPDX-License-Identifier: BSD-2-Clause
5
 */
6
7
#include <AK/AllOf.h>
8
#include <AK/AnyOf.h>
9
#include <AK/CharacterTypes.h>
10
#include <AK/Find.h>
11
#include <AK/Function.h>
12
#include <AK/QuickSort.h>
13
#include <AK/TypeCasts.h>
14
#include <LibJS/Runtime/Array.h>
15
#include <LibJS/Runtime/GlobalObject.h>
16
#include <LibJS/Runtime/Intl/AbstractOperations.h>
17
#include <LibJS/Runtime/Intl/Locale.h>
18
#include <LibJS/Runtime/ValueInlines.h>
19
#include <LibLocale/Locale.h>
20
21
namespace JS::Intl {
22
23
// 6.2.1 IsStructurallyValidLanguageTag ( locale ), https://tc39.es/ecma402/#sec-isstructurallyvalidlanguagetag
24
Optional<::Locale::LocaleID> is_structurally_valid_language_tag(StringView locale)
25
0
{
26
0
    auto contains_duplicate_variant = [&](auto& variants) {
27
0
        if (variants.is_empty())
28
0
            return false;
29
30
0
        quick_sort(variants);
31
32
0
        for (size_t i = 0; i < variants.size() - 1; ++i) {
33
0
            if (variants[i].equals_ignoring_case(variants[i + 1]))
34
0
                return true;
35
0
        }
36
37
0
        return false;
38
0
    };
39
40
    // IsStructurallyValidLanguageTag returns true if all of the following conditions hold, false otherwise:
41
42
    // locale can be generated from the EBNF grammar for unicode_locale_id in Unicode Technical Standard #35 LDML § 3.2 Unicode Locale Identifier;
43
0
    auto locale_id = ::Locale::parse_unicode_locale_id(locale);
44
0
    if (!locale_id.has_value())
45
0
        return {};
46
47
    // locale does not use any of the backwards compatibility syntax described in Unicode Technical Standard #35 LDML § 3.3 BCP 47 Conformance;
48
    // https://unicode.org/reports/tr35/#BCP_47_Conformance
49
0
    if (locale.contains('_') || locale_id->language_id.is_root || !locale_id->language_id.language.has_value())
50
0
        return {};
51
52
    // the unicode_language_id within locale contains no duplicate unicode_variant_subtag subtags; and
53
0
    if (contains_duplicate_variant(locale_id->language_id.variants))
54
0
        return {};
55
56
    // if locale contains an extensions* component, that component
57
0
    Vector<char> unique_keys;
58
0
    for (auto& extension : locale_id->extensions) {
59
        // does not contain any other_extensions components with duplicate [alphanum-[tTuUxX]] subtags,
60
        // contains at most one unicode_locale_extensions component,
61
        // contains at most one transformed_extensions component, and
62
0
        char key = extension.visit(
63
0
            [](::Locale::LocaleExtension const&) { return 'u'; },
64
0
            [](::Locale::TransformedExtension const&) { return 't'; },
65
0
            [](::Locale::OtherExtension const& ext) { return static_cast<char>(to_ascii_lowercase(ext.key)); });
66
67
0
        if (unique_keys.contains_slow(key))
68
0
            return {};
69
70
0
        unique_keys.append(key);
71
72
        // if a transformed_extensions component that contains a tlang component is present, then
73
        // the tlang component contains no duplicate unicode_variant_subtag subtags.
74
0
        if (auto* transformed = extension.get_pointer<::Locale::TransformedExtension>()) {
75
0
            auto& language = transformed->language;
76
0
            if (language.has_value() && contains_duplicate_variant(language->variants))
77
0
                return {};
78
0
        }
79
0
    }
80
81
0
    return locale_id;
82
0
}
83
84
// 6.2.2 CanonicalizeUnicodeLocaleId ( locale ), https://tc39.es/ecma402/#sec-canonicalizeunicodelocaleid
85
String canonicalize_unicode_locale_id(::Locale::LocaleID& locale)
86
0
{
87
    // Note: This implementation differs from the spec in how Step 3 is implemented. The spec assumes
88
    // the input to this method is a string, and is written such that operations are performed on parts
89
    // of that string. LibUnicode gives us the parsed locale in a structure, so we can mutate that
90
    // structure directly. From a footnote in the spec:
91
    //
92
    // The third step of this algorithm ensures that a Unicode locale extension sequence in the
93
    // returned language tag contains:
94
    //     * only the first instance of any attribute duplicated in the input, and
95
    //     * only the first keyword for a given key in the input.
96
0
    for (auto& extension : locale.extensions) {
97
0
        if (!extension.has<::Locale::LocaleExtension>())
98
0
            continue;
99
100
0
        auto& locale_extension = extension.get<::Locale::LocaleExtension>();
101
102
0
        auto attributes = move(locale_extension.attributes);
103
0
        for (auto& attribute : attributes) {
104
0
            if (!locale_extension.attributes.contains_slow(attribute))
105
0
                locale_extension.attributes.append(move(attribute));
106
0
        }
107
108
0
        auto keywords = move(locale_extension.keywords);
109
0
        for (auto& keyword : keywords) {
110
0
            if (!any_of(locale_extension.keywords, [&](auto const& k) { return k.key == keyword.key; }))
111
0
                locale_extension.keywords.append(move(keyword));
112
0
        }
113
114
0
        break;
115
0
    }
116
117
    // 1. Let localeId be the string locale after performing the algorithm to transform it to canonical syntax per Unicode Technical Standard #35 LDML § 3.2.1 Canonical Unicode Locale Identifiers.
118
    // 2. Let localeId be the string localeId after performing the algorithm to transform it to canonical form.
119
0
    auto locale_id = ::Locale::canonicalize_unicode_locale_id(locale);
120
0
    VERIFY(locale_id.has_value());
121
122
    // 4. Return localeId.
123
0
    return locale_id.release_value();
124
0
}
125
126
// 6.3.1 IsWellFormedCurrencyCode ( currency ), https://tc39.es/ecma402/#sec-iswellformedcurrencycode
127
bool is_well_formed_currency_code(StringView currency)
128
0
{
129
    // 1. If the length of currency is not 3, return false.
130
0
    if (currency.length() != 3)
131
0
        return false;
132
133
    // 2. Let normalized be the ASCII-uppercase of currency.
134
    // 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.
135
0
    if (!all_of(currency, is_ascii_alpha))
136
0
        return false;
137
138
    // 4. Return true.
139
0
    return true;
140
0
}
141
142
// 6.6.1 IsWellFormedUnitIdentifier ( unitIdentifier ), https://tc39.es/ecma402/#sec-iswellformedunitidentifier
143
bool is_well_formed_unit_identifier(StringView unit_identifier)
144
0
{
145
    // 6.6.2 IsSanctionedSingleUnitIdentifier ( unitIdentifier ), https://tc39.es/ecma402/#sec-issanctionedsingleunitidentifier
146
0
    constexpr auto is_sanctioned_single_unit_identifier = [](StringView unit_identifier) {
147
        // 1. If unitIdentifier is listed in Table 2 below, return true.
148
        // 2. Else, return false.
149
0
        static constexpr auto sanctioned_units = sanctioned_single_unit_identifiers();
150
0
        return find(sanctioned_units.begin(), sanctioned_units.end(), unit_identifier) != sanctioned_units.end();
151
0
    };
152
153
    // 1. If ! IsSanctionedSingleUnitIdentifier(unitIdentifier) is true, then
154
0
    if (is_sanctioned_single_unit_identifier(unit_identifier)) {
155
        // a. Return true.
156
0
        return true;
157
0
    }
158
159
    // 2. Let i be StringIndexOf(unitIdentifier, "-per-", 0).
160
0
    auto indices = unit_identifier.find_all("-per-"sv);
161
162
    // 3. If i is -1 or StringIndexOf(unitIdentifier, "-per-", i + 1) is not -1, then
163
0
    if (indices.size() != 1) {
164
        // a. Return false.
165
0
        return false;
166
0
    }
167
168
    // 4. Assert: The five-character substring "-per-" occurs exactly once in unitIdentifier, at index i.
169
    // NOTE: We skip this because the indices vector being of size 1 already verifies this invariant.
170
171
    // 5. Let numerator be the substring of unitIdentifier from 0 to i.
172
0
    auto numerator = unit_identifier.substring_view(0, indices[0]);
173
174
    // 6. Let denominator be the substring of unitIdentifier from i + 5.
175
0
    auto denominator = unit_identifier.substring_view(indices[0] + 5);
176
177
    // 7. If ! IsSanctionedSingleUnitIdentifier(numerator) and ! IsSanctionedSingleUnitIdentifier(denominator) are both true, then
178
0
    if (is_sanctioned_single_unit_identifier(numerator) && is_sanctioned_single_unit_identifier(denominator)) {
179
        // a. Return true.
180
0
        return true;
181
0
    }
182
183
    // 8. Return false.
184
0
    return false;
185
0
}
186
187
// 9.2.1 CanonicalizeLocaleList ( locales ), https://tc39.es/ecma402/#sec-canonicalizelocalelist
188
ThrowCompletionOr<Vector<String>> canonicalize_locale_list(VM& vm, Value locales)
189
0
{
190
0
    auto& realm = *vm.current_realm();
191
192
    // 1. If locales is undefined, then
193
0
    if (locales.is_undefined()) {
194
        // a. Return a new empty List.
195
0
        return Vector<String> {};
196
0
    }
197
198
    // 2. Let seen be a new empty List.
199
0
    Vector<String> seen;
200
201
0
    Object* object = nullptr;
202
    // 3. If Type(locales) is String or Type(locales) is Object and locales has an [[InitializedLocale]] internal slot, then
203
0
    if (locales.is_string() || (locales.is_object() && is<Locale>(locales.as_object()))) {
204
        // a. Let O be CreateArrayFromList(« locales »).
205
0
        object = Array::create_from(realm, { locales });
206
0
    }
207
    // 4. Else,
208
0
    else {
209
        // a. Let O be ? ToObject(locales).
210
0
        object = TRY(locales.to_object(vm));
211
0
    }
212
213
    // 5. Let len be ? ToLength(? Get(O, "length")).
214
0
    auto length_value = TRY(object->get(vm.names.length));
215
0
    auto length = TRY(length_value.to_length(vm));
216
217
    // 6. Let k be 0.
218
    // 7. Repeat, while k < len,
219
0
    for (size_t k = 0; k < length; ++k) {
220
        // a. Let Pk be ToString(k).
221
0
        auto property_key = PropertyKey { k };
222
223
        // b. Let kPresent be ? HasProperty(O, Pk).
224
0
        auto key_present = TRY(object->has_property(property_key));
225
226
        // c. If kPresent is true, then
227
0
        if (key_present) {
228
            // i. Let kValue be ? Get(O, Pk).
229
0
            auto key_value = TRY(object->get(property_key));
230
231
            // ii. If Type(kValue) is not String or Object, throw a TypeError exception.
232
0
            if (!key_value.is_string() && !key_value.is_object())
233
0
                return vm.throw_completion<TypeError>(ErrorType::NotAnObjectOrString, key_value);
234
235
0
            String tag;
236
237
            // iii. If Type(kValue) is Object and kValue has an [[InitializedLocale]] internal slot, then
238
0
            if (key_value.is_object() && is<Locale>(key_value.as_object())) {
239
                // 1. Let tag be kValue.[[Locale]].
240
0
                tag = static_cast<Locale const&>(key_value.as_object()).locale();
241
0
            }
242
            // iv. Else,
243
0
            else {
244
                // 1. Let tag be ? ToString(kValue).
245
0
                tag = TRY(key_value.to_string(vm));
246
0
            }
247
248
            // v. If ! IsStructurallyValidLanguageTag(tag) is false, throw a RangeError exception.
249
0
            auto locale_id = is_structurally_valid_language_tag(tag);
250
0
            if (!locale_id.has_value())
251
0
                return vm.throw_completion<RangeError>(ErrorType::IntlInvalidLanguageTag, tag);
252
253
            // vi. Let canonicalizedTag be ! CanonicalizeUnicodeLocaleId(tag).
254
0
            auto canonicalized_tag = JS::Intl::canonicalize_unicode_locale_id(*locale_id);
255
256
            // vii. If canonicalizedTag is not an element of seen, append canonicalizedTag as the last element of seen.
257
0
            if (!seen.contains_slow(canonicalized_tag))
258
0
                seen.append(move(canonicalized_tag));
259
0
        }
260
261
        // d. Increase k by 1.
262
0
    }
263
264
0
    return seen;
265
0
}
266
267
// 9.2.2 BestAvailableLocale ( availableLocales, locale ), https://tc39.es/ecma402/#sec-bestavailablelocale
268
Optional<StringView> best_available_locale(StringView locale)
269
0
{
270
    // 1. Let candidate be locale.
271
0
    StringView candidate = locale;
272
273
    // 2. Repeat,
274
0
    while (true) {
275
        // a. If availableLocales contains candidate, return candidate.
276
0
        if (::Locale::is_locale_available(candidate))
277
0
            return candidate;
278
279
        // b. Let pos be the character index of the last occurrence of "-" (U+002D) within candidate. If that character does not occur, return undefined.
280
0
        auto pos = candidate.find_last('-');
281
0
        if (!pos.has_value())
282
0
            return {};
283
284
        // c. If pos ≥ 2 and the character "-" occurs at index pos-2 of candidate, decrease pos by 2.
285
0
        if ((*pos >= 2) && (candidate[*pos - 2] == '-'))
286
0
            pos = *pos - 2;
287
288
        // d. Let candidate be the substring of candidate from position 0, inclusive, to position pos, exclusive.
289
0
        candidate = candidate.substring_view(0, *pos);
290
0
    }
291
0
}
292
293
struct MatcherResult {
294
    String locale;
295
    Vector<::Locale::Extension> extensions {};
296
};
297
298
// 9.2.3 LookupMatcher ( availableLocales, requestedLocales ), https://tc39.es/ecma402/#sec-lookupmatcher
299
static MatcherResult lookup_matcher(Vector<String> const& requested_locales)
300
0
{
301
    // 1. Let result be a new Record.
302
0
    MatcherResult result {};
303
304
    // 2. For each element locale of requestedLocales, do
305
0
    for (auto const& locale : requested_locales) {
306
0
        auto locale_id = ::Locale::parse_unicode_locale_id(locale);
307
0
        VERIFY(locale_id.has_value());
308
309
        // a. Let noExtensionsLocale be the String value that is locale with any Unicode locale extension sequences removed.
310
0
        auto extensions = locale_id->remove_extension_type<::Locale::LocaleExtension>();
311
0
        auto no_extensions_locale = locale_id->to_string();
312
313
        // b. Let availableLocale be ! BestAvailableLocale(availableLocales, noExtensionsLocale).
314
0
        auto available_locale = best_available_locale(no_extensions_locale);
315
316
        // c. If availableLocale is not undefined, then
317
0
        if (available_locale.has_value()) {
318
            // i. Set result.[[locale]] to availableLocale.
319
0
            result.locale = MUST(String::from_utf8(*available_locale));
320
321
            // ii. If locale and noExtensionsLocale are not the same String value, then
322
0
            if (locale != no_extensions_locale) {
323
                // 1. Let extension be the String value consisting of the substring of the Unicode locale extension sequence within locale.
324
                // 2. Set result.[[extension]] to extension.
325
0
                result.extensions.extend(move(extensions));
326
0
            }
327
328
            // iii. Return result.
329
0
            return result;
330
0
        }
331
0
    }
332
333
    // 3. Let defLocale be ! DefaultLocale().
334
    // 4. Set result.[[locale]] to defLocale.
335
0
    result.locale = MUST(String::from_utf8(::Locale::default_locale()));
336
337
    // 5. Return result.
338
0
    return result;
339
0
}
340
341
// 9.2.4 BestFitMatcher ( availableLocales, requestedLocales ), https://tc39.es/ecma402/#sec-bestfitmatcher
342
static MatcherResult best_fit_matcher(Vector<String> const& requested_locales)
343
0
{
344
    // The algorithm is implementation dependent, but should produce results that a typical user of the requested locales would
345
    // perceive as at least as good as those produced by the LookupMatcher abstract operation.
346
0
    return lookup_matcher(requested_locales);
347
0
}
348
349
// 9.2.6 InsertUnicodeExtensionAndCanonicalize ( locale, extension ), https://tc39.es/ecma402/#sec-insert-unicode-extension-and-canonicalize
350
String insert_unicode_extension_and_canonicalize(::Locale::LocaleID locale, ::Locale::LocaleExtension extension)
351
0
{
352
    // Note: This implementation differs from the spec in how the extension is inserted. The spec assumes
353
    // the input to this method is a string, and is written such that operations are performed on parts
354
    // of that string. LibUnicode gives us the parsed locale in a structure, so we can mutate that
355
    // structure directly.
356
0
    locale.extensions.append(move(extension));
357
358
0
    return JS::Intl::canonicalize_unicode_locale_id(locale);
359
0
}
360
361
template<typename T>
362
static auto& find_key_in_value(T& value, StringView key)
363
0
{
364
0
    if (key == "ca"sv)
365
0
        return value.ca;
366
0
    if (key == "co"sv)
367
0
        return value.co;
368
0
    if (key == "hc"sv)
369
0
        return value.hc;
370
0
    if (key == "kf"sv)
371
0
        return value.kf;
372
0
    if (key == "kn"sv)
373
0
        return value.kn;
374
0
    if (key == "nu"sv)
375
0
        return value.nu;
376
377
    // If you hit this point, you must add any missing keys from [[RelevantExtensionKeys]] to LocaleOptions and LocaleResult.
378
0
    VERIFY_NOT_REACHED();
379
0
}
Unexecuted instantiation: AbstractOperations.cpp:auto& JS::Intl::find_key_in_value<JS::Intl::LocaleOptions const>(JS::Intl::LocaleOptions const&, AK::StringView)
Unexecuted instantiation: AbstractOperations.cpp:auto& JS::Intl::find_key_in_value<JS::Intl::LocaleResult>(JS::Intl::LocaleResult&, AK::StringView)
380
381
// 9.2.7 ResolveLocale ( availableLocales, requestedLocales, options, relevantExtensionKeys, localeData ), https://tc39.es/ecma402/#sec-resolvelocale
382
LocaleResult resolve_locale(Vector<String> const& requested_locales, LocaleOptions const& options, ReadonlySpan<StringView> relevant_extension_keys)
383
0
{
384
    // 1. Let matcher be options.[[localeMatcher]].
385
0
    auto const& matcher = options.locale_matcher;
386
0
    MatcherResult matcher_result;
387
388
    // 2. If matcher is "lookup", then
389
0
    if (matcher.is_string() && (matcher.as_string().utf8_string_view()) == "lookup"sv) {
390
        // a. Let r be ! LookupMatcher(availableLocales, requestedLocales).
391
0
        matcher_result = lookup_matcher(requested_locales);
392
0
    }
393
    // 3. Else,
394
0
    else {
395
        // a. Let r be ! BestFitMatcher(availableLocales, requestedLocales).
396
0
        matcher_result = best_fit_matcher(requested_locales);
397
0
    }
398
399
    // 4. Let foundLocale be r.[[locale]].
400
0
    auto found_locale = move(matcher_result.locale);
401
402
    // 5. Let result be a new Record.
403
0
    LocaleResult result {};
404
405
    // 6. Set result.[[dataLocale]] to foundLocale.
406
0
    result.data_locale = found_locale;
407
408
    // 7. If r has an [[extension]] field, then
409
0
    Vector<::Locale::Keyword> keywords;
410
0
    for (auto& extension : matcher_result.extensions) {
411
0
        if (!extension.has<::Locale::LocaleExtension>())
412
0
            continue;
413
414
        // a. Let components be ! UnicodeExtensionComponents(r.[[extension]]).
415
0
        auto& components = extension.get<::Locale::LocaleExtension>();
416
        // b. Let keywords be components.[[Keywords]].
417
0
        keywords = move(components.keywords);
418
419
0
        break;
420
0
    }
421
422
    // 8. Let supportedExtension be "-u".
423
0
    ::Locale::LocaleExtension supported_extension {};
424
425
    // 9. For each element key of relevantExtensionKeys, do
426
0
    for (auto const& key : relevant_extension_keys) {
427
        // a. Let foundLocaleData be localeData.[[<foundLocale>]].
428
        // b. Assert: Type(foundLocaleData) is Record.
429
        // c. Let keyLocaleData be foundLocaleData.[[<key>]].
430
        // d. Assert: Type(keyLocaleData) is List.
431
0
        auto key_locale_data = ::Locale::get_available_keyword_values(key);
432
433
        // e. Let value be keyLocaleData[0].
434
        // f. Assert: Type(value) is either String or Null.
435
        // NOTE: ECMA-402 assumes keyLocaleData is sorted by locale preference. Our list is sorted
436
        //       alphabetically, so we get the locale's preferred value from LibUnicode.
437
0
        Optional<String> value;
438
0
        if (auto preference = ::Locale::get_preferred_keyword_value_for_locale(found_locale, key); preference.has_value())
439
0
            value = MUST(String::from_utf8(*preference));
440
441
        // g. Let supportedExtensionAddition be "".
442
0
        Optional<::Locale::Keyword> supported_extension_addition {};
443
444
        // h. If r has an [[extension]] field, then
445
0
        for (auto& entry : keywords) {
446
            // i. If keywords contains an element whose [[Key]] is the same as key, then
447
0
            if (entry.key != key)
448
0
                continue;
449
450
            // 1. Let entry be the element of keywords whose [[Key]] is the same as key.
451
            // 2. Let requestedValue be entry.[[Value]].
452
0
            auto requested_value = entry.value;
453
454
            // 3. If requestedValue is not the empty String, then
455
0
            if (!requested_value.is_empty()) {
456
                // a. If keyLocaleData contains requestedValue, then
457
0
                if (key_locale_data.contains_slow(requested_value)) {
458
                    // i. Let value be requestedValue.
459
0
                    value = move(requested_value);
460
461
                    // ii. Let supportedExtensionAddition be the string-concatenation of "-", key, "-", and value.
462
0
                    supported_extension_addition = ::Locale::Keyword { MUST(String::from_utf8(key)), move(entry.value) };
463
0
                }
464
0
            }
465
            // 4. Else if keyLocaleData contains "true", then
466
0
            else if (key_locale_data.contains_slow("true"sv)) {
467
                // a. Let value be "true".
468
0
                value = "true"_string;
469
470
                // b. Let supportedExtensionAddition be the string-concatenation of "-" and key.
471
0
                supported_extension_addition = ::Locale::Keyword { MUST(String::from_utf8(key)), {} };
472
0
            }
473
474
0
            break;
475
0
        }
476
477
        // i. If options has a field [[<key>]], then
478
        // i. Let optionsValue be options.[[<key>]].
479
        // ii. Assert: Type(optionsValue) is either String, Undefined, or Null.
480
0
        auto options_value = find_key_in_value(options, key);
481
482
        // iii. If Type(optionsValue) is String, then
483
0
        if (options_value.has_value()) {
484
            // 1. Let optionsValue be the string optionsValue after performing the algorithm steps to transform Unicode extension values to canonical syntax per Unicode Technical Standard #35 LDML § 3.2.1 Canonical Unicode Locale Identifiers, treating key as ukey and optionsValue as uvalue productions.
485
            // 2. Let optionsValue be the string optionsValue after performing the algorithm steps to replace Unicode extension values with their canonical form per Unicode Technical Standard #35 LDML § 3.2.1 Canonical Unicode Locale Identifiers, treating key as ukey and optionsValue as uvalue productions.
486
0
            ::Locale::canonicalize_unicode_extension_values(key, *options_value, true);
487
488
            // 3. If optionsValue is the empty String, then
489
0
            if (options_value->is_empty()) {
490
                // a. Let optionsValue be "true".
491
0
                options_value = "true"_string;
492
0
            }
493
0
        }
494
495
        // iv. If SameValue(optionsValue, value) is false and keyLocaleData contains optionsValue, then
496
0
        if (options_value.has_value() && (options_value != value) && key_locale_data.contains_slow(*options_value)) {
497
            // 1. Let value be optionsValue.
498
0
            value = move(options_value);
499
500
            // 2. Let supportedExtensionAddition be "".
501
0
            supported_extension_addition.clear();
502
0
        }
503
504
        // j. Set result.[[<key>]] to value.
505
0
        find_key_in_value(result, key) = move(value);
506
507
        // k. Set supportedExtension to the string-concatenation of supportedExtension and supportedExtensionAddition.
508
0
        if (supported_extension_addition.has_value())
509
0
            supported_extension.keywords.append(supported_extension_addition.release_value());
510
0
    }
511
512
    // 10. If supportedExtension is not "-u", then
513
0
    if (!supported_extension.keywords.is_empty()) {
514
0
        auto locale_id = ::Locale::parse_unicode_locale_id(found_locale);
515
0
        VERIFY(locale_id.has_value());
516
517
        // a. Set foundLocale to InsertUnicodeExtensionAndCanonicalize(foundLocale, supportedExtension).
518
0
        found_locale = insert_unicode_extension_and_canonicalize(locale_id.release_value(), move(supported_extension));
519
0
    }
520
521
    // 11. Set result.[[locale]] to foundLocale.
522
0
    result.locale = move(found_locale);
523
524
    // 12. Return result.
525
0
    return result;
526
0
}
527
528
// 9.2.8 LookupSupportedLocales ( availableLocales, requestedLocales ), https://tc39.es/ecma402/#sec-lookupsupportedlocales
529
static Vector<String> lookup_supported_locales(Vector<String> const& requested_locales)
530
0
{
531
    // 1. Let subset be a new empty List.
532
0
    Vector<String> subset;
533
534
    // 2. For each element locale of requestedLocales, do
535
0
    for (auto const& locale : requested_locales) {
536
0
        auto locale_id = ::Locale::parse_unicode_locale_id(locale);
537
0
        VERIFY(locale_id.has_value());
538
539
        // a. Let noExtensionsLocale be the String value that is locale with any Unicode locale extension sequences removed.
540
0
        locale_id->remove_extension_type<::Locale::LocaleExtension>();
541
0
        auto no_extensions_locale = locale_id->to_string();
542
543
        // b. Let availableLocale be ! BestAvailableLocale(availableLocales, noExtensionsLocale).
544
0
        auto available_locale = best_available_locale(no_extensions_locale);
545
546
        // c. If availableLocale is not undefined, append locale to the end of subset.
547
0
        if (available_locale.has_value())
548
0
            subset.append(locale);
549
0
    }
550
551
    // 3. Return subset.
552
0
    return subset;
553
0
}
554
555
// 9.2.9 BestFitSupportedLocales ( availableLocales, requestedLocales ), https://tc39.es/ecma402/#sec-bestfitsupportedlocales
556
static Vector<String> best_fit_supported_locales(Vector<String> const& requested_locales)
557
0
{
558
    // The BestFitSupportedLocales abstract operation returns the subset of the provided BCP 47
559
    // language priority list requestedLocales for which availableLocales has a matching locale
560
    // when using the Best Fit Matcher algorithm. Locales appear in the same order in the returned
561
    // list as in requestedLocales. The steps taken are implementation dependent.
562
563
    // :yakbrain:
564
0
    return lookup_supported_locales(requested_locales);
565
0
}
566
567
// 9.2.10 SupportedLocales ( availableLocales, requestedLocales, options ), https://tc39.es/ecma402/#sec-supportedlocales
568
ThrowCompletionOr<Array*> supported_locales(VM& vm, Vector<String> const& requested_locales, Value options)
569
0
{
570
0
    auto& realm = *vm.current_realm();
571
572
    // 1. Set options to ? CoerceOptionsToObject(options).
573
0
    auto* options_object = TRY(coerce_options_to_object(vm, options));
574
575
    // 2. Let matcher be ? GetOption(options, "localeMatcher", string, « "lookup", "best fit" », "best fit").
576
0
    auto matcher = TRY(get_option(vm, *options_object, vm.names.localeMatcher, OptionType::String, { "lookup"sv, "best fit"sv }, "best fit"sv));
577
578
0
    Vector<String> supported_locales;
579
580
    // 3. If matcher is "best fit", then
581
0
    if (matcher.as_string().utf8_string_view() == "best fit"sv) {
582
        // a. Let supportedLocales be BestFitSupportedLocales(availableLocales, requestedLocales).
583
0
        supported_locales = best_fit_supported_locales(requested_locales);
584
0
    }
585
    // 4. Else,
586
0
    else {
587
        // a. Let supportedLocales be LookupSupportedLocales(availableLocales, requestedLocales).
588
0
        supported_locales = lookup_supported_locales(requested_locales);
589
0
    }
590
591
    // 5. Return CreateArrayFromList(supportedLocales).
592
0
    return Array::create_from<String>(realm, supported_locales, [&vm](auto& locale) { return PrimitiveString::create(vm, move(locale)); }).ptr();
593
0
}
594
595
// 9.2.12 CoerceOptionsToObject ( options ), https://tc39.es/ecma402/#sec-coerceoptionstoobject
596
ThrowCompletionOr<Object*> coerce_options_to_object(VM& vm, Value options)
597
0
{
598
0
    auto& realm = *vm.current_realm();
599
600
    // 1. If options is undefined, then
601
0
    if (options.is_undefined()) {
602
        // a. Return OrdinaryObjectCreate(null).
603
0
        return Object::create(realm, nullptr).ptr();
604
0
    }
605
606
    // 2. Return ? ToObject(options).
607
0
    return TRY(options.to_object(vm)).ptr();
608
0
}
609
610
// NOTE: 9.2.13 GetOption has been removed and is being pulled in from ECMA-262 in the Temporal proposal.
611
612
// 9.2.14 GetBooleanOrStringNumberFormatOption ( options, property, stringValues, fallback ), https://tc39.es/ecma402/#sec-getbooleanorstringnumberformatoption
613
ThrowCompletionOr<StringOrBoolean> get_boolean_or_string_number_format_option(VM& vm, Object const& options, PropertyKey const& property, ReadonlySpan<StringView> string_values, StringOrBoolean fallback)
614
0
{
615
    // 1. Let value be ? Get(options, property).
616
0
    auto value = TRY(options.get(property));
617
618
    // 2. If value is undefined, return fallback.
619
0
    if (value.is_undefined())
620
0
        return fallback;
621
622
    // 3. If value is true, return true.
623
0
    if (value.is_boolean() && value.as_bool())
624
0
        return StringOrBoolean { true };
625
626
    // 4. If ToBoolean(value) is false, return false.
627
0
    if (!value.to_boolean())
628
0
        return StringOrBoolean { false };
629
630
    // 5. Let value be ? ToString(value).
631
0
    auto value_string = TRY(value.to_string(vm));
632
633
    // 6. If stringValues does not contain value, throw a RangeError exception.
634
0
    auto it = find(string_values.begin(), string_values.end(), value_string.bytes_as_string_view());
635
0
    if (it == string_values.end())
636
0
        return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, value_string, property.as_string());
637
638
    // 7. Return value.
639
0
    return StringOrBoolean { *it };
640
0
}
641
642
// 9.2.15 DefaultNumberOption ( value, minimum, maximum, fallback ), https://tc39.es/ecma402/#sec-defaultnumberoption
643
ThrowCompletionOr<Optional<int>> default_number_option(VM& vm, Value value, int minimum, int maximum, Optional<int> fallback)
644
0
{
645
    // 1. If value is undefined, return fallback.
646
0
    if (value.is_undefined())
647
0
        return fallback;
648
649
    // 2. Set value to ? ToNumber(value).
650
0
    value = TRY(value.to_number(vm));
651
652
    // 3. If value is NaN or less than minimum or greater than maximum, throw a RangeError exception.
653
0
    if (value.is_nan() || (value.as_double() < minimum) || (value.as_double() > maximum))
654
0
        return vm.throw_completion<RangeError>(ErrorType::IntlNumberIsNaNOrOutOfRange, value, minimum, maximum);
655
656
    // 4. Return floor(value).
657
0
    return floor(value.as_double());
658
0
}
659
660
// 9.2.16 GetNumberOption ( options, property, minimum, maximum, fallback ), https://tc39.es/ecma402/#sec-getnumberoption
661
ThrowCompletionOr<Optional<int>> get_number_option(VM& vm, Object const& options, PropertyKey const& property, int minimum, int maximum, Optional<int> fallback)
662
0
{
663
    // 1. Assert: Type(options) is Object.
664
665
    // 2. Let value be ? Get(options, property).
666
0
    auto value = TRY(options.get(property));
667
668
    // 3. Return ? DefaultNumberOption(value, minimum, maximum, fallback).
669
0
    return default_number_option(vm, value, minimum, maximum, move(fallback));
670
0
}
671
672
// 9.2.17 PartitionPattern ( pattern ), https://tc39.es/ecma402/#sec-partitionpattern
673
Vector<PatternPartition> partition_pattern(StringView pattern)
674
0
{
675
    // 1. Let result be a new empty List.
676
0
    Vector<PatternPartition> result;
677
678
    // 2. Let beginIndex be StringIndexOf(pattern, "{", 0).
679
0
    auto begin_index = pattern.find('{', 0);
680
681
    // 3. Let endIndex be 0.
682
0
    size_t end_index = 0;
683
684
    // 4. Let nextIndex be 0.
685
0
    size_t next_index = 0;
686
687
    // 5. Let length be the number of code units in pattern.
688
    // 6. Repeat, while beginIndex is an integer index into pattern,
689
0
    while (begin_index.has_value()) {
690
        // a. Set endIndex to StringIndexOf(pattern, "}", beginIndex).
691
0
        end_index = pattern.find('}', *begin_index).value();
692
693
        // b. Assert: endIndex is greater than beginIndex.
694
0
        VERIFY(end_index > *begin_index);
695
696
        // c. If beginIndex is greater than nextIndex, then
697
0
        if (*begin_index > next_index) {
698
            // i. Let literal be a substring of pattern from position nextIndex, inclusive, to position beginIndex, exclusive.
699
0
            auto literal = pattern.substring_view(next_index, *begin_index - next_index);
700
701
            // ii. Append a new Record { [[Type]]: "literal", [[Value]]: literal } as the last element of the list result.
702
0
            result.append({ "literal"sv, MUST(String::from_utf8(literal)) });
703
0
        }
704
705
        // d. Let p be the substring of pattern from position beginIndex, exclusive, to position endIndex, exclusive.
706
0
        auto partition = pattern.substring_view(*begin_index + 1, end_index - *begin_index - 1);
707
708
        // e. Append a new Record { [[Type]]: p, [[Value]]: undefined } as the last element of the list result.
709
0
        result.append({ partition, {} });
710
711
        // f. Set nextIndex to endIndex + 1.
712
0
        next_index = end_index + 1;
713
714
        // g. Set beginIndex to StringIndexOf(pattern, "{", nextIndex).
715
0
        begin_index = pattern.find('{', next_index);
716
0
    }
717
718
    // 7. If nextIndex is less than length, then
719
0
    if (next_index < pattern.length()) {
720
        // a. Let literal be the substring of pattern from position nextIndex, inclusive, to position length, exclusive.
721
0
        auto literal = pattern.substring_view(next_index);
722
723
        // b. Append a new Record { [[Type]]: "literal", [[Value]]: literal } as the last element of the list result.
724
0
        result.append({ "literal"sv, MUST(String::from_utf8(literal)) });
725
0
    }
726
727
    // 8. Return result.
728
0
    return result;
729
0
}
730
731
}