| // © 2017 and later: Unicode, Inc. and others. |
| // License & terms of use: http://www.unicode.org/copyright.html#License |
| package com.ibm.icu.impl.locale; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Objects; |
| |
| import com.ibm.icu.util.LocalePriorityList; |
| import com.ibm.icu.util.ULocale; |
| |
| /** |
| * Immutable class that picks the best match between a user's desired locales and |
| * and application's supported locales. |
| * |
| * <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>TODO: Migration notes. |
| * |
| * @author markdavis |
| */ |
| public final class XLocaleMatcher { |
| private static final LSR UND_LSR = new LSR("und","",""); |
| private static final ULocale UND_ULOCALE = new ULocale("und"); |
| private static final Locale UND_LOCALE = new Locale("und"); |
| |
| // 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 Builder#setFavorSubtag(FavorSubtag) |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| public enum FavorSubtag { |
| /** |
| * Language differences are most important, then script differences, then region differences. |
| * (This is the default behavior.) |
| * |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| LANGUAGE, |
| /** |
| * Makes script differences matter relatively more than language differences. |
| * |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| SCRIPT |
| } |
| |
| /** |
| * Builder option for whether all desired locales are treated equally or |
| * earlier ones are preferred. |
| * |
| * @see Builder#setDemotionPerDesiredLocale(Demotion) |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| public enum Demotion { |
| /** |
| * All desired locales are treated equally. |
| * |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| 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> |
| * |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| REGION |
| } |
| |
| /** |
| * Data for the best-matching pair of a desired and a supported locale. |
| * |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| 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. |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| 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. |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| 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 the list of supported locales is empty and |
| * no explicit default locale is set. |
| * |
| * @return the best-matching supported locale, or null. |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| 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 the list of supported locales is empty and |
| * no explicit default locale is set. |
| * |
| * @return the best-matching supported locale, or null. |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| 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. |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| 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. |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| 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. |
| * |
| * <p>Example: desired=ar-SA-u-nu-latn, supported=ar-EG, service locale=ar-EG-u-nu-latn |
| * |
| * @return the service locale, combining the best-matching desired and supported locales. |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| public ULocale makeServiceULocale() { |
| ULocale bestDesired = getDesiredULocale(); |
| ULocale serviceLocale = supportedULocale; |
| if (!serviceLocale.equals(bestDesired) && bestDesired != null) { |
| ULocale.Builder b = new ULocale.Builder().setLocale(serviceLocale); |
| |
| // Copy the region from bestDesired, if there is one. |
| // TODO: Seems wrong to clobber serviceLocale.getCountry() if that is not empty. |
| 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 serviceLocale variants. |
| // For example, "sco-ulster-fonipa" + "...-fonupa" => "sco-fonupa" (replacing ulster). |
| // TODO: Why replace? Why not append? |
| 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 serviceLocale extensions. |
| // For example, "th-u-nu-latn-ca-buddhist" + "...-u-nu-native" => "th-u-nu-native" |
| // (replacing calendar). |
| // TODO: Maybe enumerate -u- keys to not replace others in the serviceLocale?? |
| // (Unsure about this one.) |
| for (char extensionKey : bestDesired.getExtensionKeys()) { |
| b.setExtension(extensionKey, bestDesired.getExtension(extensionKey)); |
| } |
| serviceLocale = b.build(); |
| } |
| return serviceLocale; |
| } |
| |
| /** |
| * 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. |
| * |
| * <p>Example: desired=ar-SA-u-nu-latn, supported=ar-EG, service locale=ar-EG-u-nu-latn |
| * |
| * @return the service locale, combining the best-matching desired and supported locales. |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| public Locale makeServiceLocale() { |
| return makeServiceULocale().toLocale(); |
| } |
| } |
| |
| private final int thresholdDistance; |
| private final int demotionPerDesiredLocale; |
| private final FavorSubtag favorSubtag; |
| |
| // 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 ULocale defaultULocale; |
| private final Locale defaultLocale; |
| private final int defaultLocaleIndex; |
| |
| /** |
| * LocaleMatcher Builder. |
| * |
| * @see XLocaleMatcher#builder() |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| public static class Builder { |
| private List<ULocale> supportedLocales; |
| private int thresholdDistance = -1; |
| private Demotion demotion; |
| private ULocale defaultLocale; |
| private FavorSubtag favor; |
| |
| /** |
| * Parses the string like {@link LocalePriorityList} does and |
| * sets the supported locales accordingly. |
| * Clears any previously set/added supported locales first. |
| * |
| * @param locales the languagePriorityList to set |
| * @return this Builder object |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| 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 locale |
| * @return this Builder object |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| 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 |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| 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 the list of locale |
| * @return this Builder object |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| 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 the list of locale |
| * @return this Builder object |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| public Builder addSupportedLocale(Locale locale) { |
| return addSupportedULocale(ULocale.forLocale(locale)); |
| } |
| |
| /** |
| * Sets the default locale; if null, or if it is not set explicitly, |
| * then the first supported locale is used as the default locale. |
| * |
| * @param defaultLocale the default locale |
| * @return this Builder object |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| public Builder setDefaultULocale(ULocale defaultLocale) { |
| this.defaultLocale = defaultLocale; |
| 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. |
| * |
| * @param defaultLocale the default locale |
| * @return this Builder object |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| public Builder setDefaultLocale(Locale defaultLocale) { |
| this.defaultLocale = ULocale.forLocale(defaultLocale); |
| 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 |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| 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 |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| public Builder setDemotionPerDesiredLocale(Demotion demotion) { |
| this.demotion = demotion; |
| return this; |
| } |
| |
| /** |
| * <i>Internal only!</i> |
| * |
| * @param thresholdDistance the thresholdDistance to set, with -1 = default |
| * @return this Builder object |
| * @internal |
| * @deprecated This API is ICU internal only. |
| */ |
| @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 XLocaleMatcher. |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| public XLocaleMatcher build() { |
| return new XLocaleMatcher(this); |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder s = new StringBuilder().append("{XLocaleMatcher.Builder"); |
| if (!supportedLocales.isEmpty()) { |
| s.append(" supported={").append(supportedLocales.toString()).append('}'); |
| } |
| if (defaultLocale != null) { |
| s.append(" default=").append(defaultLocale.toString()); |
| } |
| if (favor != null) { |
| s.append(" distance=").append(favor.toString()); |
| } |
| if (thresholdDistance >= 0) { |
| s.append(String.format(" threshold=%d", thresholdDistance)); |
| } |
| if (demotion != null) { |
| s.append(" demotion=").append(demotion.toString()); |
| } |
| return s.append('}').toString(); |
| } |
| } |
| |
| /** |
| * Returns a builder used in chaining parameters for building a LocaleMatcher. |
| * |
| * @return a new Builder object |
| * @draft ICU 65 |
| * @provisional This API might change or be removed in a future release. |
| */ |
| public static Builder builder() { |
| return new Builder(); |
| } |
| |
| /** Convenience method */ |
| public XLocaleMatcher(String supportedLocales) { |
| this(builder().setSupportedLocales(supportedLocales)); |
| } |
| /** Convenience method */ |
| public XLocaleMatcher(LocalePriorityList supportedLocales) { |
| this(builder().setSupportedULocales(supportedLocales.getULocales())); |
| } |
| |
| private XLocaleMatcher(Builder builder) { |
| thresholdDistance = builder.thresholdDistance < 0 ? |
| LocaleDistance.INSTANCE.getDefaultScriptDistance() : builder.thresholdDistance; |
| // 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.size(); |
| supportedULocales = new ULocale[supportedLocalesLength]; |
| supportedLocales = new Locale[supportedLocalesLength]; |
| // Supported LRSs in input order. |
| LSR lsrs[] = new LSR[supportedLocalesLength]; |
| // Also find the first supported locale whose LSR is |
| // the same as that for the default locale. |
| ULocale udef = builder.defaultLocale; |
| Locale def = null; |
| LSR defLSR = null; |
| int idef = -1; |
| if (udef != null) { |
| def = udef.toLocale(); |
| defLSR = getMaximalLsrOrUnd(udef); |
| } |
| int i = 0; |
| for (ULocale locale : builder.supportedLocales) { |
| supportedULocales[i] = locale; |
| supportedLocales[i] = locale.toLocale(); |
| LSR lsr = lsrs[i] = getMaximalLsrOrUnd(locale); |
| if (idef < 0 && defLSR != null && lsr.equals(defLSR)) { |
| idef = i; |
| } |
| ++i; |
| } |
| |
| // We need an unordered map from LSR to first supported locale with that LSR, |
| // and an ordered list of (LSR, Indexes). |
| // We use a LinkedHashMap for both, |
| // and insert the supported locales in the following order: |
| // 1. Default locale, if it is supported. |
| // 2. Priority locales in builder order. |
| // 3. Remaining locales in builder order. |
| supportedLsrToIndex = new LinkedHashMap<>(supportedLocalesLength); |
| Map<LSR, Integer> otherLsrToIndex = null; |
| if (idef >= 0) { |
| supportedLsrToIndex.put(defLSR, idef); |
| } |
| i = 0; |
| for (ULocale locale : supportedULocales) { |
| if (i == idef) { continue; } |
| LSR lsr = lsrs[i]; |
| if (defLSR == null) { |
| assert i == 0; |
| udef = locale; |
| def = supportedLocales[0]; |
| defLSR = lsr; |
| idef = 0; |
| supportedLsrToIndex.put(lsr, 0); |
| } else if (lsr.equals(defLSR) || LocaleDistance.INSTANCE.isParadigmLSR(lsr)) { |
| putIfAbsent(supportedLsrToIndex, lsr, i); |
| } else { |
| if (otherLsrToIndex == null) { |
| otherLsrToIndex = new LinkedHashMap<>(supportedLocalesLength); |
| } |
| putIfAbsent(otherLsrToIndex, lsr, i); |
| } |
| ++i; |
| } |
| if (otherLsrToIndex != null) { |
| supportedLsrToIndex.putAll(otherLsrToIndex); |
| } |
| int numSuppLsrs = supportedLsrToIndex.size(); |
| supportedLsrs = new LSR[numSuppLsrs]; |
| supportedIndexes = new int[numSuppLsrs]; |
| i = 0; |
| for (Map.Entry<LSR, Integer> entry : supportedLsrToIndex.entrySet()) { |
| supportedLsrs[i] = entry.getKey(); // = lsrs[entry.getValue()] |
| supportedIndexes[i++] = entry.getValue(); |
| } |
| |
| defaultULocale = udef; |
| defaultLocale = def; |
| defaultLocaleIndex = idef; |
| demotionPerDesiredLocale = |
| builder.demotion == Demotion.NONE ? 0 : |
| LocaleDistance.INSTANCE.getDefaultDemotionPerDesiredLocale(); // null or REGION |
| favorSubtag = builder.favor; |
| } |
| |
| private static final void putIfAbsent(Map<LSR, Integer> lsrToIndex, LSR lsr, int i) { |
| Integer index = lsrToIndex.get(lsr); |
| if (index == null) { |
| lsrToIndex.put(lsr, i); |
| } |
| } |
| |
| private static final LSR getMaximalLsrOrUnd(ULocale locale) { |
| if (locale.equals(UND_ULOCALE)) { |
| return UND_LSR; |
| } else { |
| return XLikelySubtags.INSTANCE.makeMaximizedLsrFrom(locale); |
| } |
| } |
| |
| private static final LSR getMaximalLsrOrUnd(Locale locale) { |
| if (locale.equals(UND_LOCALE)) { |
| return UND_LSR; |
| } else { |
| return XLikelySubtags.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; |
| } |
| } |
| |
| public ULocale getBestMatch(ULocale desiredLocale) { |
| LSR desiredLSR = getMaximalLsrOrUnd(desiredLocale); |
| int suppIndex = getBestSuppIndex(desiredLSR, null); |
| return suppIndex >= 0 ? supportedULocales[suppIndex] : defaultULocale; |
| } |
| |
| 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; |
| } |
| |
| public ULocale getBestMatch(String desiredLocaleList) { |
| return getBestMatch(LocalePriorityList.add(desiredLocaleList).build()); |
| } |
| |
| public Locale getBestLocale(Locale desiredLocale) { |
| LSR desiredLSR = getMaximalLsrOrUnd(desiredLocale); |
| int suppIndex = getBestSuppIndex(desiredLSR, null); |
| return suppIndex >= 0 ? supportedLocales[suppIndex] : defaultLocale; |
| } |
| |
| 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 makeResult(ULocale desiredLocale, ULocaleLsrIterator lsrIter, int suppIndex) { |
| if (suppIndex < 0) { |
| return new Result(null, defaultULocale, null, defaultLocale, -1, defaultLocaleIndex); |
| } 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 new Result(null, defaultULocale, null, defaultLocale, -1, defaultLocaleIndex); |
| } 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); |
| } |
| } |
| |
| 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 makeResult(UND_ULOCALE, null, -1); |
| } |
| ULocaleLsrIterator lsrIter = new ULocaleLsrIterator(desiredIter); |
| LSR desiredLSR = lsrIter.next(); |
| int suppIndex = getBestSuppIndex(desiredLSR, lsrIter); |
| return makeResult(null, lsrIter, suppIndex); |
| } |
| |
| public Result getBestLocaleResult(Locale desiredLocale) { |
| LSR desiredLSR = getMaximalLsrOrUnd(desiredLocale); |
| int suppIndex = getBestSuppIndex(desiredLSR, null); |
| return makeResult(desiredLocale, null, suppIndex); |
| } |
| |
| public Result getBestLocaleResult(Iterable<Locale> desiredLocales) { |
| Iterator<Locale> desiredIter = desiredLocales.iterator(); |
| if (!desiredIter.hasNext()) { |
| return makeResult(UND_LOCALE, null, -1); |
| } |
| 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; |
| for (int bestDistance = thresholdDistance;;) { |
| // Quick check for exact maximized LSR. |
| Integer index = supportedLsrToIndex.get(desiredLSR); |
| if (index != null) { |
| int suppIndex = index; |
| if (TRACE_MATCHER) { |
| System.err.printf("Returning %s: desiredLSR=supportedLSR\n", |
| supportedULocales[suppIndex]); |
| } |
| if (remainingIter != null) { remainingIter.rememberCurrent(desiredIndex); } |
| return suppIndex; |
| } |
| int bestIndexAndDistance = LocaleDistance.INSTANCE.getBestIndexAndDistance( |
| desiredLSR, supportedLsrs, bestDistance, favorSubtag); |
| if (bestIndexAndDistance >= 0) { |
| bestDistance = bestIndexAndDistance & 0xff; |
| if (remainingIter != null) { remainingIter.rememberCurrent(desiredIndex); } |
| bestSupportedLsrIndex = bestIndexAndDistance >> 8; |
| } |
| if ((bestDistance -= demotionPerDesiredLocale) <= 0) { |
| break; |
| } |
| if (remainingIter == null || !remainingIter.hasNext()) { |
| break; |
| } |
| desiredLSR = remainingIter.next(); |
| } |
| if (bestSupportedLsrIndex < 0) { |
| if (TRACE_MATCHER) { |
| System.err.printf("Returning default %s: no good match\n", defaultULocale); |
| } |
| return -1; |
| } |
| int suppIndex = supportedIndexes[bestSupportedLsrIndex]; |
| if (TRACE_MATCHER) { |
| System.err.printf("Returning %s: best matching supported locale\n", |
| supportedULocales[suppIndex]); |
| } |
| return suppIndex; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder s = new StringBuilder().append("{XLocaleMatcher"); |
| if (supportedULocales.length > 0) { |
| s.append(" supported={").append(supportedULocales[0].toString()); |
| for (int i = 1; i < supportedULocales.length; ++i) { |
| s.append(", ").append(supportedULocales[i].toString()); |
| } |
| s.append('}'); |
| } |
| s.append(" default=").append(Objects.toString(defaultULocale)); |
| if (favorSubtag != null) { |
| s.append(" distance=").append(favorSubtag.toString()); |
| } |
| if (thresholdDistance >= 0) { |
| s.append(String.format(" threshold=%d", thresholdDistance)); |
| } |
| s.append(String.format(" demotion=%d", demotionPerDesiredLocale)); |
| return s.append('}').toString(); |
| } |
| |
| /** |
| * 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. This is (100-distance(desired, supported))/100.0. |
| * <br>Note that |
| * the precise values may change over time; no code should be made dependent |
| * on the values remaining constant. |
| * @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 distance = LocaleDistance.INSTANCE.getBestIndexAndDistance( |
| XLikelySubtags.INSTANCE.makeMaximizedLsrFrom(desired), |
| new LSR[] { XLikelySubtags.INSTANCE.makeMaximizedLsrFrom(supported) }, |
| thresholdDistance, favorSubtag) & 0xff; |
| return (100 - distance) / 100.0; |
| } |
| |
| /** |
| * Canonicalize 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. |
| * @param ulocale language/locale code |
| * @return ULocale with remapped subtags. |
| * @stable ICU 4.4 |
| */ |
| public ULocale canonicalize(ULocale ulocale) { |
| // TODO |
| return null; |
| } |
| } |