| /* GENERATED SOURCE. DO NOT MODIFY. */ |
| // © 2016 and later: Unicode, Inc. and others. |
| // License & terms of use: http://www.unicode.org/copyright.html |
| /* |
| **************************************************************************************** |
| * Copyright (C) 2009-2016, Google, Inc.; International Business Machines Corporation |
| * and others. All Rights Reserved. |
| **************************************************************************************** |
| */ |
| package android.icu.util; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| |
| import android.icu.impl.locale.LSR; |
| import android.icu.impl.locale.LikelySubtags; |
| import android.icu.impl.locale.LocaleDistance; |
| |
| /** |
| * Immutable class that picks the best match between a user's desired locales and |
| * an application's supported locales. |
| * |
| * <p>Example: |
| * <pre> |
| * LocaleMatcher matcher = LocaleMatcher.builder().setSupportedLocales("fr, en-GB, en").build(); |
| * Locale bestSupported = matcher.getBestLocale(Locale.US); // "en" |
| * </pre> |
| * |
| * <p>A matcher takes into account when languages are close to one another, |
| * such as Danish and Norwegian, |
| * and when regional variants are close, like en-GB and en-AU as opposed to en-US. |
| * |
| * <p>If there are multiple supported locales with the same (language, script, region) |
| * likely subtags, then the current implementation returns the first of those locales. |
| * It ignores variant subtags (except for pseudolocale variants) and extensions. |
| * This may change in future versions. |
| * |
| * <p>For example, the current implementation does not distinguish between |
| * de, de-DE, de-Latn, de-1901, de-u-co-phonebk. |
| * |
| * <p>If you prefer one equivalent locale over another, then provide only the preferred one, |
| * or place it earlier in the list of supported locales. |
| * |
| * <p>Otherwise, the order of supported locales may have no effect on the best-match results. |
| * The current implementation compares each desired locale with supported locales |
| * in the following order: |
| * 1. Default locale, if supported; |
| * 2. CLDR "paradigm locales" like en-GB and es-419; |
| * 3. other supported locales. |
| * This may change in future versions. |
| * |
| * <p>Often a product will just need one matcher instance, built with the languages |
| * that it supports. However, it may want multiple instances with different |
| * default languages based on additional information, such as the domain. |
| * |
| * <p>This class is not intended for public subclassing. |
| * |
| * @author [email protected] |
| * @hide Only a subset of ICU is exposed in Android |
| */ |
| public final class LocaleMatcher { |
| private static final LSR UND_LSR = new LSR("und","","", LSR.EXPLICIT_LSR); |
| // In ULocale, "und" and "" make the same object. |
| private static final ULocale UND_ULOCALE = new ULocale("und"); |
| // In Locale, "und" and "" make different objects. |
| private static final Locale UND_LOCALE = new Locale("und"); |
| private static final Locale EMPTY_LOCALE = new Locale(""); |
| |
| // Activates debugging output to stderr with details of GetBestMatch. |
| private static final boolean TRACE_MATCHER = false; |
| |
| private static abstract class LsrIterator implements Iterator<LSR> { |
| int bestDesiredIndex = -1; |
| |
| @Override |
| public void remove() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| public abstract void rememberCurrent(int desiredIndex); |
| } |
| |
| /** |
| * Builder option for whether the language subtag or the script subtag is most important. |
| * |
| * @see LocaleMatcher.Builder#setFavorSubtag(LocaleMatcher.FavorSubtag) |
| * @hide Only a subset of ICU is exposed in Android |
| */ |
| public enum FavorSubtag { |
| /** |
| * Language differences are most important, then script differences, then region differences. |
| * (This is the default behavior.) |
| */ |
| LANGUAGE, |
| /** |
| * Makes script differences matter relatively more than language differences. |
| */ |
| SCRIPT |
| } |
| |
| /** |
| * Builder option for whether all desired locales are treated equally or |
| * earlier ones are preferred. |
| * |
| * @see LocaleMatcher.Builder#setDemotionPerDesiredLocale(LocaleMatcher.Demotion) |
| * @hide Only a subset of ICU is exposed in Android |
| */ |
| public enum Demotion { |
| /** |
| * All desired locales are treated equally. |
| */ |
| NONE, |
| /** |
| * Earlier desired locales are preferred. |
| * |
| * <p>From each desired locale to the next, |
| * the distance to any supported locale is increased by an additional amount |
| * which is at least as large as most region mismatches. |
| * A later desired locale has to have a better match with some supported locale |
| * due to more than merely having the same region subtag. |
| * |
| * <p>For example: <code>Supported={en, sv} desired=[en-GB, sv]</code> |
| * yields <code>Result(en-GB, en)</code> because |
| * with the demotion of sv its perfect match is no better than |
| * the region distance between the earlier desired locale en-GB and en=en-US. |
| * |
| * <p>Notes: |
| * <ul> |
| * <li>In some cases, language and/or script differences can be as small as |
| * the typical region difference. (Example: sr-Latn vs. sr-Cyrl) |
| * <li>It is possible for certain region differences to be larger than usual, |
| * and larger than the demotion. |
| * (As of CLDR 35 there is no such case, but |
| * this is possible in future versions of the data.) |
| * </ul> |
| */ |
| REGION |
| } |
| |
| /** |
| * Builder option for whether to include or ignore one-way (fallback) match data. |
| * The LocaleMatcher uses CLDR languageMatch data which includes fallback (oneway=true) entries. |
| * Sometimes it is desirable to ignore those. |
| * |
| * <p>For example, consider a web application with the UI in a given language, |
| * with a link to another, related web app. |
| * The link should include the UI language, and the target server may also use |
| * the client’s Accept-Language header data. |
| * The target server has its own list of supported languages. |
| * One may want to favor UI language consistency, that is, |
| * if there is a decent match for the original UI language, we want to use it, |
| * but not if it is merely a fallback. |
| * |
| * @see LocaleMatcher.Builder#setDirection(LocaleMatcher.Direction) |
| * @hide Only a subset of ICU is exposed in Android |
| */ |
| public enum Direction { |
| /** |
| * Locale matching includes one-way matches such as Breton→French. (default) |
| */ |
| WITH_ONE_WAY, |
| /** |
| * Locale matching limited to two-way matches including e.g. Danish↔Norwegian |
| * but ignoring one-way matches. |
| */ |
| ONLY_TWO_WAY |
| } |
| |
| /** |
| * Data for the best-matching pair of a desired and a supported locale. |
| * |
| * @hide Only a subset of ICU is exposed in Android |
| */ |
| public static final class Result { |
| private final ULocale desiredULocale; |
| private final ULocale supportedULocale; |
| private final Locale desiredLocale; |
| private final Locale supportedLocale; |
| private final int desiredIndex; |
| private final int supportedIndex; |
| |
| private Result(ULocale udesired, ULocale usupported, |
| Locale desired, Locale supported, |
| int desIndex, int suppIndex) { |
| desiredULocale = udesired; |
| supportedULocale = usupported; |
| desiredLocale = desired; |
| supportedLocale = supported; |
| desiredIndex = desIndex; |
| supportedIndex = suppIndex; |
| } |
| |
| /** |
| * Returns the best-matching desired locale. |
| * null if the list of desired locales is empty or if none matched well enough. |
| * |
| * @return the best-matching desired locale, or null. |
| */ |
| public ULocale getDesiredULocale() { |
| return desiredULocale == null && desiredLocale != null ? |
| ULocale.forLocale(desiredLocale) : desiredULocale; |
| } |
| /** |
| * Returns the best-matching desired locale. |
| * null if the list of desired locales is empty or if none matched well enough. |
| * |
| * @return the best-matching desired locale, or null. |
| */ |
| public Locale getDesiredLocale() { |
| return desiredLocale == null && desiredULocale != null ? |
| desiredULocale.toLocale() : desiredLocale; |
| } |
| |
| /** |
| * Returns the best-matching supported locale. |
| * If none matched well enough, this is the default locale. |
| * The default locale is null if {@link Builder#setNoDefaultLocale()} was called, |
| * or if the list of supported locales is empty and no explicit default locale is set. |
| * |
| * @return the best-matching supported locale, or null. |
| */ |
| public ULocale getSupportedULocale() { return supportedULocale; } |
| /** |
| * Returns the best-matching supported locale. |
| * If none matched well enough, this is the default locale. |
| * The default locale is null if {@link Builder#setNoDefaultLocale()} was called, |
| * or if the list of supported locales is empty and no explicit default locale is set. |
| * |
| * @return the best-matching supported locale, or null. |
| */ |
| public Locale getSupportedLocale() { return supportedLocale; } |
| |
| /** |
| * Returns the index of the best-matching desired locale in the input Iterable order. |
| * -1 if the list of desired locales is empty or if none matched well enough. |
| * |
| * @return the index of the best-matching desired locale, or -1. |
| */ |
| public int getDesiredIndex() { return desiredIndex; } |
| |
| /** |
| * Returns the index of the best-matching supported locale in the |
| * constructor’s or builder’s input order (“set” Collection plus “added” locales). |
| * If the matcher was built from a locale list string, then the iteration order is that |
| * of a LocalePriorityList built from the same string. |
| * -1 if the list of supported locales is empty or if none matched well enough. |
| * |
| * @return the index of the best-matching supported locale, or -1. |
| */ |
| public int getSupportedIndex() { return supportedIndex; } |
| |
| /** |
| * Takes the best-matching supported locale and adds relevant fields of the |
| * best-matching desired locale, such as the -t- and -u- extensions. |
| * May replace some fields of the supported locale. |
| * The result is the locale that should be used for date and number formatting, collation, etc. |
| * Returns null if getSupportedLocale() returns null. |
| * |
| * <p>Example: desired=ar-SA-u-nu-latn, supported=ar-EG, resolved locale=ar-SA-u-nu-latn |
| * |
| * @return a locale combining the best-matching desired and supported locales. |
| */ |
| public ULocale makeResolvedULocale() { |
| ULocale bestDesired = getDesiredULocale(); |
| if (supportedULocale == null || bestDesired == null || |
| supportedULocale.equals(bestDesired)) { |
| return supportedULocale; |
| } |
| ULocale.Builder b = new ULocale.Builder().setLocale(supportedULocale); |
| |
| // Copy the region from bestDesired, if there is one. |
| String region = bestDesired.getCountry(); |
| if (!region.isEmpty()) { |
| b.setRegion(region); |
| } |
| |
| // Copy the variants from bestDesired, if there are any. |
| // Note that this will override any supportedULocale variants. |
| // For example, "sco-ulster-fonipa" + "...-fonupa" => "sco-fonupa" (replacing ulster). |
| String variants = bestDesired.getVariant(); |
| if (!variants.isEmpty()) { |
| b.setVariant(variants); |
| } |
| |
| // Copy the extensions from bestDesired, if there are any. |
| // Note that this will override any supportedULocale extensions. |
| // For example, "th-u-nu-latn-ca-buddhist" + "...-u-nu-native" => "th-u-nu-native" |
| // (replacing calendar). |
| for (char extensionKey : bestDesired.getExtensionKeys()) { |
| b.setExtension(extensionKey, bestDesired.getExtension(extensionKey)); |
| } |
| return b.build(); |
| } |
| |
| /** |
| * Takes the best-matching supported locale and adds relevant fields of the |
| * best-matching desired locale, such as the -t- and -u- extensions. |
| * May replace some fields of the supported locale. |
| * The result is the locale that should be used for |
| * date and number formatting, collation, etc. |
| * Returns null if getSupportedLocale() returns null. |
| * |
| * <p>Example: desired=ar-SA-u-nu-latn, supported=ar-EG, resolved locale=ar-SA-u-nu-latn |
| * |
| * @return a locale combining the best-matching desired and supported locales. |
| */ |
| public Locale makeResolvedLocale() { |
| ULocale resolved = makeResolvedULocale(); |
| return resolved != null ? resolved.toLocale() : null; |
| } |
| } |
| |
| private final int thresholdDistance; |
| private final int demotionPerDesiredLocale; |
| private final FavorSubtag favorSubtag; |
| private final Direction direction; |
| |
| // These are in input order. |
| private final ULocale[] supportedULocales; |
| private final Locale[] supportedLocales; |
| // These are in preference order: 1. Default locale 2. paradigm locales 3. others. |
| private final Map<LSR, Integer> supportedLsrToIndex; |
| // Array versions of the supportedLsrToIndex keys and values. |
| // The distance lookup loops over the supportedLSRs and returns the index of the best match. |
| private final LSR[] supportedLSRs; |
| private final int[] supportedIndexes; |
| private final int supportedLSRsLength; |
| private final ULocale defaultULocale; |
| private final Locale defaultLocale; |
| |
| /** |
| * LocaleMatcher Builder. |
| * |
| * @see LocaleMatcher#builder() |
| * @hide Only a subset of ICU is exposed in Android |
| */ |
| public static final class Builder { |
| private List<ULocale> supportedLocales; |
| private int thresholdDistance = -1; |
| private Demotion demotion; |
| private ULocale defaultLocale; |
| private boolean withDefault = true; |
| private FavorSubtag favor; |
| private Direction direction; |
| private ULocale maxDistanceDesired; |
| private ULocale maxDistanceSupported; |
| |
| private Builder() {} |
| |
| /** |
| * Parses the string like {@link LocalePriorityList} does and |
| * sets the supported locales accordingly. |
| * Clears any previously set/added supported locales first. |
| * |
| * @param locales the string of locales to set, to be parsed like LocalePriorityList does |
| * @return this Builder object |
| */ |
| public Builder setSupportedLocales(String locales) { |
| return setSupportedULocales(LocalePriorityList.add(locales).build().getULocales()); |
| } |
| |
| /** |
| * Copies the supported locales, preserving iteration order. |
| * Clears any previously set/added supported locales first. |
| * Duplicates are allowed, and are not removed. |
| * |
| * @param locales the list of locales |
| * @return this Builder object |
| */ |
| public Builder setSupportedULocales(Collection<ULocale> locales) { |
| supportedLocales = new ArrayList<>(locales); |
| return this; |
| } |
| |
| /** |
| * Copies the supported locales, preserving iteration order. |
| * Clears any previously set/added supported locales first. |
| * Duplicates are allowed, and are not removed. |
| * |
| * @param locales the list of locale |
| * @return this Builder object |
| */ |
| public Builder setSupportedLocales(Collection<Locale> locales) { |
| supportedLocales = new ArrayList<>(locales.size()); |
| for (Locale locale : locales) { |
| supportedLocales.add(ULocale.forLocale(locale)); |
| } |
| return this; |
| } |
| |
| /** |
| * Adds another supported locale. |
| * Duplicates are allowed, and are not removed. |
| * |
| * @param locale another locale |
| * @return this Builder object |
| */ |
| public Builder addSupportedULocale(ULocale locale) { |
| if (supportedLocales == null) { |
| supportedLocales = new ArrayList<>(); |
| } |
| supportedLocales.add(locale); |
| return this; |
| } |
| |
| /** |
| * Adds another supported locale. |
| * Duplicates are allowed, and are not removed. |
| * |
| * @param locale another locale |
| * @return this Builder object |
| */ |
| public Builder addSupportedLocale(Locale locale) { |
| return addSupportedULocale(ULocale.forLocale(locale)); |
| } |
| |
| /** |
| * Sets no default locale. |
| * There will be no explicit or implicit default locale. |
| * If there is no good match, then the matcher will return null for the |
| * best supported locale. |
| */ |
| public Builder setNoDefaultLocale() { |
| this.defaultLocale = null; |
| withDefault = false; |
| return this; |
| } |
| |
| /** |
| * Sets the default locale; if null, or if it is not set explicitly, |
| * then the first supported locale is used as the default locale. |
| * There is no default locale at all (null will be returned instead) |
| * if {@link #setNoDefaultLocale()} is called. |
| * |
| * @param defaultLocale the default locale |
| * @return this Builder object |
| */ |
| public Builder setDefaultULocale(ULocale defaultLocale) { |
| this.defaultLocale = defaultLocale; |
| withDefault = true; |
| return this; |
| } |
| |
| /** |
| * Sets the default locale; if null, or if it is not set explicitly, |
| * then the first supported locale is used as the default locale. |
| * There is no default locale at all (null will be returned instead) |
| * if {@link #setNoDefaultLocale()} is called. |
| * |
| * @param defaultLocale the default locale |
| * @return this Builder object |
| */ |
| public Builder setDefaultLocale(Locale defaultLocale) { |
| this.defaultLocale = ULocale.forLocale(defaultLocale); |
| withDefault = true; |
| return this; |
| } |
| |
| /** |
| * If SCRIPT, then the language differences are smaller than script differences. |
| * This is used in situations (such as maps) where |
| * it is better to fall back to the same script than a similar language. |
| * |
| * @param subtag the subtag to favor |
| * @return this Builder object |
| */ |
| public Builder setFavorSubtag(FavorSubtag subtag) { |
| this.favor = subtag; |
| return this; |
| } |
| |
| /** |
| * Option for whether all desired locales are treated equally or |
| * earlier ones are preferred (this is the default). |
| * |
| * @param demotion the demotion per desired locale to set. |
| * @return this Builder object |
| */ |
| public Builder setDemotionPerDesiredLocale(Demotion demotion) { |
| this.demotion = demotion; |
| return this; |
| } |
| |
| /** |
| * Option for whether to include or ignore one-way (fallback) match data. |
| * By default, they are included. |
| * |
| * @param direction the match direction to set. |
| * @return this Builder object |
| */ |
| public Builder setDirection(Direction direction) { |
| this.direction = direction; |
| return this; |
| } |
| |
| /** |
| * Sets the maximum distance for an acceptable match. |
| * The matcher will return a match for a pair of locales only if |
| * they match at least as well as the pair given here. |
| * |
| * <p>For example, setMaxDistance(en-US, en-GB) limits matches to ones where the |
| * (desired, support) locales have a distance no greater than a region subtag difference. |
| * This is much stricter than the CLDR default. |
| * |
| * <p>The details of locale matching are subject to changes in |
| * CLDR data and in the algorithm. |
| * Specifying a maximum distance in relative terms via a sample pair of locales |
| * insulates from changes that affect all distance metrics similarly, |
| * but some changes will necessarily affect relative distances between |
| * different pairs of locales. |
| * |
| * @param desired the desired locale for distance comparison. |
| * @param supported the supported locale for distance comparison. |
| * @return this Builder object |
| */ |
| public Builder setMaxDistance(Locale desired, Locale supported) { |
| if (desired == null || supported == null) { |
| throw new IllegalArgumentException("desired/supported locales must not be null"); |
| } |
| return setMaxDistance(ULocale.forLocale(desired), ULocale.forLocale(supported)); |
| } |
| |
| /** |
| * Sets the maximum distance for an acceptable match. |
| * The matcher will return a match for a pair of locales only if |
| * they match at least as well as the pair given here. |
| * |
| * <p>For example, setMaxDistance(en-US, en-GB) limits matches to ones where the |
| * (desired, support) locales have a distance no greater than a region subtag difference. |
| * This is much stricter than the CLDR default. |
| * |
| * <p>The details of locale matching are subject to changes in |
| * CLDR data and in the algorithm. |
| * Specifying a maximum distance in relative terms via a sample pair of locales |
| * insulates from changes that affect all distance metrics similarly, |
| * but some changes will necessarily affect relative distances between |
| * different pairs of locales. |
| * |
| * @param desired the desired locale for distance comparison. |
| * @param supported the supported locale for distance comparison. |
| * @return this Builder object |
| */ |
| public Builder setMaxDistance(ULocale desired, ULocale supported) { |
| if (desired == null || supported == null) { |
| throw new IllegalArgumentException("desired/supported locales must not be null"); |
| } |
| maxDistanceDesired = desired; |
| maxDistanceSupported = supported; |
| return this; |
| } |
| |
| /** |
| * <i>Internal only!</i> |
| * |
| * @param thresholdDistance the thresholdDistance to set, with -1 = default |
| * @return this Builder object |
| * @deprecated This API is ICU internal only. |
| * @hide draft / provisional / internal are hidden on Android |
| */ |
| @Deprecated |
| public Builder internalSetThresholdDistance(int thresholdDistance) { |
| if (thresholdDistance > 100) { |
| thresholdDistance = 100; |
| } |
| this.thresholdDistance = thresholdDistance; |
| return this; |
| } |
| |
| /** |
| * Builds and returns a new locale matcher. |
| * This builder can continue to be used. |
| * |
| * @return new LocaleMatcher. |
| */ |
| public LocaleMatcher build() { |
| return new LocaleMatcher(this); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String toString() { |
| StringBuilder s = new StringBuilder().append("{LocaleMatcher.Builder"); |
| if (supportedLocales != null && !supportedLocales.isEmpty()) { |
| s.append(" supported={").append(supportedLocales).append('}'); |
| } |
| if (defaultLocale != null) { |
| s.append(" default=").append(defaultLocale); |
| } |
| if (favor != null) { |
| s.append(" distance=").append(favor); |
| } |
| if (thresholdDistance >= 0) { |
| s.append(String.format(" threshold=%d", thresholdDistance)); |
| } |
| if (demotion != null) { |
| s.append(" demotion=").append(demotion); |
| } |
| return s.append('}').toString(); |
| } |
| } |
| |
| /** |
| * Returns a builder used in chaining parameters for building a LocaleMatcher. |
| * |
| * @return a new Builder object |
| */ |
| public static Builder builder() { |
| return new Builder(); |
| } |
| |
| /** |
| * Copies the supported locales, preserving iteration order, and constructs a LocaleMatcher. |
| * The first locale is used as the default locale for when there is no good match. |
| * |
| * @param supportedLocales list of locales |
| */ |
| public LocaleMatcher(LocalePriorityList supportedLocales) { |
| this(builder().setSupportedULocales(supportedLocales.getULocales())); |
| } |
| |
| /** |
| * Parses the string like {@link LocalePriorityList} does and |
| * constructs a LocaleMatcher for the supported locales parsed from the string. |
| * The first one (in LocalePriorityList iteration order) is used as the default locale for |
| * when there is no good match. |
| * |
| * @param supportedLocales the string of locales to set, |
| * to be parsed like LocalePriorityList does |
| */ |
| public LocaleMatcher(String supportedLocales) { |
| this(builder().setSupportedLocales(supportedLocales)); |
| } |
| |
| private LocaleMatcher(Builder builder) { |
| ULocale udef = builder.defaultLocale; |
| Locale def = null; |
| LSR defLSR = null; |
| if (udef != null) { |
| def = udef.toLocale(); |
| defLSR = getMaximalLsrOrUnd(udef); |
| } |
| // Store the supported locales in input order, |
| // so that when different types are used (e.g., java.util.Locale) |
| // we can return those by parallel index. |
| int supportedLocalesLength = builder.supportedLocales != null ? |
| builder.supportedLocales.size() : 0; |
| supportedULocales = new ULocale[supportedLocalesLength]; |
| supportedLocales = new Locale[supportedLocalesLength]; |
| // Supported LRSs in input order. |
| LSR lsrs[] = new LSR[supportedLocalesLength]; |
| int i = 0; |
| if (supportedLocalesLength > 0) { |
| for (ULocale locale : builder.supportedLocales) { |
| supportedULocales[i] = locale; |
| supportedLocales[i] = locale.toLocale(); |
| lsrs[i] = getMaximalLsrOrUnd(locale); |
| ++i; |
| } |
| } |
| |
| // We need an unordered map from LSR to first supported locale with that LSR, |
| // and an ordered list of (LSR, supported index) for |
| // the supported locales in the following order: |
| // 1. Default locale, if it is supported. |
| // 2. Priority locales (aka "paradigm locales") in builder order. |
| // 3. Remaining locales in builder order. |
| supportedLsrToIndex = new HashMap<>(supportedLocalesLength); |
| supportedLSRs = new LSR[supportedLocalesLength]; |
| supportedIndexes = new int[supportedLocalesLength]; |
| int suppLength = 0; |
| // Determine insertion order. |
| // Add locales immediately that are equivalent to the default. |
| byte[] order = new byte[supportedLocalesLength]; |
| int numParadigms = 0; |
| i = 0; |
| for (ULocale locale : supportedULocales) { |
| LSR lsr = lsrs[i]; |
| if (defLSR == null && builder.withDefault) { |
| // Implicit default locale = first supported locale, if not turned off. |
| assert i == 0; |
| udef = locale; |
| def = supportedLocales[0]; |
| defLSR = lsr; |
| suppLength = putIfAbsent(lsr, 0, suppLength); |
| } else if (defLSR != null && lsr.isEquivalentTo(defLSR)) { |
| suppLength = putIfAbsent(lsr, i, suppLength); |
| } else if (LocaleDistance.INSTANCE.isParadigmLSR(lsr)) { |
| order[i] = 2; |
| ++numParadigms; |
| } else { |
| order[i] = 3; |
| } |
| ++i; |
| } |
| // Add supported paradigm locales. |
| int paradigmLimit = suppLength + numParadigms; |
| for (i = 0; i < supportedLocalesLength && suppLength < paradigmLimit; ++i) { |
| if (order[i] == 2) { |
| suppLength = putIfAbsent(lsrs[i], i, suppLength); |
| } |
| } |
| // Add remaining supported locales. |
| for (i = 0; i < supportedLocalesLength; ++i) { |
| if (order[i] == 3) { |
| suppLength = putIfAbsent(lsrs[i], i, suppLength); |
| } |
| } |
| supportedLSRsLength = suppLength; |
| // If supportedLSRsLength < supportedLocalesLength then |
| // we waste as many array slots as there are duplicate supported LSRs, |
| // but the amount of wasted space is small as long as there are few duplicates. |
| |
| defaultULocale = udef; |
| defaultLocale = def; |
| demotionPerDesiredLocale = |
| builder.demotion == Demotion.NONE ? 0 : |
| LocaleDistance.INSTANCE.getDefaultDemotionPerDesiredLocale(); // null or REGION |
| favorSubtag = builder.favor; |
| direction = builder.direction; |
| |
| int threshold; |
| if (builder.thresholdDistance >= 0) { |
| threshold = builder.thresholdDistance; |
| } else if (builder.maxDistanceDesired != null) { |
| int indexAndDistance = LocaleDistance.INSTANCE.getBestIndexAndDistance( |
| getMaximalLsrOrUnd(builder.maxDistanceDesired), |
| new LSR[] { getMaximalLsrOrUnd(builder.maxDistanceSupported) }, 1, |
| LocaleDistance.shiftDistance(100), favorSubtag, direction); |
| // +1 for an exclusive threshold from an inclusive max. |
| threshold = LocaleDistance.getDistanceFloor(indexAndDistance) + 1; |
| } else { |
| threshold = LocaleDistance.INSTANCE.getDefaultScriptDistance(); |
| } |
| thresholdDistance = threshold; |
| |
| if (TRACE_MATCHER) { |
| System.err.printf("new LocaleMatcher: %s\n", toString()); |
| } |
| } |
| |
| private final int putIfAbsent(LSR lsr, int i, int suppLength) { |
| if (!supportedLsrToIndex.containsKey(lsr)) { |
| supportedLsrToIndex.put(lsr, i); |
| supportedLSRs[suppLength] = lsr; |
| supportedIndexes[suppLength++] = i; |
| } |
| return suppLength; |
| } |
| |
| private static final LSR getMaximalLsrOrUnd(ULocale locale) { |
| if (locale.equals(UND_ULOCALE)) { |
| return UND_LSR; |
| } else { |
| return LikelySubtags.INSTANCE.makeMaximizedLsrFrom(locale, false); |
| } |
| } |
| |
| private static final LSR getMaximalLsrOrUnd(Locale locale) { |
| if (locale.equals(UND_LOCALE) || locale.equals(EMPTY_LOCALE)) { |
| return UND_LSR; |
| } else { |
| return LikelySubtags.INSTANCE.makeMaximizedLsrFrom(locale); |
| } |
| } |
| |
| private static final class ULocaleLsrIterator extends LsrIterator { |
| private Iterator<ULocale> locales; |
| private ULocale current, remembered; |
| |
| ULocaleLsrIterator(Iterator<ULocale> locales) { |
| this.locales = locales; |
| } |
| |
| @Override |
| public boolean hasNext() { |
| return locales.hasNext(); |
| } |
| |
| @Override |
| public LSR next() { |
| current = locales.next(); |
| return getMaximalLsrOrUnd(current); |
| } |
| |
| @Override |
| public void rememberCurrent(int desiredIndex) { |
| bestDesiredIndex = desiredIndex; |
| remembered = current; |
| } |
| } |
| |
| private static final class LocaleLsrIterator extends LsrIterator { |
| private Iterator<Locale> locales; |
| private Locale current, remembered; |
| |
| LocaleLsrIterator(Iterator<Locale> locales) { |
| this.locales = locales; |
| } |
| |
| @Override |
| public boolean hasNext() { |
| return locales.hasNext(); |
| } |
| |
| @Override |
| public LSR next() { |
| current = locales.next(); |
| return getMaximalLsrOrUnd(current); |
| } |
| |
| @Override |
| public void rememberCurrent(int desiredIndex) { |
| bestDesiredIndex = desiredIndex; |
| remembered = current; |
| } |
| } |
| |
| /** |
| * Returns the supported locale which best matches the desired locale. |
| * |
| * @param desiredLocale Typically a user's language. |
| * @return the best-matching supported locale. |
| */ |
| public ULocale getBestMatch(ULocale desiredLocale) { |
| LSR desiredLSR = getMaximalLsrOrUnd(desiredLocale); |
| int suppIndex = getBestSuppIndex(desiredLSR, null); |
| return suppIndex >= 0 ? supportedULocales[suppIndex] : defaultULocale; |
| } |
| |
| /** |
| * Returns the supported locale which best matches one of the desired locales. |
| * |
| * @param desiredLocales Typically a user's languages, in order of preference (descending). |
| * (In ICU 4.4..63 this parameter had type LocalePriorityList.) |
| * @return the best-matching supported locale. |
| */ |
| public ULocale getBestMatch(Iterable<ULocale> desiredLocales) { |
| Iterator<ULocale> desiredIter = desiredLocales.iterator(); |
| if (!desiredIter.hasNext()) { |
| return defaultULocale; |
| } |
| ULocaleLsrIterator lsrIter = new ULocaleLsrIterator(desiredIter); |
| LSR desiredLSR = lsrIter.next(); |
| int suppIndex = getBestSuppIndex(desiredLSR, lsrIter); |
| return suppIndex >= 0 ? supportedULocales[suppIndex] : defaultULocale; |
| } |
| |
| /** |
| * Parses the string like {@link LocalePriorityList} does and |
| * returns the supported locale which best matches one of the desired locales. |
| * |
| * @param desiredLocaleList Typically a user's languages, |
| * as a string which is to be parsed like LocalePriorityList does. |
| * @return the best-matching supported locale. |
| */ |
| public ULocale getBestMatch(String desiredLocaleList) { |
| return getBestMatch(LocalePriorityList.add(desiredLocaleList).build()); |
| } |
| |
| /** |
| * Returns the supported locale which best matches the desired locale. |
| * |
| * @param desiredLocale Typically a user's language. |
| * @return the best-matching supported locale. |
| */ |
| public Locale getBestLocale(Locale desiredLocale) { |
| LSR desiredLSR = getMaximalLsrOrUnd(desiredLocale); |
| int suppIndex = getBestSuppIndex(desiredLSR, null); |
| return suppIndex >= 0 ? supportedLocales[suppIndex] : defaultLocale; |
| } |
| |
| /** |
| * Returns the supported locale which best matches one of the desired locales. |
| * |
| * @param desiredLocales Typically a user's languages, in order of preference (descending). |
| * @return the best-matching supported locale. |
| */ |
| public Locale getBestLocale(Iterable<Locale> desiredLocales) { |
| Iterator<Locale> desiredIter = desiredLocales.iterator(); |
| if (!desiredIter.hasNext()) { |
| return defaultLocale; |
| } |
| LocaleLsrIterator lsrIter = new LocaleLsrIterator(desiredIter); |
| LSR desiredLSR = lsrIter.next(); |
| int suppIndex = getBestSuppIndex(desiredLSR, lsrIter); |
| return suppIndex >= 0 ? supportedLocales[suppIndex] : defaultLocale; |
| } |
| |
| private Result defaultResult() { |
| return new Result(null, defaultULocale, null, defaultLocale, -1, -1); |
| } |
| |
| private Result makeResult(ULocale desiredLocale, ULocaleLsrIterator lsrIter, int suppIndex) { |
| if (suppIndex < 0) { |
| return defaultResult(); |
| } else if (desiredLocale != null) { |
| return new Result(desiredLocale, supportedULocales[suppIndex], |
| null, supportedLocales[suppIndex], 0, suppIndex); |
| } else { |
| return new Result(lsrIter.remembered, supportedULocales[suppIndex], |
| null, supportedLocales[suppIndex], lsrIter.bestDesiredIndex, suppIndex); |
| } |
| } |
| |
| private Result makeResult(Locale desiredLocale, LocaleLsrIterator lsrIter, int suppIndex) { |
| if (suppIndex < 0) { |
| return defaultResult(); |
| } else if (desiredLocale != null) { |
| return new Result(null, supportedULocales[suppIndex], |
| desiredLocale, supportedLocales[suppIndex], 0, suppIndex); |
| } else { |
| return new Result(null, supportedULocales[suppIndex], |
| lsrIter.remembered, supportedLocales[suppIndex], |
| lsrIter.bestDesiredIndex, suppIndex); |
| } |
| } |
| |
| /** |
| * Returns the best match between the desired locale and the supported locales. |
| * |
| * @param desiredLocale Typically a user's language. |
| * @return the best-matching pair of the desired and a supported locale. |
| */ |
| public Result getBestMatchResult(ULocale desiredLocale) { |
| LSR desiredLSR = getMaximalLsrOrUnd(desiredLocale); |
| int suppIndex = getBestSuppIndex(desiredLSR, null); |
| return makeResult(desiredLocale, null, suppIndex); |
| } |
| |
| /** |
| * Returns the best match between the desired and supported locales. |
| * |
| * @param desiredLocales Typically a user's languages, in order of preference (descending). |
| * @return the best-matching pair of a desired and a supported locale. |
| */ |
| public Result getBestMatchResult(Iterable<ULocale> desiredLocales) { |
| Iterator<ULocale> desiredIter = desiredLocales.iterator(); |
| if (!desiredIter.hasNext()) { |
| return defaultResult(); |
| } |
| ULocaleLsrIterator lsrIter = new ULocaleLsrIterator(desiredIter); |
| LSR desiredLSR = lsrIter.next(); |
| int suppIndex = getBestSuppIndex(desiredLSR, lsrIter); |
| return makeResult(null, lsrIter, suppIndex); |
| } |
| |
| /** |
| * Returns the best match between the desired locale and the supported locales. |
| * |
| * @param desiredLocale Typically a user's language. |
| * @return the best-matching pair of the desired and a supported locale. |
| */ |
| public Result getBestLocaleResult(Locale desiredLocale) { |
| LSR desiredLSR = getMaximalLsrOrUnd(desiredLocale); |
| int suppIndex = getBestSuppIndex(desiredLSR, null); |
| return makeResult(desiredLocale, null, suppIndex); |
| } |
| |
| /** |
| * Returns the best match between the desired and supported locales. |
| * |
| * @param desiredLocales Typically a user's languages, in order of preference (descending). |
| * @return the best-matching pair of a desired and a supported locale. |
| */ |
| public Result getBestLocaleResult(Iterable<Locale> desiredLocales) { |
| Iterator<Locale> desiredIter = desiredLocales.iterator(); |
| if (!desiredIter.hasNext()) { |
| return defaultResult(); |
| } |
| LocaleLsrIterator lsrIter = new LocaleLsrIterator(desiredIter); |
| LSR desiredLSR = lsrIter.next(); |
| int suppIndex = getBestSuppIndex(desiredLSR, lsrIter); |
| return makeResult(null, lsrIter, suppIndex); |
| } |
| |
| /** |
| * @param desiredLSR The first desired locale's LSR. |
| * @param remainingIter Remaining desired LSRs, null or empty if none. |
| * @return the index of the best-matching supported locale, or -1 if there is no good match. |
| */ |
| private int getBestSuppIndex(LSR desiredLSR, LsrIterator remainingIter) { |
| int desiredIndex = 0; |
| int bestSupportedLsrIndex = -1; |
| StringBuilder sb = null; |
| if (TRACE_MATCHER) { |
| sb = new StringBuilder("LocaleMatcher desired:"); |
| } |
| for (int bestShiftedDistance = LocaleDistance.shiftDistance(thresholdDistance);;) { |
| if (TRACE_MATCHER) { |
| sb.append(' ').append(desiredLSR); |
| } |
| // Quick check for exact maximized LSR. |
| Integer index = supportedLsrToIndex.get(desiredLSR); |
| if (index != null) { |
| int suppIndex = index; |
| if (TRACE_MATCHER) { |
| System.err.printf("%s --> best=%s: desiredLSR=supportedLSR\n", |
| sb, supportedULocales[suppIndex]); |
| } |
| if (remainingIter != null) { remainingIter.rememberCurrent(desiredIndex); } |
| return suppIndex; |
| } |
| int bestIndexAndDistance = LocaleDistance.INSTANCE.getBestIndexAndDistance( |
| desiredLSR, supportedLSRs, supportedLSRsLength, |
| bestShiftedDistance, favorSubtag, direction); |
| if (bestIndexAndDistance >= 0) { |
| bestShiftedDistance = LocaleDistance.getShiftedDistance(bestIndexAndDistance); |
| if (remainingIter != null) { remainingIter.rememberCurrent(desiredIndex); } |
| bestSupportedLsrIndex = LocaleDistance.getIndex(bestIndexAndDistance); |
| } |
| if ((bestShiftedDistance -= LocaleDistance.shiftDistance(demotionPerDesiredLocale)) |
| <= 0) { |
| break; |
| } |
| if (remainingIter == null || !remainingIter.hasNext()) { |
| break; |
| } |
| desiredLSR = remainingIter.next(); |
| ++desiredIndex; |
| } |
| if (bestSupportedLsrIndex < 0) { |
| if (TRACE_MATCHER) { |
| System.err.printf("%s --> best=default %s: no good match\n", sb, defaultULocale); |
| } |
| return -1; |
| } |
| int suppIndex = supportedIndexes[bestSupportedLsrIndex]; |
| if (TRACE_MATCHER) { |
| System.err.printf("%s --> best=%s: best matching supported locale\n", |
| sb, supportedULocales[suppIndex]); |
| } |
| return suppIndex; |
| } |
| |
| /** |
| * Returns true if the pair of locales matches acceptably. |
| * This is influenced by Builder options such as setDirection(), setFavorSubtag(), |
| * and setMaxDistance(). |
| * |
| * @param desired The desired locale. |
| * @param supported The supported locale. |
| * @return true if the pair of locales matches acceptably. |
| */ |
| public boolean isMatch(Locale desired, Locale supported) { |
| int indexAndDistance = LocaleDistance.INSTANCE.getBestIndexAndDistance( |
| getMaximalLsrOrUnd(desired), |
| new LSR[] { getMaximalLsrOrUnd(supported) }, 1, |
| LocaleDistance.shiftDistance(thresholdDistance), favorSubtag, direction); |
| return indexAndDistance >= 0; |
| } |
| |
| /** |
| * Returns true if the pair of locales matches acceptably. |
| * This is influenced by Builder options such as setDirection(), setFavorSubtag(), |
| * and setMaxDistance(). |
| * |
| * @param desired The desired locale. |
| * @param supported The supported locale. |
| * @return true if the pair of locales matches acceptably. |
| */ |
| public boolean isMatch(ULocale desired, ULocale supported) { |
| int indexAndDistance = LocaleDistance.INSTANCE.getBestIndexAndDistance( |
| getMaximalLsrOrUnd(desired), |
| new LSR[] { getMaximalLsrOrUnd(supported) }, 1, |
| LocaleDistance.shiftDistance(thresholdDistance), favorSubtag, direction); |
| return indexAndDistance >= 0; |
| } |
| |
| /** |
| * Returns a fraction between 0 and 1, where 1 means that the languages are a |
| * perfect match, and 0 means that they are completely different. |
| * |
| * <p>This is mostly an implementation detail, and the precise values may change over time. |
| * The implementation may use either the maximized forms or the others ones, or both. |
| * The implementation may or may not rely on the forms to be consistent with each other. |
| * |
| * <p>Callers should construct and use a matcher rather than match pairs of locales directly. |
| * |
| * @param desired Desired locale. |
| * @param desiredMax Maximized locale (using likely subtags). |
| * @param supported Supported locale. |
| * @param supportedMax Maximized locale (using likely subtags). |
| * @return value between 0 and 1, inclusive. |
| * @deprecated ICU 65 Build and use a matcher rather than comparing pairs of locales. |
| */ |
| @Deprecated |
| public double match(ULocale desired, ULocale desiredMax, ULocale supported, ULocale supportedMax) { |
| // Returns the inverse of the distance: That is, 1-distance(desired, supported). |
| int indexAndDistance = LocaleDistance.INSTANCE.getBestIndexAndDistance( |
| getMaximalLsrOrUnd(desired), |
| new LSR[] { getMaximalLsrOrUnd(supported) }, 1, |
| LocaleDistance.shiftDistance(thresholdDistance), favorSubtag, direction); |
| double distance = LocaleDistance.getDistanceDouble(indexAndDistance); |
| if (TRACE_MATCHER) { |
| System.err.printf("LocaleMatcher distance(desired=%s, supported=%s)=%g\n", |
| String.valueOf(desired), String.valueOf(supported), distance); |
| } |
| return (100.0 - distance) / 100.0; |
| } |
| |
| /** |
| * Partially canonicalizes a locale (language). Note that for now, it is canonicalizing |
| * according to CLDR conventions (he vs iw, etc), since that is what is needed |
| * for likelySubtags. |
| * |
| * <p>Currently, this is a much simpler canonicalization than what the ULocale class does: |
| * The language/script/region subtags are each mapped separately, ignoring the other subtags. |
| * If none of these change, then the input locale is returned. |
| * Otherwise a new ULocale with only those subtags is returned, removing variants and extensions. |
| * |
| * @param locale language/locale code |
| * @return ULocale with remapped subtags. |
| */ |
| public ULocale canonicalize(ULocale locale) { |
| return LikelySubtags.INSTANCE.canonicalize(locale); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String toString() { |
| StringBuilder s = new StringBuilder().append("{LocaleMatcher"); |
| // Supported languages in the order that we try to match them. |
| if (supportedLSRsLength > 0) { |
| s.append(" supportedLSRs={").append(supportedLSRs[0]); |
| for (int i = 1; i < supportedLSRsLength; ++i) { |
| s.append(", ").append(supportedLSRs[i]); |
| } |
| s.append('}'); |
| } |
| s.append(" default=").append(defaultULocale); |
| if (favorSubtag != null) { |
| s.append(" favor=").append(favorSubtag); |
| } |
| if (direction != null) { |
| s.append(" direction=").append(direction); |
| } |
| if (thresholdDistance >= 0) { |
| s.append(String.format(" threshold=%d", thresholdDistance)); |
| } |
| s.append(String.format(" demotion=%d", demotionPerDesiredLocale)); |
| return s.append('}').toString(); |
| } |
| } |