[INTL] Implement Intl.DateTimeFormat.prototype.resolvedOptions ()
[WebKit.git] / Source / JavaScriptCore / runtime / IntlObject.cpp
1 /*
2  * Copyright (C) 2015 Andy VanWagoner (thetalecrafter@gmail.com)
3  * Copyright (C) 2015 Sukolsak Sakshuwong (sukolsak@gmail.com)
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  * 1. Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
15  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
16  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
18  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
24  * THE POSSIBILITY OF SUCH DAMAGE.
25  */
26
27 #include "config.h"
28 #include "IntlObject.h"
29
30 #if ENABLE(INTL)
31
32 #include "Error.h"
33 #include "FunctionPrototype.h"
34 #include "IntlCollator.h"
35 #include "IntlCollatorConstructor.h"
36 #include "IntlCollatorPrototype.h"
37 #include "IntlDateTimeFormat.h"
38 #include "IntlDateTimeFormatConstructor.h"
39 #include "IntlDateTimeFormatPrototype.h"
40 #include "IntlNumberFormat.h"
41 #include "IntlNumberFormatConstructor.h"
42 #include "IntlNumberFormatPrototype.h"
43 #include "JSCInlines.h"
44 #include "JSCJSValueInlines.h"
45 #include "Lookup.h"
46 #include "ObjectPrototype.h"
47 #include <unicode/uloc.h>
48 #include <unicode/unumsys.h>
49 #include <wtf/Assertions.h>
50 #include <wtf/NeverDestroyed.h>
51
52 namespace JSC {
53
54 STATIC_ASSERT_IS_TRIVIALLY_DESTRUCTIBLE(IntlObject);
55
56 }
57
58 namespace JSC {
59
60 struct MatcherResult {
61     String locale;
62     String extension;
63     size_t extensionIndex;
64 };
65
66 const ClassInfo IntlObject::s_info = { "Object", &Base::s_info, 0, CREATE_METHOD_TABLE(IntlObject) };
67
68 IntlObject::IntlObject(VM& vm, Structure* structure)
69     : JSNonFinalObject(vm, structure)
70 {
71 }
72
73 IntlObject* IntlObject::create(VM& vm, JSGlobalObject* globalObject, Structure* structure)
74 {
75     IntlObject* object = new (NotNull, allocateCell<IntlObject>(vm.heap)) IntlObject(vm, structure);
76     object->finishCreation(vm, globalObject);
77     return object;
78 }
79
80 void IntlObject::finishCreation(VM& vm, JSGlobalObject* globalObject)
81 {
82     Base::finishCreation(vm);
83     ASSERT(inherits(info()));
84
85     // Set up Collator.
86     IntlCollatorPrototype* collatorPrototype = IntlCollatorPrototype::create(vm, globalObject, IntlCollatorPrototype::createStructure(vm, globalObject, globalObject->objectPrototype()));
87     Structure* collatorStructure = IntlCollator::createStructure(vm, globalObject, collatorPrototype);
88     IntlCollatorConstructor* collatorConstructor = IntlCollatorConstructor::create(vm, IntlCollatorConstructor::createStructure(vm, globalObject, globalObject->functionPrototype()), collatorPrototype, collatorStructure);
89
90     collatorPrototype->putDirectWithoutTransition(vm, vm.propertyNames->constructor, collatorConstructor, DontEnum);
91
92     // Set up NumberFormat.
93     IntlNumberFormatPrototype* numberFormatPrototype = IntlNumberFormatPrototype::create(vm, globalObject, IntlNumberFormatPrototype::createStructure(vm, globalObject, globalObject->objectPrototype()));
94     Structure* numberFormatStructure = IntlNumberFormat::createStructure(vm, globalObject, numberFormatPrototype);
95     IntlNumberFormatConstructor* numberFormatConstructor = IntlNumberFormatConstructor::create(vm, IntlNumberFormatConstructor::createStructure(vm, globalObject, globalObject->functionPrototype()), numberFormatPrototype, numberFormatStructure);
96
97     numberFormatPrototype->putDirectWithoutTransition(vm, vm.propertyNames->constructor, numberFormatConstructor, DontEnum);
98
99     // Set up DateTimeFormat.
100     IntlDateTimeFormatPrototype* dateTimeFormatPrototype = IntlDateTimeFormatPrototype::create(vm, globalObject, IntlDateTimeFormatPrototype::createStructure(vm, globalObject, globalObject->objectPrototype()));
101     Structure* dateTimeFormatStructure = IntlDateTimeFormat::createStructure(vm, globalObject, dateTimeFormatPrototype);
102     IntlDateTimeFormatConstructor* dateTimeFormatConstructor = IntlDateTimeFormatConstructor::create(vm, IntlDateTimeFormatConstructor::createStructure(vm, globalObject, globalObject->functionPrototype()), dateTimeFormatPrototype, dateTimeFormatStructure);
103
104     dateTimeFormatPrototype->putDirectWithoutTransition(vm, vm.propertyNames->constructor, dateTimeFormatConstructor, DontEnum);
105
106     // 8.1 Properties of the Intl Object (ECMA-402 2.0)
107     putDirectWithoutTransition(vm, vm.propertyNames->Collator, collatorConstructor, DontEnum);
108     putDirectWithoutTransition(vm, vm.propertyNames->NumberFormat, numberFormatConstructor, DontEnum);
109     putDirectWithoutTransition(vm, vm.propertyNames->DateTimeFormat, dateTimeFormatConstructor, DontEnum);
110 }
111
112 Structure* IntlObject::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
113 {
114     return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info());
115 }
116
117 String defaultLocale()
118 {
119     // 6.2.4 DefaultLocale ()
120     String locale = uloc_getDefault();
121     convertICULocaleToBCP47LanguageTag(locale);
122     return locale;
123 }
124
125 void convertICULocaleToBCP47LanguageTag(String& locale)
126 {
127     locale.replace('_', '-');
128 }
129
130 bool intlBooleanOption(ExecState& state, JSValue options, PropertyName property, bool& usesFallback)
131 {
132     // 9.2.9 GetOption (options, property, type, values, fallback)
133     // For type="boolean". values is always undefined.
134
135     // 1. Let opts be ToObject(options).
136     JSObject* opts = options.toObject(&state);
137
138     // 2. ReturnIfAbrupt(opts).
139     if (state.hadException())
140         return false;
141
142     // 3. Let value be Get(opts, property).
143     JSValue value = opts->get(&state, property);
144
145     // 4. ReturnIfAbrupt(value).
146     if (state.hadException())
147         return false;
148
149     // 5. If value is not undefined, then
150     if (!value.isUndefined()) {
151         // a. Assert: type is "boolean" or "string".
152         // Function dedicated to "boolean".
153
154         // b. If type is "boolean", then
155         // i. Let value be ToBoolean(value).
156         bool booleanValue = value.toBoolean(&state);
157
158         // e. Return value.
159         usesFallback = false;
160         return booleanValue;
161     }
162
163     // 6. Else return fallback.
164     // Because fallback can be undefined, we let the caller handle it instead.
165     usesFallback = true;
166     return false;
167 }
168
169 String intlStringOption(ExecState& state, JSValue options, PropertyName property, std::initializer_list<const char*> values, const char* notFound, const char* fallback)
170 {
171     // 9.2.9 GetOption (options, property, type, values, fallback)
172     // For type="string".
173
174     // 1. Let opts be ToObject(options).
175     JSObject* opts = options.toObject(&state);
176
177     // 2. ReturnIfAbrupt(opts).
178     if (state.hadException())
179         return { };
180
181     // 3. Let value be Get(opts, property).
182     JSValue value = opts->get(&state, property);
183
184     // 4. ReturnIfAbrupt(value).
185     if (state.hadException())
186         return { };
187
188     // 5. If value is not undefined, then
189     if (!value.isUndefined()) {
190         // a. Assert: type is "boolean" or "string".
191         // Function dedicated to "string".
192
193         // c. If type is "string", then
194         // i. Let value be ToString(value).
195         String stringValue = value.toWTFString(&state);
196
197         // ii. ReturnIfAbrupt(value).
198         if (state.hadException())
199             return { };
200
201         // d. If values is not undefined, then
202         // i. If values does not contain an element equal to value, throw a RangeError exception.
203         if (values.size() && std::find(values.begin(), values.end(), stringValue) == values.end()) {
204             state.vm().throwException(&state, createRangeError(&state, notFound));
205             return { };
206         }
207
208         // e. Return value.
209         return stringValue;
210     }
211
212     // 6. Else return fallback.
213     return fallback;
214 }
215
216 static String privateUseLangTag(const Vector<String>& parts, size_t startIndex)
217 {
218     size_t numParts = parts.size();
219     size_t currentIndex = startIndex;
220
221     // Check for privateuse.
222     // privateuse = "x" 1*("-" (2*8alphanum))
223     StringBuilder privateuse;
224     while (currentIndex < numParts) {
225         const String& singleton = parts[currentIndex];
226         unsigned singletonLength = singleton.length();
227         bool isValid = (singletonLength == 1 && (singleton == "x" || singleton == "X"));
228         if (!isValid)
229             break;
230
231         if (currentIndex != startIndex)
232             privateuse.append('-');
233
234         ++currentIndex;
235         unsigned numExtParts = 0;
236         privateuse.append('x');
237         while (currentIndex < numParts) {
238             const String& extPart = parts[currentIndex];
239             unsigned extPartLength = extPart.length();
240
241             bool isValid = (extPartLength >= 2 && extPartLength <= 8 && extPart.isAllSpecialCharacters<isASCIIAlphanumeric>());
242             if (!isValid)
243                 break;
244
245             ++currentIndex;
246             ++numExtParts;
247             privateuse.append('-');
248             privateuse.append(extPart.convertToASCIILowercase());
249         }
250
251         // Requires at least one production.
252         if (!numExtParts)
253             return String();
254     }
255
256     // Leftovers makes it invalid.
257     if (currentIndex < numParts)
258         return String();
259
260     return privateuse.toString();
261 }
262
263 static String canonicalLangTag(const Vector<String>& parts)
264 {
265     ASSERT(!parts.isEmpty());
266
267     // Follows the grammar at https://www.rfc-editor.org/rfc/bcp/bcp47.txt
268     // langtag = language ["-" script] ["-" region] *("-" variant) *("-" extension) ["-" privateuse]
269
270     size_t numParts = parts.size();
271     // Check for language.
272     // language = 2*3ALPHA ["-" extlang] / 4ALPHA / 5*8ALPHA
273     size_t currentIndex = 0;
274     const String& language = parts[currentIndex];
275     unsigned languageLength = language.length();
276     bool canHaveExtlang = languageLength >= 2 && languageLength <= 3;
277     bool isValidLanguage = languageLength >= 2 && languageLength <= 8 && language.isAllSpecialCharacters<isASCIIAlpha>();
278     if (!isValidLanguage)
279         return String();
280
281     ++currentIndex;
282     StringBuilder canonical;
283     canonical.append(language.convertToASCIILowercase());
284
285     // Check for extlang.
286     // extlang = 3ALPHA *2("-" 3ALPHA)
287     if (canHaveExtlang) {
288         for (unsigned times = 0; times < 3 && currentIndex < numParts; ++times) {
289             const String& extlang = parts[currentIndex];
290             unsigned extlangLength = extlang.length();
291             if (extlangLength == 3 && extlang.isAllSpecialCharacters<isASCIIAlpha>()) {
292                 ++currentIndex;
293                 canonical.append('-');
294                 canonical.append(extlang.convertToASCIILowercase());
295             } else
296                 break;
297         }
298     }
299
300     // Check for script.
301     // script = 4ALPHA
302     if (currentIndex < numParts) {
303         const String& script = parts[currentIndex];
304         unsigned scriptLength = script.length();
305         if (scriptLength == 4 && script.isAllSpecialCharacters<isASCIIAlpha>()) {
306             ++currentIndex;
307             canonical.append('-');
308             canonical.append(toASCIIUpper(script[0]));
309             canonical.append(script.substring(1, 3).convertToASCIILowercase());
310         }
311     }
312
313     // Check for region.
314     // region = 2ALPHA / 3DIGIT
315     if (currentIndex < numParts) {
316         const String& region = parts[currentIndex];
317         unsigned regionLength = region.length();
318         bool isValidRegion = (
319             (regionLength == 2 && region.isAllSpecialCharacters<isASCIIAlpha>())
320             || (regionLength == 3 && region.isAllSpecialCharacters<isASCIIDigit>())
321         );
322         if (isValidRegion) {
323             ++currentIndex;
324             canonical.append('-');
325             canonical.append(region.upper());
326         }
327     }
328
329     // Check for variant.
330     // variant = 5*8alphanum / (DIGIT 3alphanum)
331     HashSet<String> subtags;
332     while (currentIndex < numParts) {
333         const String& variant = parts[currentIndex];
334         unsigned variantLength = variant.length();
335         bool isValidVariant = (
336             (variantLength >= 5 && variantLength <= 8 && variant.isAllSpecialCharacters<isASCIIAlphanumeric>())
337             || (variantLength == 4 && isASCIIDigit(variant[0]) && variant.substring(1, 3).isAllSpecialCharacters<isASCIIAlphanumeric>())
338         );
339         if (!isValidVariant)
340             break;
341
342         // Cannot include duplicate subtags (case insensitive).
343         String lowerVariant = variant.convertToASCIILowercase();
344         if (!subtags.add(lowerVariant).isNewEntry)
345             return String();
346
347         ++currentIndex;
348
349         // Reordering variant subtags is not required in the spec.
350         canonical.append('-');
351         canonical.append(lowerVariant);
352     }
353
354     // Check for extension.
355     // extension = singleton 1*("-" (2*8alphanum))
356     // singleton = alphanum except x or X
357     subtags.clear();
358     Vector<String> extensions;
359     while (currentIndex < numParts) {
360         const String& possibleSingleton = parts[currentIndex];
361         unsigned singletonLength = possibleSingleton.length();
362         bool isValidSingleton = (singletonLength == 1 && possibleSingleton != "x" && possibleSingleton != "X" && isASCIIAlphanumeric(possibleSingleton[0]));
363         if (!isValidSingleton)
364             break;
365
366         // Cannot include duplicate singleton (case insensitive).
367         String singleton = possibleSingleton.convertToASCIILowercase();
368         if (!subtags.add(singleton).isNewEntry)
369             return String();
370
371         ++currentIndex;
372         int numExtParts = 0;
373         StringBuilder extension;
374         extension.append(singleton);
375         while (currentIndex < numParts) {
376             const String& extPart = parts[currentIndex];
377             unsigned extPartLength = extPart.length();
378
379             bool isValid = (extPartLength >= 2 && extPartLength <= 8 && extPart.isAllSpecialCharacters<isASCIIAlphanumeric>());
380             if (!isValid)
381                 break;
382
383             ++currentIndex;
384             ++numExtParts;
385             extension.append('-');
386             extension.append(extPart.convertToASCIILowercase());
387         }
388
389         // Requires at least one production.
390         if (!numExtParts)
391             return String();
392
393         extensions.append(extension.toString());
394     }
395
396     // Add extensions to canonical sorted by singleton.
397     std::sort(
398         extensions.begin(),
399         extensions.end(),
400         [] (const String& a, const String& b) -> bool {
401             return a[0] < b[0];
402         }
403     );
404     size_t numExtenstions = extensions.size();
405     for (size_t i = 0; i < numExtenstions; ++i) {
406         canonical.append('-');
407         canonical.append(extensions[i]);
408     }
409
410     // Check for privateuse.
411     if (currentIndex < numParts) {
412         String privateuse = privateUseLangTag(parts, currentIndex);
413         if (privateuse.isNull())
414             return String();
415         canonical.append('-');
416         canonical.append(privateuse);
417     }
418
419     // FIXME: Replace subtags with their preferred values.
420
421     return canonical.toString();
422 }
423
424 static String grandfatheredLangTag(const String& locale)
425 {
426     // grandfathered = irregular / regular
427     // FIXME: convert to a compile time hash table if this is causing performance issues.
428     HashMap<String, String> tagMap = {
429         // Irregular.
430         { ASCIILiteral("en-gb-oed"), ASCIILiteral("en-GB-oed") },
431         { ASCIILiteral("i-ami"), ASCIILiteral("ami") },
432         { ASCIILiteral("i-bnn"), ASCIILiteral("bnn") },
433         { ASCIILiteral("i-default"), ASCIILiteral("i-default") },
434         { ASCIILiteral("i-enochian"), ASCIILiteral("i-enochian") },
435         { ASCIILiteral("i-hak"), ASCIILiteral("hak") },
436         { ASCIILiteral("i-klingon"), ASCIILiteral("tlh") },
437         { ASCIILiteral("i-lux"), ASCIILiteral("lb") },
438         { ASCIILiteral("i-mingo"), ASCIILiteral("i-mingo") },
439         { ASCIILiteral("i-navajo"), ASCIILiteral("nv") },
440         { ASCIILiteral("i-pwn"), ASCIILiteral("pwn") },
441         { ASCIILiteral("i-tao"), ASCIILiteral("tao") },
442         { ASCIILiteral("i-tay"), ASCIILiteral("tay") },
443         { ASCIILiteral("i-tsu"), ASCIILiteral("tsu") },
444         { ASCIILiteral("sgn-be-fr"), ASCIILiteral("sfb") },
445         { ASCIILiteral("sgn-be-nl"), ASCIILiteral("vgt") },
446         { ASCIILiteral("sgn-ch-de"), ASCIILiteral("sgg") },
447         // Regular.
448         { ASCIILiteral("art-lojban"), ASCIILiteral("jbo") },
449         { ASCIILiteral("cel-gaulish"), ASCIILiteral("cel-gaulish") },
450         { ASCIILiteral("no-bok"), ASCIILiteral("nb") },
451         { ASCIILiteral("no-nyn"), ASCIILiteral("nn") },
452         { ASCIILiteral("zh-guoyu"), ASCIILiteral("cmn") },
453         { ASCIILiteral("zh-hakka"), ASCIILiteral("hak") },
454         { ASCIILiteral("zh-min"), ASCIILiteral("zh-min") },
455         { ASCIILiteral("zh-min-nan"), ASCIILiteral("nan") },
456         { ASCIILiteral("zh-xiang"), ASCIILiteral("hsn") }
457     };
458
459     return tagMap.get(locale.convertToASCIILowercase());
460 }
461
462 static String canonicalizeLanguageTag(const String& locale)
463 {
464     // 6.2.2 IsStructurallyValidLanguageTag (locale)
465     // 6.2.3 CanonicalizeLanguageTag (locale)
466     // These are done one after another in CanonicalizeLocaleList, so they are combined here to reduce duplication.
467     // https://www.rfc-editor.org/rfc/bcp/bcp47.txt
468
469     // Language-Tag = langtag / privateuse / grandfathered
470     String grandfather = grandfatheredLangTag(locale);
471     if (!grandfather.isNull())
472         return grandfather;
473
474     // FIXME: Replace redundant tags [RFC4647].
475
476     Vector<String> parts;
477     locale.split('-', true, parts);
478     if (!parts.isEmpty()) {
479         String langtag = canonicalLangTag(parts);
480         if (!langtag.isNull())
481             return langtag;
482
483         String privateuse = privateUseLangTag(parts, 0);
484         if (!privateuse.isNull())
485             return privateuse;
486     }
487
488     return String();
489 }
490
491 Vector<String> canonicalizeLocaleList(ExecState& state, JSValue locales)
492 {
493     // 9.2.1 CanonicalizeLocaleList (locales)
494     VM& vm = state.vm();
495     JSGlobalObject* globalObject = state.callee()->globalObject();
496     Vector<String> seen;
497
498     // 1. If locales is undefined, then a. Return a new empty List.
499     if (locales.isUndefined())
500         return seen;
501
502     // 2. Let seen be an empty List.
503     // Done before to also return in step 1, if needed.
504
505     // 3. If Type(locales) is String, then
506     JSObject* localesObject;
507     if (locales.isString()) {
508         //  a. Let aLocales be CreateArrayFromList(«locales»).
509         JSArray* localesArray = JSArray::tryCreateUninitialized(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous), 1);
510         localesArray->initializeIndex(vm, 0, locales);
511         // 4. Let O be ToObject(aLocales).
512         localesObject = localesArray;
513     } else {
514         // 4. Let O be ToObject(aLocales).
515         localesObject = locales.toObject(&state);
516     }
517
518     // 5. ReturnIfAbrupt(O).
519     if (state.hadException())
520         return Vector<String>();
521
522     // 6. Let len be ToLength(Get(O, "length")).
523     JSValue lengthProperty = localesObject->get(&state, vm.propertyNames->length);
524     if (state.hadException())
525         return Vector<String>();
526
527     double length = lengthProperty.toLength(&state);
528     if (state.hadException())
529         return Vector<String>();
530
531     // Keep track of locales that have been added to the list.
532     HashSet<String> seenSet;
533
534     // 7. Let k be 0.
535     // 8. Repeat, while k < len
536     for (double k = 0; k < length; ++k) {
537         // a. Let Pk be ToString(k).
538         // Not needed because hasProperty and get take an int for numeric key.
539
540         // b. Let kPresent be HasProperty(O, Pk).
541         bool kPresent = localesObject->hasProperty(&state, k);
542
543         // c. ReturnIfAbrupt(kPresent).
544         if (state.hadException())
545             return Vector<String>();
546
547         // d. If kPresent is true, then
548         if (kPresent) {
549             // i. Let kValue be Get(O, Pk).
550             JSValue kValue = localesObject->get(&state, k);
551
552             // ii. ReturnIfAbrupt(kValue).
553             if (state.hadException())
554                 return Vector<String>();
555
556             // iii. If Type(kValue) is not String or Object, throw a TypeError exception.
557             if (!kValue.isString() && !kValue.isObject()) {
558                 throwTypeError(&state, ASCIILiteral("locale value must be a string or object"));
559                 return Vector<String>();
560             }
561
562             // iv. Let tag be ToString(kValue).
563             JSString* tag = kValue.toString(&state);
564
565             // v. ReturnIfAbrupt(tag).
566             if (state.hadException())
567                 return Vector<String>();
568
569             // vi. If IsStructurallyValidLanguageTag(tag) is false, throw a RangeError exception.
570             // vii. Let canonicalizedTag be CanonicalizeLanguageTag(tag).
571             String canonicalizedTag = canonicalizeLanguageTag(tag->value(&state));
572             if (canonicalizedTag.isNull()) {
573                 state.vm().throwException(&state, createRangeError(&state, String::format("invalid language tag: %s", tag->value(&state).utf8().data())));
574                 return Vector<String>();
575             }
576
577             // viii. If canonicalizedTag is not an element of seen, append canonicalizedTag as the last element of seen.
578             if (seenSet.add(canonicalizedTag).isNewEntry)
579                 seen.append(canonicalizedTag);
580         }
581         // e. Increase k by 1.
582     }
583
584     return seen;
585 }
586
587 String bestAvailableLocale(const HashSet<String>& availableLocales, const String& locale)
588 {
589     // 9.2.2 BestAvailableLocale (availableLocales, locale)
590     // 1. Let candidate be locale.
591     String candidate = locale;
592
593     // 2. Repeat
594     while (!candidate.isEmpty()) {
595         // a. If availableLocales contains an element equal to candidate, then return candidate.
596         if (availableLocales.contains(candidate))
597             return candidate;
598
599         // b. Let pos be the character index of the last occurrence of "-" (U+002D) within candidate. If that character does not occur, return undefined.
600         size_t pos = candidate.reverseFind('-');
601         if (pos == notFound)
602             return String();
603
604         // c. If pos ≥ 2 and the character "-" occurs at index pos-2 of candidate, then decrease pos by 2.
605         if (pos >= 2 && candidate[pos - 2] == '-')
606             pos -= 2;
607
608         // d. Let candidate be the substring of candidate from position 0, inclusive, to position pos, exclusive.
609         candidate = candidate.substring(0, pos);
610     }
611
612     return String();
613 }
614
615 String removeUnicodeLocaleExtension(const String& locale)
616 {
617     Vector<String> parts;
618     locale.split('-', parts);
619     StringBuilder builder;
620     size_t partsSize = parts.size();
621     if (partsSize > 0)
622         builder.append(parts[0]);
623     for (size_t p = 1; p < partsSize; ++p) {
624         if (parts[p] == "u") {
625             // Skip the u- and anything that follows until another singleton.
626             // While the next part is part of the unicode extension, skip it.
627             while (p + 1 < partsSize && parts[p + 1].length() > 1)
628                 ++p;
629         } else {
630             builder.append('-');
631             builder.append(parts[p]);
632         }
633     }
634     return builder.toString();
635 }
636
637 static MatcherResult lookupMatcher(const HashSet<String>& availableLocales, const Vector<String>& requestedLocales)
638 {
639     // 9.2.3 LookupMatcher (availableLocales, requestedLocales) (ECMA-402 2.0)
640     String locale;
641     String noExtensionsLocale;
642     String availableLocale;
643     for (size_t i = 0; i < requestedLocales.size() && availableLocale.isNull(); ++i) {
644         locale = requestedLocales[i];
645         noExtensionsLocale = removeUnicodeLocaleExtension(locale);
646         availableLocale = bestAvailableLocale(availableLocales, noExtensionsLocale);
647     }
648
649     MatcherResult result;
650     if (!availableLocale.isNull()) {
651         result.locale = availableLocale;
652         if (locale != noExtensionsLocale) {
653             // i. Let extension be the String value consisting of the first substring of locale that is a Unicode locale extension sequence.
654             // ii. Let extensionIndex be the character position of the initial "-" extension sequence within locale.
655             size_t extensionIndex = locale.find("-u-");
656             RELEASE_ASSERT(extensionIndex != notFound);
657
658             size_t extensionLength = locale.length() - extensionIndex;
659             size_t end = extensionIndex + 3;
660             while (end < locale.length()) {
661                 end = locale.find('-', end);
662                 if (end == notFound)
663                     break;
664                 if (end + 2 < locale.length() && locale[end + 2] == '-') {
665                     extensionLength = end - extensionIndex;
666                     break;
667                 }
668                 end++;
669             }
670             result.extension = locale.substring(extensionIndex, extensionLength);
671             result.extensionIndex = extensionIndex;
672         }
673     } else
674         result.locale = defaultLocale();
675     return result;
676 }
677
678 static MatcherResult bestFitMatcher(const HashSet<String>& availableLocales, const Vector<String>& requestedLocales)
679 {
680     // 9.2.4 BestFitMatcher (availableLocales, requestedLocales) (ECMA-402 2.0)
681     // FIXME: Implement something better than lookup.
682     return lookupMatcher(availableLocales, requestedLocales);
683 }
684
685 HashMap<String, String> resolveLocale(const HashSet<String>& availableLocales, const Vector<String>& requestedLocales, const HashMap<String, String>& options, const char* const relevantExtensionKeys[], size_t relevantExtensionKeyCount, Vector<String> (*localeData)(const String&, size_t))
686 {
687     // 9.2.5 ResolveLocale (availableLocales, requestedLocales, options, relevantExtensionKeys, localeData) (ECMA-402 2.0)
688     // 1. Let matcher be the value of options.[[localeMatcher]].
689     const String& matcher = options.get(ASCIILiteral("localeMatcher"));
690
691     // 2. If matcher is "lookup", then
692     MatcherResult (*matcherOperation)(const HashSet<String>&, const Vector<String>&);
693     if (matcher == "lookup") {
694         // a. Let MatcherOperation be the abstract operation LookupMatcher.
695         matcherOperation = lookupMatcher;
696     } else { // 3. Else
697         // a. Let MatcherOperation be the abstract operation BestFitMatcher.
698         matcherOperation = bestFitMatcher;
699     }
700
701     // 4. Let r be MatcherOperation(availableLocales, requestedLocales).
702     MatcherResult matcherResult = matcherOperation(availableLocales, requestedLocales);
703
704     // 5. Let foundLocale be the value of r.[[locale]].
705     String foundLocale = matcherResult.locale;
706
707     // 6. If r has an [[extension]] field, then
708     Vector<String> extensionSubtags;
709     if (!matcherResult.extension.isNull()) {
710         // a. Let extension be the value of r.[[extension]].
711         // b. Let extensionIndex be the value of r.[[extensionIndex]].
712         // c. Let extensionSubtags be Call(%StringProto_split%, extension, «"-"») .
713         // d. Let extensionSubtagsLength be Get(CreateArrayFromList(extensionSubtags), "length").
714         matcherResult.extension.split('-', extensionSubtags);
715     }
716
717     // 7. Let result be a new Record.
718     HashMap<String, String> result;
719
720     // 8. Set result.[[dataLocale]] to foundLocale.
721     result.add(ASCIILiteral("dataLocale"), foundLocale);
722
723     // 9. Let supportedExtension be "-u".
724     String supportedExtension = ASCIILiteral("-u");
725
726     // 10. Let k be 0.
727     // 11. Let rExtensionKeys be ToObject(CreateArrayFromList(relevantExtensionKeys)).
728     // 12. ReturnIfAbrupt(rExtensionKeys).
729     // 13. Let len be ToLength(Get(rExtensionKeys, "length")).
730     // 14. Repeat while k < len
731     for (size_t keyIndex = 0; keyIndex < relevantExtensionKeyCount; ++keyIndex) {
732         // a. Let key be Get(rExtensionKeys, ToString(k)).
733         // b. ReturnIfAbrupt(key).
734         const char* key = relevantExtensionKeys[keyIndex];
735
736         // c. Let foundLocaleData be Get(localeData, foundLocale).
737         // d. ReturnIfAbrupt(foundLocaleData).
738         // e. Let keyLocaleData be ToObject(Get(foundLocaleData, key)).
739         // f. ReturnIfAbrupt(keyLocaleData).
740         Vector<String> keyLocaleData = localeData(foundLocale, keyIndex);
741
742         // g. Let value be ToString(Get(keyLocaleData, "0")).
743         // h. ReturnIfAbrupt(value).
744         ASSERT(!keyLocaleData.isEmpty());
745         String value = keyLocaleData[0];
746
747         // i. Let supportedExtensionAddition be "".
748         String supportedExtensionAddition;
749
750         // j. If extensionSubtags is not undefined, then
751         if (!extensionSubtags.isEmpty()) {
752             // i. Let keyPos be Call(%ArrayProto_indexOf%, extensionSubtags, «key») .
753             size_t keyPos = extensionSubtags.find(key);
754             // ii. If keyPos != -1, then
755             if (keyPos != notFound) {
756                 // FIXME: https://github.com/tc39/ecma402/issues/59
757                 // 1. If keyPos + 1 < extensionSubtagsLength and the length of the result of Get(extensionSubtags, ToString(keyPos +1)) is greater than 2, then
758                 if (keyPos + 1 < extensionSubtags.size() && extensionSubtags[keyPos + 1].length() > 2) {
759                     const String& requestedValue = extensionSubtags[keyPos + 1];
760                     if (keyLocaleData.contains(requestedValue)) {
761                         value = requestedValue;
762                         supportedExtensionAddition = makeString('-', key, '-', value);
763                     }
764                 } else if (keyLocaleData.contains(static_cast<String>(ASCIILiteral("true")))) {
765                     // 2. Else, if the result of Call(%StringProto_includes%, keyLocaleData, «"true"») is true, then
766                     value = ASCIILiteral("true");
767                 }
768             }
769         }
770
771         // k. If options has a field [[<key>]], then
772         HashMap<String, String>::const_iterator iterator = options.find(key);
773         if (iterator != options.end()) {
774             // i. Let optionsValue be the value of ToString(options.[[<key>]]).
775             // ii. ReturnIfAbrupt(optionsValue).
776             const String& optionsValue = iterator->value;
777             // iii. If the result of Call(%StringProto_includes%, keyLocaleData, «optionsValue») is true, then
778             if (!optionsValue.isNull() && keyLocaleData.contains(optionsValue)) {
779                 // 1. If optionsValue is not equal to value, then
780                 if (optionsValue != value) {
781                     value = optionsValue;
782                     supportedExtensionAddition = String();
783                 }
784             }
785         }
786
787         // l. Set result.[[<key>]] to value.
788         result.add(key, value);
789
790         // m. Append supportedExtensionAddition to supportedExtension.
791         supportedExtension.append(supportedExtensionAddition);
792
793         // n. Increase k by 1.
794     }
795
796     // 15. If the number of elements in supportedExtension is greater than 2, then
797     if (supportedExtension.length() > 2) {
798         // a. Let preExtension be the substring of foundLocale from position 0, inclusive, to position extensionIndex, exclusive.
799         // b. Let postExtension be the substring of foundLocale from position extensionIndex to the end of the string.
800         // c. Let foundLocale be the concatenation of preExtension, supportedExtension, and postExtension.
801         String preExtension = foundLocale.substring(0, matcherResult.extensionIndex);
802         String postExtension = foundLocale.substring(matcherResult.extensionIndex);
803         foundLocale = preExtension + supportedExtension + postExtension;
804     }
805
806     // 16. Set result.[[locale]] to foundLocale.
807     result.add(ASCIILiteral("locale"), foundLocale);
808
809     // 17. Return result.
810     return result;
811 }
812
813 static JSArray* lookupSupportedLocales(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales)
814 {
815     // 9.2.6 LookupSupportedLocales (availableLocales, requestedLocales)
816
817     // 1. Let rLocales be CreateArrayFromList(requestedLocales).
818     // Already an array.
819
820     // 2. Let len be ToLength(Get(rLocales, "length")).
821     size_t len = requestedLocales.size();
822
823     // 3. Let subset be an empty List.
824     VM& vm = state.vm();
825     JSGlobalObject* globalObject = state.callee()->globalObject();
826     JSArray* subset = JSArray::tryCreateUninitialized(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithUndecided), 0);
827     if (!subset) {
828         throwOutOfMemoryError(&state);
829         return nullptr;
830     }
831
832     // 4. Let k be 0.
833     // 5. Repeat while k < len
834     for (size_t k = 0; k < len; ++k) {
835         // a. Let Pk be ToString(k).
836         // b. Let locale be Get(rLocales, Pk).
837         // c. ReturnIfAbrupt(locale).
838         const String& locale = requestedLocales[k];
839
840         // d. Let noExtensionsLocale be the String value that is locale with all Unicode locale extension sequences removed.
841         String noExtensionsLocale = removeUnicodeLocaleExtension(locale);
842
843         // e. Let availableLocale be BestAvailableLocale(availableLocales, noExtensionsLocale).
844         String availableLocale = bestAvailableLocale(availableLocales, noExtensionsLocale);
845
846         // f. If availableLocale is not undefined, then append locale to the end of subset.
847         if (!availableLocale.isNull())
848             subset->push(&state, jsString(&state, locale));
849
850         // g. Increment k by 1.
851     }
852
853     // 6. Return subset.
854     return subset;
855 }
856
857 static JSArray* bestFitSupportedLocales(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales)
858 {
859     // 9.2.7 BestFitSupportedLocales (availableLocales, requestedLocales)
860     // FIXME: Implement something better than lookup.
861     return lookupSupportedLocales(state, availableLocales, requestedLocales);
862 }
863
864 JSValue supportedLocales(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales, JSValue options)
865 {
866     // 9.2.8 SupportedLocales (availableLocales, requestedLocales, options)
867     VM& vm = state.vm();
868     String matcher;
869
870     // 1. If options is not undefined, then
871     if (!options.isUndefined()) {
872         // a. Let matcher be GetOption(options, "localeMatcher", "string", « "lookup", "best fit" », "best fit").
873         matcher = intlStringOption(state, options, vm.propertyNames->localeMatcher, { "lookup", "best fit" }, "localeMatcher must be either \"lookup\" or \"best fit\"", "best fit");
874         // b. ReturnIfAbrupt(matcher).
875         if (state.hadException())
876             return jsUndefined();
877     } else {
878         // 2. Else, let matcher be "best fit".
879         matcher = ASCIILiteral("best fit");
880     }
881
882     JSArray* supportedLocales;
883     // 3. If matcher is "best fit",
884     if (matcher == "best fit") {
885         // a. Let MatcherOperation be the abstract operation BestFitSupportedLocales.
886         // 5. Let supportedLocales be MatcherOperation(availableLocales, requestedLocales).
887         supportedLocales = bestFitSupportedLocales(state, availableLocales, requestedLocales);
888     } else {
889         // 4. Else
890         // a. Let MatcherOperation be the abstract operation LookupSupportedLocales.
891         // 5. Let supportedLocales be MatcherOperation(availableLocales, requestedLocales).
892         supportedLocales = lookupSupportedLocales(state, availableLocales, requestedLocales);
893     }
894
895     if (state.hadException())
896         return jsUndefined();
897
898     // 6. Let subset be CreateArrayFromList(supportedLocales).
899     // Already an array.
900
901     // 7. Let keys be subset.[[OwnPropertyKeys]]().
902     PropertyNameArray keys(&state, PropertyNameMode::Strings);
903     supportedLocales->getOwnPropertyNames(supportedLocales, &state, keys, EnumerationMode());
904
905     PropertyDescriptor desc;
906     desc.setConfigurable(false);
907     desc.setWritable(false);
908
909     // 8. Repeat for each element P of keys in List order,
910     size_t len = keys.size();
911     for (size_t i = 0; i < len; ++i) {
912         // a. Let desc be PropertyDescriptor { [[Configurable]]: false, [[Writable]]: false }.
913         // Created above for reuse.
914
915         // b. Let status be DefinePropertyOrThrow(subset, P, desc).
916         supportedLocales->defineOwnProperty(supportedLocales, &state, keys[i], desc, true);
917
918         // c. Assert: status is not abrupt completion.
919         if (state.hadException())
920             return jsUndefined();
921     }
922
923     // 9. Return subset.
924     return supportedLocales;
925 }
926
927 Vector<String> getNumberingSystemsForLocale(const String& locale)
928 {
929     static NeverDestroyed<Vector<String>> cachedNumberingSystems;
930     Vector<String>& availableNumberingSystems = cachedNumberingSystems.get();
931     if (availableNumberingSystems.isEmpty()) {
932         UErrorCode status(U_ZERO_ERROR);
933         UEnumeration* numberingSystemNames = unumsys_openAvailableNames(&status);
934         ASSERT(U_SUCCESS(status));
935         status = U_ZERO_ERROR;
936
937         int32_t resultLength;
938         // Numbering system names are always ASCII, so use char[].
939         while (const char* result = uenum_next(numberingSystemNames, &resultLength, &status)) {
940             ASSERT(U_SUCCESS(status));
941             status = U_ZERO_ERROR;
942             availableNumberingSystems.append(String(result, resultLength));
943         }
944         uenum_close(numberingSystemNames);
945     }
946
947     UErrorCode status(U_ZERO_ERROR);
948     UNumberingSystem* defaultSystem = unumsys_open(locale.utf8().data(), &status);
949     ASSERT(U_SUCCESS(status));
950     String defaultSystemName(unumsys_getName(defaultSystem));
951     unumsys_close(defaultSystem);
952
953     Vector<String> numberingSystems({ defaultSystemName });
954     numberingSystems.appendVector(availableNumberingSystems);
955     return numberingSystems;
956 }
957
958 } // namespace JSC
959
960 #endif // ENABLE(INTL)