/**
* Copyright (c) 2012-2016 André Bargull
* Alle Rechte vorbehalten / All Rights Reserved. Use is subject to license terms.
*
* <https://github.com/anba/es6draft>
*/
package com.github.anba.es6draft.runtime.objects.intl;
import static com.github.anba.es6draft.runtime.AbstractOperations.*;
import static com.github.anba.es6draft.runtime.internal.Errors.newRangeError;
import static com.github.anba.es6draft.runtime.internal.Errors.newTypeError;
import static com.github.anba.es6draft.runtime.types.builtins.ArrayObject.ArrayCreate;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import com.github.anba.es6draft.runtime.ExecutionContext;
import com.github.anba.es6draft.runtime.Realm;
import com.github.anba.es6draft.runtime.internal.Lazy;
import com.github.anba.es6draft.runtime.internal.Messages;
import com.github.anba.es6draft.runtime.objects.intl.LanguageTagParser.LanguageTag;
import com.github.anba.es6draft.runtime.types.PropertyDescriptor;
import com.github.anba.es6draft.runtime.types.ScriptObject;
import com.github.anba.es6draft.runtime.types.Type;
import com.github.anba.es6draft.runtime.types.builtins.ArrayObject;
import com.ibm.icu.util.LocaleMatcher;
import com.ibm.icu.util.LocalePriorityList;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.TimeZone.SystemTimeZoneType;
import com.ibm.icu.util.ULocale;
/**
* <h1>9 Locale and Parameter Negotiation</h1><br>
* <h2>9.2 Abstract Operations</h2>
*/
public final class IntlAbstractOperations {
private IntlAbstractOperations() {
}
/**
* 6.1 Case Sensitivity and Case Mapping
*
* @param s
* the string
* @return the upper case string
*/
public static String ToUpperCase(String s) {
char[] ca = s.toCharArray();
for (int i = 0, len = ca.length; i < len; ++i) {
char c = ca[i];
if ('a' <= c && c <= 'z') {
c = (char) (c - ('a' - 'A'));
}
ca[i] = c;
}
return new String(ca);
}
/**
*
*/
public static final class LanguageTagUnicodeExtension {
final String languageTag;
final int startIndex;
final int endIndex;
LanguageTagUnicodeExtension(String languageTag) {
this.languageTag = languageTag;
this.startIndex = findStartIndex(languageTag);
this.endIndex = findEndIndex(languageTag, startIndex);
}
/**
* Returns the language tag with the unicode extension sequence removed.
*
* @return the language tag with the unicode extension sequence removed
*/
public String getLanguageTag() {
return removeUnicodeExtension(languageTag, startIndex, endIndex);
}
/**
* Returns the unicode extension sequence.
*
* @return the unicode extension sequence
*/
public String getUnicodeExtension() {
if (startIndex == -1) {
return "";
}
return languageTag.substring(startIndex, endIndex);
}
int getSubtagStartIndex() {
if (startIndex == -1) {
return -1;
}
return startIndex + 2;
}
static String removeExtension(String languageTag) {
int startIndex = findStartIndex(languageTag);
int endIndex = findEndIndex(languageTag, startIndex);
return removeUnicodeExtension(languageTag, startIndex, endIndex);
}
private static String removeUnicodeExtension(String languageTag, int startIndex, int endIndex) {
if (startIndex == -1) {
return languageTag;
}
if (endIndex == languageTag.length()) {
return languageTag.substring(0, startIndex);
}
return languageTag.substring(0, startIndex) + languageTag.substring(endIndex);
}
private static int findStartIndex(String languageTag) {
if (languageTag.startsWith("x-")) {
// privateuse-only case
return -1;
}
int indexUnicode = languageTag.indexOf("-u-");
if (indexUnicode == -1) {
// no unicode extension
return -1;
}
int indexPrivateUse = languageTag.lastIndexOf("-x-", indexUnicode);
if (indexPrivateUse != -1) {
// -u- in privateuse case
return -1;
}
return indexUnicode;
}
private static int findEndIndex(String languageTag, int indexUnicode) {
// found unicode extension, search end index
if (indexUnicode == -1) {
return -1;
}
int endIndex = languageTag.length();
for (int i = indexUnicode + 3;;) {
int sep = languageTag.indexOf('-', i);
if (sep == -1) {
// end of string reached
break;
}
assert sep + 2 < languageTag.length() : languageTag;
if (languageTag.charAt(sep + 2) == '-') {
// next singleton found
endIndex = sep;
break;
}
i = sep + 1;
}
return endIndex;
}
}
/**
* 6.2.1 Unicode Locale Extension Sequences
*
* @param languageTag
* the canonicalized language tag
* @return the pair {@code [languageTagWithoutExtension, unicodeExtension]}
*/
public static LanguageTagUnicodeExtension UnicodeLocaleExtSequence(String languageTag) {
return new LanguageTagUnicodeExtension(languageTag);
}
/**
* 6.2.1 Unicode Locale Extension Sequences
*
* @param languageTag
* the canonicalized language tag
* @return the language tag with the unicode extension sequence removed
*/
public static String RemoveUnicodeLocaleExtension(String languageTag) {
return LanguageTagUnicodeExtension.removeExtension(languageTag);
}
/**
* 6.2.2 IsStructurallyValidLanguageTag (locale)
*
* @param locale
* the locale string
* @return the parsed language tag or {@code null} if the locale is not a valid language tag
*/
public static LanguageTag IsStructurallyValidLanguageTag(String locale) {
return LanguageTagParser.parse(locale);
}
/**
* 6.2.3 CanonicalizeLanguageTag (locale)
*
* @param locale
* the language tag
* @return the canonicalized language tag
*/
public static String CanonicalizeLanguageTag(LanguageTag locale) {
return locale.canonicalize();
}
@SuppressWarnings("serial")
private static final class ValidLanguageTags extends LinkedHashMap<String, String> {
private static final int MAX_SIZE = 12;
ValidLanguageTags() {
super(16, 0.75f, true);
}
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
return size() > MAX_SIZE;
}
}
private static final Map<String, String> validLanguageTags = new ValidLanguageTags();
static {
validLanguageTags.put("en", "en");
validLanguageTags.put("en-GB", "en-GB");
validLanguageTags.put("en-US", "en-US");
}
private static final String DEFAULT_LOCALE = "en";
private static String sanitizeLanguageTag(String languageTag) {
LanguageTag tag = IsStructurallyValidLanguageTag(languageTag);
if (tag == null) {
return DEFAULT_LOCALE;
}
String locale = RemoveUnicodeLocaleExtension(tag.canonicalize());
locale = BestLocale(GetAvailableLocales(LanguageData.getAvailableCollatorLocales()), locale);
if (locale == null) {
return DEFAULT_LOCALE;
}
locale = BestLocale(GetAvailableLocales(LanguageData.getAvailableDateFormatLocales()), locale);
if (locale == null) {
return DEFAULT_LOCALE;
}
locale = BestLocale(GetAvailableLocales(LanguageData.getAvailableNumberFormatLocales()), locale);
if (locale == null) {
return DEFAULT_LOCALE;
}
return locale;
}
private static String BestLocale(Set<String> availableLocales, String locale) {
if (BEST_FIT_SUPPORTED) {
return BestFitAvailableLocale(availableLocales, locale);
}
return BestAvailableLocale(availableLocales, locale);
}
/**
* 6.2.4 DefaultLocale ()
*
* @param realm
* the realm instance
* @return the default locale
*/
public static String DefaultLocale(Realm realm) {
String languageTag = realm.getLocale().toLanguageTag();
synchronized (validLanguageTags) {
String valid = validLanguageTags.get(languageTag);
if (valid == null) {
validLanguageTags.put(languageTag, valid = sanitizeLanguageTag(languageTag));
}
return valid;
}
}
/**
* 6.3.1 IsWellFormedCurrencyCode (currency)
*
* @param currency
* the currency string
* @return {@code true} if the currency string is well formed
*/
public static boolean IsWellFormedCurrencyCode(String currency) {
/* step 1 (case normalization omitted) */
String normalized = currency;
/* step 2 */
if (normalized.length() != 3) {
return false;
}
/* steps 3-4 */
return isAlpha(normalized.charAt(0)) && isAlpha(normalized.charAt(1)) && isAlpha(normalized.charAt(2));
}
private static boolean isAlpha(char c) {
return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z');
}
private static final Set<String> JDK_TIMEZONE_NAMES = set("ACT", "AET", "AGT", "ART", "AST", "BET", "BST", "CAT",
"CNT", "CST", "CTT", "EAT", "ECT", "IET", "IST", "JST", "MIT", "NET", "NST", "PLT", "PNT", "PRT", "PST",
"SST", "VST");
private static final Lazy<HashMap<String, String>> timezones = Lazy.syncOf(() -> {
HashMap<String, String> map = new HashMap<>();
for (String id : TimeZone.getAvailableIDs(SystemTimeZoneType.ANY, null, null)) {
if (JDK_TIMEZONE_NAMES.contains(id)) {
// ignore non-IANA, JDK-specific timezones
continue;
}
map.put(ToUpperCase(id), id);
}
return map;
});
/**
* 6.4.1 IsValidTimeZoneName (timeZone)
*
* @param timeZone
* the time zone name
* @return {@code true} if the time zone name is valid
*/
public static boolean IsValidTimeZoneName(String timeZone) {
return timezones.get().containsKey(ToUpperCase(timeZone));
}
/**
* 6.4.2 CanonicalizeTimeZoneName (timeZone)
*
* @param timeZone
* the time zone name
* @return the canonicalized time zone name
*/
public static String CanonicalizeTimeZoneName(String timeZone) {
/* step 1 */
String ianaTimeZone = timezones.get().get(ToUpperCase(timeZone));
/* step 2 */
ianaTimeZone = TimeZone.getCanonicalID(ianaTimeZone);
assert ianaTimeZone != null : "invalid timezone: " + timeZone;
/* step 3 */
if ("Etc/UTC".equals(ianaTimeZone) || "Etc/GMT".equals(ianaTimeZone)) {
return "UTC";
}
/* step 4 */
return ianaTimeZone;
}
/**
* 6.4.3 DefaultTimeZone ()
*
* @param realm
* the realm instance
* @return the default time zone
*/
public static String DefaultTimeZone(Realm realm) {
return realm.getTimeZone().getID();
}
private static final HashMap<String, String[]> oldStyleLanguageTags;
static {
// generated from CLDR-2.0.0
HashMap<String, String[]> map = new HashMap<>();
map.put("az-Latn-AZ", new String[] { "az-AZ" });
map.put("ha-Latn-GH", new String[] { "ha-GH" });
map.put("ha-Latn-NE", new String[] { "ha-NE" });
map.put("ha-Latn-NG", new String[] { "ha-NG" });
map.put("kk-Cyrl-KZ", new String[] { "kk-KZ" });
map.put("ku-Arab-IQ", new String[] { "ku-IQ" });
map.put("ku-Arab-IR", new String[] { "ku-IR" });
map.put("ku-Latn-SY", new String[] { "ku-SY" });
map.put("ku-Latn-TR", new String[] { "ku-TR" });
map.put("mn-Mong-CN", new String[] { "mn-CN" });
map.put("mn-Cyrl-MN", new String[] { "mn-MN" });
map.put("pa-Guru-IN", new String[] { "pa-IN" });
map.put("pa-Arab-PK", new String[] { "pa-PK" });
map.put("shi-Latn-MA", new String[] { "shi-MA" });
map.put("sr-Latn-BA", new String[] { "sh-BA" });
map.put("sr-Latn-RS", new String[] { "sh-CS", "sh-YU" });
map.put("sr-Cyrl-BA", new String[] { "sr-BA" });
map.put("sr-Cyrl-RS", new String[] { "sr-CS", "sr-RS", "sr-YU" });
map.put("sr-Latn-ME", new String[] { "sr-ME" });
map.put("tg-Cyrl-TJ", new String[] { "tg-TJ" });
map.put("fil-PH", new String[] { "tl-PH" });
map.put("tzm-Latn-MA", new String[] { "tzm-MA" });
map.put("uz-Arab-AF", new String[] { "uz-AF" });
map.put("uz-Cyrl-UZ", new String[] { "uz-UZ" });
map.put("vai-Vaii-LR", new String[] { "vai-LR" });
map.put("zh-Hans-CN", new String[] { "zh-CN" });
map.put("zh-Hant-HK", new String[] { "zh-HK" });
map.put("zh-Hant-MO", new String[] { "zh-MO" });
map.put("zh-Hans-SG", new String[] { "zh-SG" });
map.put("zh-Hant-TW", new String[] { "zh-TW" });
oldStyleLanguageTags = map;
}
/**
* 9.1 Internal Properties of Service Constructors
*
* @param locales
* the supported locales
* @return the set of available locales
*/
public static Set<String> GetAvailableLocales(Collection<String> locales) {
HashMap<String, String[]> oldTags = oldStyleLanguageTags;
HashSet<String> available = new LRUHashSet(locales);
for (Map.Entry<String, String[]> oldTag : oldTags.entrySet()) {
if (available.contains(oldTag.getKey())) {
available.addAll(Arrays.asList(oldTag.getValue()));
}
}
return available;
}
@SuppressWarnings("serial")
private static final class LRUHashSet extends HashSet<String> {
private transient String lastKey;
private transient BestFitMatch lastValue;
LRUHashSet(Collection<String> c) {
super(c);
}
static LRUHashSet from(Set<String> set) {
return set instanceof LRUHashSet ? (LRUHashSet) set : null;
}
static BestFitMatch get(LRUHashSet set, String key) {
if (set != null && key.equals(set.lastKey)) {
return set.lastValue;
}
return null;
}
static void set(LRUHashSet set, String key, BestFitMatch value) {
if (set != null) {
set.lastKey = key;
set.lastValue = value;
}
}
}
public enum ExtensionKey {
/** calendar */
ca,
/** collation */
co,
/** colCaseFirst */
kf,
/** colNumeric */
kn,
/** numbers */
nu;
static ExtensionKey forName(String name, int index) {
assert index + 2 <= name.length();
char c0 = name.charAt(index), c1 = name.charAt(index + 1);
if (c0 == 'c') {
return c1 == 'a' ? ca : c1 == 'o' ? co : null;
}
if (c0 == 'k') {
return c1 == 'f' ? kf : c1 == 'n' ? kn : null;
}
return c0 == 'n' && c1 == 'u' ? nu : null;
}
}
/**
* 9.1 Internal Properties of Service Constructors
*/
public interface LocaleData {
LocaleDataInfo info(ULocale locale);
}
/**
* 9.1 Internal Properties of Service Constructors
*/
public interface LocaleDataInfo {
/**
* Returns {@link #entries(IntlAbstractOperations.ExtensionKey)}.get(0).
*
* @param extensionKey
* the extension key
* @return the extension key default value
*/
String defaultValue(ExtensionKey extensionKey);
/**
* Returns [[sortLocaleData]], [[searchLocaleData]] or [[localeData]].
*
* @param extensionKey
* the extension key
* @return the list of extension key entries
*/
List<String> entries(ExtensionKey extensionKey);
}
/**
* 9.2.1 CanonicalizeLocaleList (locales)
*
* @param cx
* the execution context
* @param locales
* the locales array
* @return the set of canonicalized locales
*/
public static Set<String> CanonicalizeLocaleList(ExecutionContext cx, Object locales) {
/* step 1 */
if (Type.isUndefined(locales)) {
return emptySet();
}
/* steps 2-8 (string only) */
if (Type.isString(locales)) {
// handle the string-only case directly
String tag = ToFlatString(cx, locales);
LanguageTag langTag = IsStructurallyValidLanguageTag(tag);
if (langTag == null) {
throw newRangeError(cx, Messages.Key.IntlStructurallyInvalidLanguageTag, tag);
}
tag = CanonicalizeLanguageTag(langTag);
return singleton(tag);
}
/* step 2 */
LinkedHashSet<String> seen = new LinkedHashSet<>();
/* step 3 (not applicable) */
/* step 4 */
ScriptObject o = ToObject(cx, locales);
/* step 5 */
long len = ToLength(cx, Get(cx, o, "length"));
/* steps 6-7 */
for (long k = 0; k < len; ++k) {
/* step 7.a */
long pk = k;
/* step 7.b */
boolean kPresent = HasProperty(cx, o, pk);
/* step 7.c */
if (kPresent) {
/* step 7.c.i */
Object kValue = Get(cx, o, pk);
/* step 7.c.ii */
if (!(Type.isString(kValue) || Type.isObject(kValue))) {
throw newTypeError(cx, Messages.Key.IntlInvalidLanguageTagType, Type.of(kValue).toString());
}
/* step 7.c.iii */
String tag = ToFlatString(cx, kValue);
/* step 7.c.iv */
LanguageTag langTag = IsStructurallyValidLanguageTag(tag);
if (langTag == null) {
throw newRangeError(cx, Messages.Key.IntlStructurallyInvalidLanguageTag, tag);
}
/* step 7.c.v */
tag = CanonicalizeLanguageTag(langTag);
/* step 7.c.vi */
seen.add(tag);
}
}
/* step 8 */
return seen;
}
/**
* 9.2.2 BestAvailableLocale (availableLocales, locale)
*
* @param availableLocales
* the set of available locales
* @param locale
* the requested locale
* @return the best available locale
*/
public static String BestAvailableLocale(Set<String> availableLocales, String locale) {
/* step 1 */
String candidate = locale;
/* step 2 */
while (true) {
/* step 2.a */
if (availableLocales.contains(candidate)) {
return candidate;
}
/* step 2.b */
int pos = candidate.lastIndexOf('-');
if (pos == -1) {
return null;
}
/* step 2.c */
if (pos >= 2 && candidate.charAt(pos - 2) == '-') {
pos -= 2;
}
/* step 2.d */
candidate = candidate.substring(0, pos);
}
}
/**
* The result record type returned by BestFitMatcher and LookupMatcher.
*/
public static final class LocaleMatch {
/** [[locale]] */
private final String locale;
/** [[extension]] */
private final String extension;
/** [[extensionIndex]] */
private final int extensionIndex;
LocaleMatch(String locale) {
this.locale = locale;
this.extension = "";
this.extensionIndex = -1;
}
LocaleMatch(String locale, LanguageTagUnicodeExtension unicodeExt) {
this.locale = locale;
this.extension = unicodeExt.getUnicodeExtension();
this.extensionIndex = unicodeExt.getSubtagStartIndex();
}
/**
* [[locale]]
*
* @return the locale language tag
*/
public String getLocale() {
return locale;
}
/**
* [[extension]]
*
* @return the language extension tag
*/
public String getExtension() {
return extension;
}
/**
* [[extensionIndex]]
*
* @return the start index of the language extension tag
*/
public int getExtensionIndex() {
return extensionIndex;
}
}
/**
* 9.2.3 LookupMatcher (availableLocales, requestedLocales)
*
* @param realm
* the realm instance
* @param availableLocales
* the set of available locales
* @param requestedLocales
* the set of requested locales
* @return the lookup matcher
*/
public static LocaleMatch LookupMatcher(Realm realm, Lazy<Set<String>> availableLocales,
Set<String> requestedLocales) {
/* steps 1-5 */
for (String locale : requestedLocales) {
/* step 5.a (not applicable) */
/* step 5.b */
LanguageTagUnicodeExtension unicodeExt = UnicodeLocaleExtSequence(locale);
String noExtensionsLocale = unicodeExt.getLanguageTag();
/* step 5.c */
String availableLocale = BestAvailableLocale(availableLocales.get(), noExtensionsLocale);
if (availableLocale != null) {
/* steps 6-7, 9 */
return new LocaleMatch(availableLocale, unicodeExt);
}
/* step 5.d (not applicable) */
}
/* steps 6, 8-9 */
return new LocaleMatch(DefaultLocale(realm));
}
/**
* 9.2.4 BestFitMatcher (availableLocales, requestedLocales)
*
* @param realm
* the realm instance
* @param availableLocales
* the set of available locales
* @param requestedLocales
* the set of requested locales
* @return the best-fit matcher
*/
public static LocaleMatch BestFitMatcher(Realm realm, Lazy<Set<String>> availableLocales,
Set<String> requestedLocales) {
if (!BEST_FIT_SUPPORTED) {
return LookupMatcher(realm, availableLocales, requestedLocales);
}
// fast path when no specific locale was requested
if (requestedLocales.isEmpty()) {
return new LocaleMatch(DefaultLocale(realm));
}
// fast path
Set<String> available = availableLocales.get();
for (String locale : requestedLocales) {
LanguageTagUnicodeExtension unicodeExt = UnicodeLocaleExtSequence(locale);
String noExtensionsLocale = unicodeExt.getLanguageTag();
if (available.contains(noExtensionsLocale)) {
return new LocaleMatch(noExtensionsLocale, unicodeExt);
}
}
// search for best fit match
LocaleMatcher matcher = CreateDefaultMatcher();
BestFitMatch bestMatch = null;
String bestMatchCandidate = null;
for (String locale : requestedLocales) {
String noExtLocale = RemoveUnicodeLocaleExtension(locale);
BestFitMatch match = BestFitAvailableLocale(matcher, available, noExtLocale);
double score = match.score;
if (score >= BEST_FIT_MIN_MATCH && (bestMatch == null || score > bestMatch.score)) {
// System.out.printf("%s -> %s [%f]%n", locale, match.locale, match.score);
bestMatch = match;
bestMatchCandidate = locale;
if (score == 1.0) {
break;
}
}
}
// If no best fit match was found, fall back to lookup matcher algorithm.
if (bestMatch == null) {
return LookupMatcher(realm, availableLocales, requestedLocales);
}
return new LocaleMatch(bestMatch.locale, UnicodeLocaleExtSequence(bestMatchCandidate));
}
/**
* Minimum match value for best fit matcher, currently set to 0.5 to match ICU4J's defaults.
*/
private static final double BEST_FIT_MIN_MATCH = 0.5;
private static final boolean BEST_FIT_SUPPORTED = true;
private static LocaleMatcher CreateDefaultMatcher() {
LocalePriorityList priorityList = LocalePriorityList.add(ULocale.ROOT).build();
return new LocaleMatcher(priorityList);
}
/**
* Hard cache to reduce time required to finish intl-tests. The cache size is limited by the total number of
* available locales.
*/
private static final ConcurrentHashMap<String, LocaleEntry> maximizedLocales = new ConcurrentHashMap<>();
private static LocaleEntry GetMaximizedLocale(LocaleMatcher matcher, String locale) {
LocaleEntry entry = maximizedLocales.get(locale);
if (entry == null) {
entry = createLocaleEntry(matcher, locale);
maximizedLocales.put(locale, entry);
}
return entry;
}
private static final class LocaleEntry {
private final ULocale canonicalized;
private final ULocale maximized;
LocaleEntry(ULocale canonicalized, ULocale maximized) {
this.canonicalized = canonicalized;
this.maximized = maximized;
}
ULocale getCanonicalized() {
return canonicalized;
}
ULocale getMaximized() {
return maximized;
}
}
private static LocaleEntry createLocaleEntry(LocaleMatcher matcher, String locale) {
ULocale canonicalized = matcher.canonicalize(ULocale.forLanguageTag(locale));
ULocale maximized = addLikelySubtagsWithDefaults(canonicalized);
return new LocaleEntry(canonicalized, maximized);
}
private static final class BestFitMatch {
final String locale;
final double score;
BestFitMatch(String locale, double score) {
this.locale = locale;
this.score = score;
}
}
private static BestFitMatch BestFitAvailableLocale(LocaleMatcher matcher, Set<String> availableLocales,
String requestedLocale) {
// Return early if requested locale is available as-is.
if (availableLocales.contains(requestedLocale)) {
return new BestFitMatch(requestedLocale, 1.0);
}
// Check cache next.
LRUHashSet lruAvailableLocales = LRUHashSet.from(availableLocales);
BestFitMatch lastResolved = LRUHashSet.get(lruAvailableLocales, requestedLocale);
if (lastResolved != null) {
return lastResolved;
}
// Perform two-passes to compute best-fit available locale:
// 1) Compare maximized locales.
// 2) If no match was found, compare all available locales.
LocaleEntry requested = createLocaleEntry(matcher, requestedLocale);
String bestMatchLocale = null;
LocaleEntry bestMatchEntry = null;
double bestMatch = Double.NEGATIVE_INFINITY;
for (String available : availableLocales) {
LocaleEntry entry = GetMaximizedLocale(matcher, available);
if (requested.maximized.equals(entry.maximized)) {
double match = matcher.match(requested.getCanonicalized(), requested.getMaximized(),
entry.getCanonicalized(), entry.getMaximized());
if (match > bestMatch || (match == bestMatch && isBetterMatch(requested, bestMatchEntry, entry))) {
bestMatchLocale = available;
bestMatchEntry = entry;
bestMatch = match;
}
}
}
if (bestMatchLocale == null) {
for (String available : availableLocales) {
LocaleEntry entry = GetMaximizedLocale(matcher, available);
double match = matcher.match(requested.getCanonicalized(), requested.getMaximized(),
entry.getCanonicalized(), entry.getMaximized());
// if (match > 0.10) {
// System.out.printf("[%s; %s, %s] -> [%s; %s, %s] => %f%n", requestedLocale,
// requested.getCanonicalized(), requested.getMaximized(), available,
// entry.getCanonicalized(), entry.getMaximized(), match);
// }
if (match > bestMatch || (match == bestMatch && isBetterMatch(requested, bestMatchEntry, entry))) {
bestMatchLocale = available;
bestMatchEntry = entry;
bestMatch = match;
}
}
}
// Create result object and store in cache.
BestFitMatch result = new BestFitMatch(bestMatchLocale, bestMatch);
LRUHashSet.set(lruAvailableLocales, requestedLocale, result);
return result;
}
private static String BestFitAvailableLocale(Set<String> availableLocales, String requestedLocale) {
if (availableLocales.contains(requestedLocale)) {
return requestedLocale;
}
LocaleMatcher matcher = CreateDefaultMatcher();
BestFitMatch availableLocaleMatch = BestFitAvailableLocale(matcher, availableLocales, requestedLocale);
if (availableLocaleMatch.score >= BEST_FIT_MIN_MATCH) {
return availableLocaleMatch.locale;
} else {
// If no best fit match was found, fall back to lookup matcher algorithm
String availableLocale = BestAvailableLocale(availableLocales, requestedLocale);
if (availableLocale != null) {
return availableLocale;
}
}
return null;
}
/**
* Requests for "en-US" give two results with a perfect score:
* <ul>
* <li>[en; en, en_Latn_US]
* <li>[en-US; en_US, en_Latn_US]
* </ul>
* Prefer the more detailed result value, i.e. "en-US".
*
* @param requested
* the requested locale
* @param oldMatch
* the previous match
* @param newMatch
* the new match
* @return {@code true} if the new match is better than the previous one
*/
private static boolean isBetterMatch(LocaleEntry requested, LocaleEntry oldMatch, LocaleEntry newMatch) {
ULocale canonicalized = requested.getCanonicalized();
ULocale oldCanonicalized = oldMatch.getCanonicalized();
ULocale newCanonicalized = newMatch.getCanonicalized();
// Compare language.
String language = canonicalized.getLanguage();
String newLanguage = newCanonicalized.getLanguage();
String oldLanguage = oldCanonicalized.getLanguage();
assert !newLanguage.isEmpty() && !oldLanguage.isEmpty();
if (newLanguage.equals(language) && !oldLanguage.equals(language)) {
return true;
}
// Compare script.
String script = canonicalized.getScript();
String newScript = newCanonicalized.getScript();
String oldScript = oldCanonicalized.getScript();
if ((newScript.equals(script) && !oldScript.equals(script))
|| (newScript.isEmpty() && !oldScript.isEmpty() && !oldScript.equals(script))) {
return true;
}
// Compare region.
String region = canonicalized.getCountry();
String newRegion = newCanonicalized.getCountry();
String oldRegion = oldCanonicalized.getCountry();
if ((newRegion.equals(script) && !oldRegion.equals(region))
|| (newRegion.isEmpty() && !oldRegion.isEmpty() && !oldRegion.equals(region))) {
return true;
}
return false;
}
private static ULocale addLikelySubtagsWithDefaults(ULocale locale) {
ULocale maximized = ULocale.addLikelySubtags(locale);
if (maximized == locale) {
// If already in maximal form or no data available for maximization, make sure
// language, script and region are not undefined (ICU4J expects all are defined).
String language = locale.getLanguage();
String script = locale.getScript();
String region = locale.getCountry();
if (language.isEmpty() || script.isEmpty() || region.isEmpty()) {
return new ULocale(toLocaleId(language, script, region));
}
}
return maximized;
}
private static String toLocaleId(String language, String script, String region) {
StringBuilder sb = new StringBuilder();
sb.append(!language.isEmpty() ? language : "und");
sb.append('_');
sb.append(!script.isEmpty() ? script : "Zzzz");
sb.append('_');
sb.append(!region.isEmpty() ? region : "ZZ");
return sb.toString();
}
public static final class OptionsRecord {
public enum MatcherType {
Lookup, BestFit;
public static MatcherType forName(String name) {
switch (name) {
case "lookup":
return Lookup;
case "best fit":
return BestFit;
default:
throw new IllegalArgumentException(name);
}
}
}
/** [[localeMatcher]] */
private final MatcherType localeMatcher;
private final EnumMap<ExtensionKey, String> values = new EnumMap<>(ExtensionKey.class);
public OptionsRecord(MatcherType localeMatcher) {
this.localeMatcher = localeMatcher;
}
/**
* [[localeMatcher]]
*
* @return the matcher type
*/
public MatcherType getLocaleMatcher() {
return localeMatcher;
}
public int size() {
return values.size();
}
public boolean contains(ExtensionKey key) {
return values.containsKey(key);
}
public String get(ExtensionKey key) {
return values.get(key);
}
public void set(ExtensionKey key, String value) {
values.put(key, value);
}
}
public static final class ResolvedLocale {
private final String dataLocale;
private final String locale;
private final EnumMap<ExtensionKey, String> values;
ResolvedLocale(String dataLocale, String locale, EnumMap<ExtensionKey, String> values) {
this.dataLocale = dataLocale;
this.locale = locale;
this.values = values;
}
public String getDataLocale() {
return dataLocale;
}
public String getLocale() {
return locale;
}
public String getValue(ExtensionKey key) {
return values.get(key);
}
}
/**
* 9.2.5 UnicodeExtensionSubtags (extension)
*
* @param extension
* the Unicode locale extension sequence
* @return the map <code>« extension key → extension value »</code>
*/
public static EnumMap<ExtensionKey, String> UnicodeExtensionSubtags(String extension) {
/*
* http://unicode.org/reports/tr35/#Unicode_locale_identifier
*
* unicode_locale_extensions = sep "u" ((sep keyword)+ | (sep attribute)+ (sep keyword)*) ;
* keyword = key (sep type)? ;
* key = alphanum{2} ;
* type = alphanum{3,8} (sep alphanum{3,8})* ;
* attribute = alphanum{3,8} ;
*/
final int KEY_LENGTH = 2;
assert extension.startsWith("-u-") && extension.length() >= 3 + KEY_LENGTH : extension;
final int length = extension.length();
int startKeyword = 3;
// Skip optional leading attributes.
while (startKeyword < length) {
int index = extension.indexOf('-', startKeyword);
int endIndex = index != -1 ? index : extension.length();
if (endIndex - startKeyword > KEY_LENGTH) {
startKeyword = endIndex + 1;
} else {
break;
}
}
// Read keywords.
EnumMap<ExtensionKey, String> map = new EnumMap<>(ExtensionKey.class);
while (startKeyword < length) {
assert startKeyword + KEY_LENGTH <= length;
assert startKeyword + KEY_LENGTH == length || extension.charAt(startKeyword + KEY_LENGTH) == '-';
ExtensionKey key = ExtensionKey.forName(extension, startKeyword);
int startType = startKeyword + KEY_LENGTH + 1, nextKeyword = startType;
while (nextKeyword < length) {
int index = extension.indexOf('-', nextKeyword);
int endIndex = index != -1 ? index : extension.length();
if (endIndex - nextKeyword > KEY_LENGTH) {
nextKeyword = endIndex + 1;
} else {
break;
}
}
// Supported extension key and not a duplicate keyword.
if (key != null && !map.containsKey(key)) {
if (startType < nextKeyword) {
map.put(key, extension.substring(startType, nextKeyword - 1));
} else {
map.put(key, "");
}
}
startKeyword = nextKeyword;
}
return map;
}
/**
* 9.2.6 ResolveLocale (availableLocales, requestedLocales, options, relevantExtensionKeys, localeData)
*
* @param realm
* the realm instance
* @param availableLocales
* the set of available locales
* @param requestedLocales
* the set of requested locales
* @param options
* the options record
* @param relevantExtensionKeys
* the list of relevant extension keys
* @param localeData
* the locale data
* @return the resolved locale
*/
public static ResolvedLocale ResolveLocale(Realm realm, Lazy<Set<String>> availableLocales,
Set<String> requestedLocales, OptionsRecord options, List<ExtensionKey> relevantExtensionKeys,
LocaleData localeData) {
/* steps 1-4 */
OptionsRecord.MatcherType matcher = options.getLocaleMatcher();
LocaleMatch r;
if (matcher == OptionsRecord.MatcherType.Lookup) {
r = LookupMatcher(realm, availableLocales, requestedLocales);
} else {
r = BestFitMatcher(realm, availableLocales, requestedLocales);
}
// fast path for steps 5-16
String extension = r.getExtension();
if (extension.isEmpty() && options.size() == 0) {
return ResolveLocaleNoExtension(r, relevantExtensionKeys, localeData);
}
/* step 5 */
String foundLocale = r.getLocale();
LocaleDataInfo foundLocaleData = localeData.info(ULocale.forLanguageTag(foundLocale));
/* step 6 */
EnumMap<ExtensionKey, String> extensionSubtags = null;
if (!extension.isEmpty()) {
extensionSubtags = UnicodeExtensionSubtags(extension);
}
/* steps 7-8 (not applicable) */
/* step 9 */
StringBuilder supportedExtension = new StringBuilder("-u");
/* steps 10-13 */
EnumMap<ExtensionKey, String> values = new EnumMap<>(ExtensionKey.class);
for (ExtensionKey key : relevantExtensionKeys) {
/* steps 13.a-b (not applicable) */
/* step 13.c */
List<String> keyLocaleData = foundLocaleData.entries(key);
/* step 13.d */
String value = keyLocaleData.get(0);
/* step 13.e */
String supportedExtensionAddition = "";
/* steps 13.f, 13.f.i-ii */
if (extensionSubtags != null && extensionSubtags.containsKey(key)) {
/* steps 13.f.ii.1-2 */
if (!extensionSubtags.get(key).isEmpty()) {
/* step 13.f.ii.1 */
String requestedValue = extensionSubtags.get(key);
if (keyLocaleData.contains(requestedValue)) {
value = requestedValue;
supportedExtensionAddition = "-" + key.name() + "-" + value;
}
} else if (keyLocaleData.contains("true")) {
/* step 13.f.ii.2 */
value = "true";
}
}
/* step 13.g */
if (options.contains(key)) {
/* step 13.g.i */
String optionsValue = options.get(key);
/* step 13.g.ii */
if (keyLocaleData.contains(optionsValue) && !optionsValue.equals(value)) {
value = optionsValue;
supportedExtensionAddition = "";
}
}
/* step 13.h */
values.put(key, value);
/* step 13.i */
supportedExtension.append(supportedExtensionAddition);
/* step 13.j (not applicable) */
}
/* step 14 */
if (supportedExtension.length() > 2) {
// FIXME: spec bug - https://bugs.ecmascript.org/show_bug.cgi?id=1456
/* step 14.a */
int privateIndex = foundLocale.indexOf("-x-");
/* steps 14.b-c */
if (privateIndex == -1) {
/* step 14.b */
foundLocale += supportedExtension;
} else {
/* step 14.c */
/* step 14.c.i */
String preExtension = foundLocale.substring(0, privateIndex);
/* step 14.c.ii */
String postExtension = foundLocale.substring(privateIndex);
/* step 14.c.iii */
foundLocale = preExtension + supportedExtension + postExtension;
}
/* step 14.d */
LanguageTag languageTag = IsStructurallyValidLanguageTag(foundLocale);
assert languageTag != null : foundLocale;
/* step 14.e */
foundLocale = CanonicalizeLanguageTag(languageTag);
}
/* steps 15-16 */
return new ResolvedLocale(r.getLocale(), foundLocale, values);
}
/**
* 9.2.6 ResolveLocale (availableLocales, requestedLocales, options, relevantExtensionKeys, localeData)
*
* @param realm
* the realm record
* @param relevantExtensionKeys
* the list of relevant extension keys
* @param localeData
* the locale data
* @return the resolved locale
*/
public static ResolvedLocale ResolveDefaultLocale(Realm realm, List<ExtensionKey> relevantExtensionKeys,
LocaleData localeData) {
/* steps 1-4 */
LocaleMatch r = new LocaleMatch(DefaultLocale(realm));
/* steps 5-17 */
return ResolveLocaleNoExtension(r, relevantExtensionKeys, localeData);
}
private static ResolvedLocale ResolveLocaleNoExtension(LocaleMatch r, List<ExtensionKey> relevantExtensionKeys,
LocaleData localeData) {
/* steps 1-4 (not applicable) */
/* step 5 */
LocaleDataInfo foundLocaleData = localeData.info(ULocale.forLanguageTag(r.getLocale()));
/* steps 6-8 (not applicable) */
assert r.getExtension().isEmpty();
/* steps 9-13 */
EnumMap<ExtensionKey, String> values = new EnumMap<>(ExtensionKey.class);
for (ExtensionKey key : relevantExtensionKeys) {
values.put(key, foundLocaleData.defaultValue(key));
}
/* step 14 (not applicable) */
/* steps 15-16 */
return new ResolvedLocale(r.getLocale(), r.getLocale(), values);
}
/**
* 9.2.7 LookupSupportedLocales (availableLocales, requestedLocales)
*
* @param availableLocales
* the set of available locales
* @param requestedLocales
* the set of requested locales
* @return the list of supported locales
*/
public static List<String> LookupSupportedLocales(Set<String> availableLocales, Set<String> requestedLocales) {
int numberOfRequestedLocales = requestedLocales.size();
// Fast path for none requested locale.
if (numberOfRequestedLocales == 0) {
return emptyList();
}
// Fast path for one requested locale.
if (numberOfRequestedLocales == 1) {
String locale = requestedLocales.iterator().next();
String noExtensionsLocale = RemoveUnicodeLocaleExtension(locale);
String availableLocale = BestAvailableLocale(availableLocales, noExtensionsLocale);
if (availableLocale != null) {
return singletonList(locale);
}
return emptyList();
}
/* steps 1-5 */
ArrayList<String> subset = new ArrayList<>();
for (String locale : requestedLocales) {
/* step 5.a (not applicable) */
/* step 5.b */
String noExtensionsLocale = RemoveUnicodeLocaleExtension(locale);
/* step 5.c */
String availableLocale = BestAvailableLocale(availableLocales, noExtensionsLocale);
/* step 5.d */
if (availableLocale != null) {
subset.add(locale);
}
/* step 5.e (not applicable) */
}
/* step 6 */
return subset;
}
/**
* 9.2.8 BestFitSupportedLocales (availableLocales, requestedLocales)
*
* @param availableLocales
* the set of available locales
* @param requestedLocales
* the set of requested locales
* @return the list of best-fit supported locales
*/
public static List<String> BestFitSupportedLocales(Set<String> availableLocales, Set<String> requestedLocales) {
if (!BEST_FIT_SUPPORTED) {
return LookupSupportedLocales(availableLocales, requestedLocales);
}
int numberOfRequestedLocales = requestedLocales.size();
if (numberOfRequestedLocales == 0) {
return emptyList();
}
if (numberOfRequestedLocales == 1) {
String locale = requestedLocales.iterator().next();
String noExtensionsLocale = RemoveUnicodeLocaleExtension(locale);
if (BestFitAvailableLocale(availableLocales, noExtensionsLocale) != null) {
return singletonList(locale);
}
return emptyList();
}
LocaleMatcher matcher = CreateDefaultMatcher();
ArrayList<String> subset = new ArrayList<>();
for (String locale : requestedLocales) {
String noExtensionsLocale = RemoveUnicodeLocaleExtension(locale);
BestFitMatch availableLocaleMatch = BestFitAvailableLocale(matcher, availableLocales, noExtensionsLocale);
if (availableLocaleMatch.score >= BEST_FIT_MIN_MATCH) {
subset.add(locale);
} else {
// If no best fit match was found, fall back to lookup matcher algorithm
String availableLocale = BestAvailableLocale(availableLocales, noExtensionsLocale);
if (availableLocale != null) {
subset.add(locale);
}
}
}
return subset;
}
/**
* 9.2.9 SupportedLocales (availableLocales, requestedLocales, options)
*
* @param cx
* the execution context
* @param availableLocales
* the set of available locales
* @param requestedLocales
* the set of requested locales
* @param options
* the options object
* @return the supported locales array
*/
public static ScriptObject SupportedLocales(ExecutionContext cx, Set<String> availableLocales,
Set<String> requestedLocales, Object options) {
/* step 1 */
String matcher = null;
if (!Type.isUndefined(options)) {
matcher = GetStringOption(cx, ToObject(cx, options), "localeMatcher", set("lookup", "best fit"),
"best fit");
}
/* steps 2-5 */
List<String> supportedLocales;
if (matcher == null || "best fit".equals(matcher)) {
supportedLocales = BestFitSupportedLocales(availableLocales, requestedLocales);
} else {
supportedLocales = LookupSupportedLocales(availableLocales, requestedLocales);
}
/* steps 6-8 */
ArrayObject subset = ArrayCreate(cx, supportedLocales.size());
int index = 0;
for (Object value : supportedLocales) {
subset.defineOwnProperty(cx, index++, new PropertyDescriptor(value, false, true, false));
}
PropertyDescriptor nonConfigurableWritable = new PropertyDescriptor();
nonConfigurableWritable.setConfigurable(false);
nonConfigurableWritable.setWritable(false);
subset.defineOwnProperty(cx, "length", nonConfigurableWritable);
/* step 9 */
return subset;
}
/**
* 9.2.10 GetOption (options, property, type, values, fallback)
*
* @param cx
* the execution context
* @param options
* the options object
* @param property
* the property name
* @param values
* the optional set of allowed property values
* @param fallback
* the fallback value
* @return the string option
*/
public static String GetStringOption(ExecutionContext cx, ScriptObject options, String property, Set<String> values,
String fallback) {
/* step 1 (not applicable) */
/* step 2 */
Object value = Get(cx, options, property);
/* step 3 */
if (!Type.isUndefined(value)) {
/* steps 3.a-b (not applicable) */
/* step 3.c */
String val = ToFlatString(cx, value);
/* step 3.d */
if (values != null && !values.contains(val)) {
throw newRangeError(cx, Messages.Key.IntlInvalidOption, val);
}
/* step 3.e */
return val;
}
/* step 4 */
return fallback;
}
/**
* 9.2.10 GetOption (options, property, type, values, fallback)
*
* @param cx
* the execution context
* @param options
* the options object
* @param property
* the property name
* @param fallback
* the fallback value
* @return the boolean option
*/
public static Boolean GetBooleanOption(ExecutionContext cx, ScriptObject options, String property,
Boolean fallback) {
/* step 1 (not applicable) */
/* step 2 */
Object value = Get(cx, options, property);
/* step 3 */
if (!Type.isUndefined(value)) {
/* steps 3.a, c-d (not applicable) */
/* steps 3.b, e */
return ToBoolean(value);
}
/* step 4 */
return fallback;
}
/**
* 9.2.11 GetNumberOption (options, property, minimum, maximum, fallback)
*
* @param cx
* the execution context
* @param options
* the options object
* @param property
* the property name
* @param minimum
* the minimum value
* @param maximum
* the maximum value
* @param fallback
* the fallback value
* @return the number option
*/
public static int GetNumberOption(ExecutionContext cx, ScriptObject options, String property, int minimum,
int maximum, int fallback) {
assert minimum <= maximum;
assert minimum <= fallback && fallback <= maximum;
/* step 1 (not applicable) */
/* step 2 */
Object value = Get(cx, options, property);
/* step 3 */
if (!Type.isUndefined(value)) {
/* step 3.a */
double val = ToNumber(cx, value);
/* step 3.b */
if (Double.isNaN(val) || val < minimum || val > maximum) {
throw newRangeError(cx, Messages.Key.IntlInvalidOption, Double.toString(val));
}
/* step 3.c */
return (int) Math.floor(val);
}
/* step 4 */
return fallback;
}
@SafeVarargs
private static <T> Set<T> set(T... elements) {
return new HashSet<>(Arrays.asList(elements));
}
}