Home | History | Annotate | Download | only in locale
      1 /* GENERATED SOURCE. DO NOT MODIFY. */
      2 //  2017 and later: Unicode, Inc. and others.
      3 // License & terms of use: http://www.unicode.org/copyright.html#License
      4 package android.icu.impl.locale;
      5 
      6 import java.util.Arrays;
      7 import java.util.Collection;
      8 import java.util.LinkedHashSet;
      9 import java.util.Map;
     10 import java.util.Map.Entry;
     11 import java.util.Set;
     12 
     13 import android.icu.impl.locale.XCldrStub.ImmutableMultimap;
     14 import android.icu.impl.locale.XCldrStub.ImmutableSet;
     15 import android.icu.impl.locale.XCldrStub.LinkedHashMultimap;
     16 import android.icu.impl.locale.XCldrStub.Multimap;
     17 import android.icu.impl.locale.XLikelySubtags.LSR;
     18 import android.icu.impl.locale.XLocaleDistance.DistanceOption;
     19 import android.icu.util.LocalePriorityList;
     20 import android.icu.util.Output;
     21 import android.icu.util.ULocale;
     22 
     23 /**
     24  * Immutable class that picks best match between user's desired locales and application's supported locales.
     25  * @author markdavis
     26  * @hide Only a subset of ICU is exposed in Android
     27  */
     28 public class XLocaleMatcher {
     29     private static final LSR UND = new LSR("und","","");
     30     private static final ULocale UND_LOCALE = new ULocale("und");
     31 
     32     // normally the default values, but can be set via constructor
     33 
     34     private final XLocaleDistance localeDistance;
     35     private final int thresholdDistance;
     36     private final int demotionPerAdditionalDesiredLocale;
     37     private final DistanceOption distanceOption;
     38 
     39     // built based on application's supported languages in constructor
     40 
     41     private final Map<LSR, Set<ULocale>> supportedLanguages; // the locales in the collection are ordered!
     42     private final Set<ULocale> exactSupportedLocales; // the locales in the collection are ordered!
     43     private final ULocale defaultLanguage;
     44 
     45 
     46     public static class Builder {
     47         private Set<ULocale> supportedLanguagesList;
     48         private int thresholdDistance = -1;
     49         private int demotionPerAdditionalDesiredLocale = -1;;
     50         private ULocale defaultLanguage;
     51         private XLocaleDistance localeDistance;
     52         private DistanceOption distanceOption;
     53         /**
     54          * @param languagePriorityList the languagePriorityList to set
     55          * @return this Builder object
     56          */
     57         public Builder setSupportedLocales(String languagePriorityList) {
     58             this.supportedLanguagesList = asSet(LocalePriorityList.add(languagePriorityList).build());
     59             return this;
     60         }
     61         public Builder setSupportedLocales(LocalePriorityList languagePriorityList) {
     62             this.supportedLanguagesList = asSet(languagePriorityList);
     63             return this;
     64         }
     65         public Builder setSupportedLocales(Set<ULocale> languagePriorityList) {
     66             this.supportedLanguagesList = languagePriorityList;
     67             return this;
     68         }
     69 
     70         /**
     71          * @param thresholdDistance the thresholdDistance to set, with -1 = default
     72          * @return this Builder object
     73          */
     74         public Builder setThresholdDistance(int thresholdDistance) {
     75             this.thresholdDistance = thresholdDistance;
     76             return this;
     77         }
     78         /**
     79          * @param demotionPerAdditionalDesiredLocale the demotionPerAdditionalDesiredLocale to set, with -1 = default
     80          * @return this Builder object
     81          */
     82         public Builder setDemotionPerAdditionalDesiredLocale(int demotionPerAdditionalDesiredLocale) {
     83             this.demotionPerAdditionalDesiredLocale = demotionPerAdditionalDesiredLocale;
     84             return this;
     85         }
     86 
     87         /**
     88          * @param localeDistance the localeDistance to set, with default = XLocaleDistance.getDefault().
     89          * @return this Builder object
     90          */
     91         public Builder setLocaleDistance(XLocaleDistance localeDistance) {
     92             this.localeDistance = localeDistance;
     93             return this;
     94         }
     95 
     96         /**
     97          * Set the default language, with null = default = first supported language
     98          * @param defaultLanguage the default language
     99          * @return this Builder object
    100          */
    101         public Builder setDefaultLanguage(ULocale defaultLanguage) {
    102             this.defaultLanguage = defaultLanguage;
    103             return this;
    104         }
    105 
    106         /**
    107          * If true, then the language differences are smaller than than script differences.
    108          * This is used in situations (such as maps) where it is better to fall back to the same script than a similar language.
    109          * @param distanceOption the distance option
    110          * @return this Builder object
    111          */
    112         public Builder setDistanceOption(DistanceOption distanceOption) {
    113             this.distanceOption = distanceOption;
    114             return this;
    115         }
    116 
    117         public XLocaleMatcher build() {
    118             return new XLocaleMatcher(this);
    119         }
    120     }
    121 
    122     /**
    123      * Returns a builder used in chaining parameters for building a Locale Matcher.
    124      * @return this Builder object
    125      */
    126     public static Builder builder() {
    127         return new Builder();
    128     }
    129 
    130     /** Convenience method */
    131     public XLocaleMatcher(String supportedLocales) {
    132         this(builder().setSupportedLocales(supportedLocales));
    133     }
    134     /** Convenience method */
    135     public XLocaleMatcher(LocalePriorityList supportedLocales) {
    136         this(builder().setSupportedLocales(supportedLocales));
    137     }
    138     /** Convenience method */
    139     public XLocaleMatcher(Set<ULocale> supportedLocales) {
    140         this(builder().setSupportedLocales(supportedLocales));
    141     }
    142 
    143     /**
    144      * Create a locale matcher with the given parameters.
    145      * @param supportedLocales
    146      * @param thresholdDistance
    147      * @param demotionPerAdditionalDesiredLocale
    148      * @param localeDistance
    149      * @param likelySubtags
    150      */
    151     private XLocaleMatcher(Builder builder) {
    152         localeDistance = builder.localeDistance == null ? XLocaleDistance.getDefault()
    153             : builder.localeDistance;
    154         thresholdDistance = builder.thresholdDistance < 0 ? localeDistance.getDefaultScriptDistance()
    155             : builder.thresholdDistance;
    156         // only do AFTER above are set
    157         Set<LSR> paradigms = extractLsrSet(localeDistance.getParadigms());
    158         final Multimap<LSR, ULocale> temp2 = extractLsrMap(builder.supportedLanguagesList, paradigms);
    159         supportedLanguages = temp2.asMap();
    160         exactSupportedLocales = ImmutableSet.copyOf(temp2.values());
    161         defaultLanguage = builder.defaultLanguage != null ? builder.defaultLanguage
    162             : supportedLanguages.isEmpty() ? null
    163                 : supportedLanguages.entrySet().iterator().next().getValue().iterator().next(); // first language
    164         demotionPerAdditionalDesiredLocale = builder.demotionPerAdditionalDesiredLocale < 0 ? localeDistance.getDefaultRegionDistance()+1
    165             : builder.demotionPerAdditionalDesiredLocale;
    166         distanceOption = builder.distanceOption;
    167     }
    168 
    169     // Result is not immutable!
    170     private Set<LSR> extractLsrSet(Set<ULocale> languagePriorityList) {
    171         Set<LSR> result = new LinkedHashSet<LSR>();
    172         for (ULocale item : languagePriorityList) {
    173             final LSR max = item.equals(UND_LOCALE) ? UND : LSR.fromMaximalized(item);
    174             result.add(max);
    175         }
    176         return result;
    177     }
    178 
    179     private Multimap<LSR,ULocale> extractLsrMap(Set<ULocale> languagePriorityList, Set<LSR> priorities) {
    180         Multimap<LSR, ULocale> builder = LinkedHashMultimap.create();
    181         for (ULocale item : languagePriorityList) {
    182             final LSR max = item.equals(UND_LOCALE) ? UND : LSR.fromMaximalized(item);
    183             builder.put(max, item);
    184         }
    185         if (builder.size() > 1 && priorities != null) {
    186             // for the supported list, we put any priorities before all others, except for the first.
    187             Multimap<LSR, ULocale> builder2 = LinkedHashMultimap.create();
    188 
    189             // copy the long way so the priorities are in the same order as in the original
    190             boolean first = true;
    191             for (Entry<LSR, Set<ULocale>> entry : builder.asMap().entrySet()) {
    192                 final LSR key = entry.getKey();
    193                 if (first || priorities.contains(key)) {
    194                     builder2.putAll(key, entry.getValue());
    195                     first = false;
    196                 }
    197             }
    198             // now copy the rest
    199             builder2.putAll(builder);
    200             if (!builder2.equals(builder)) {
    201                 throw new IllegalArgumentException();
    202             }
    203             builder = builder2;
    204         }
    205         return ImmutableMultimap.copyOf(builder);
    206     }
    207 
    208 
    209     /** Convenience method */
    210     public ULocale getBestMatch(ULocale ulocale) {
    211         return getBestMatch(ulocale, null);
    212     }
    213     /** Convenience method */
    214     public ULocale getBestMatch(String languageList) {
    215         return getBestMatch(LocalePriorityList.add(languageList).build(), null);
    216     }
    217     /** Convenience method */
    218     public ULocale getBestMatch(ULocale... locales) {
    219         return getBestMatch(new LinkedHashSet<ULocale>(Arrays.asList(locales)), null);
    220     }
    221     /** Convenience method */
    222     public ULocale getBestMatch(Set<ULocale> desiredLanguages) {
    223         return getBestMatch(desiredLanguages, null);
    224     }
    225     /** Convenience method */
    226     public ULocale getBestMatch(LocalePriorityList desiredLanguages) {
    227         return getBestMatch(desiredLanguages, null);
    228     }
    229     /** Convenience method */
    230     public ULocale getBestMatch(LocalePriorityList desiredLanguages, Output<ULocale> outputBestDesired) {
    231         return getBestMatch(asSet(desiredLanguages), outputBestDesired);
    232     }
    233 
    234     // TODO add LocalePriorityList method asSet() for ordered Set view backed by LocalePriorityList
    235     private static Set<ULocale> asSet(LocalePriorityList languageList) {
    236         Set<ULocale> temp = new LinkedHashSet<ULocale>(); // maintain order
    237         for (ULocale locale : languageList) {
    238             temp.add(locale);
    239         };
    240         return temp;
    241     }
    242 
    243     /**
    244      * Get the best match between the desired languages and supported languages
    245      * @param desiredLanguages Typically the supplied user's languages, in order of preference, with best first.
    246      * @param outputBestDesired The one of the desired languages that matched best.
    247      * Set to null if the best match was not below the threshold distance.
    248      * @return the best match.
    249      */
    250     public ULocale getBestMatch(Set<ULocale> desiredLanguages, Output<ULocale> outputBestDesired) {
    251         // fast path for singleton
    252         if (desiredLanguages.size() == 1) {
    253             return getBestMatch(desiredLanguages.iterator().next(), outputBestDesired);
    254         }
    255         // TODO produce optimized version for single desired ULocale
    256         Multimap<LSR, ULocale> desiredLSRs = extractLsrMap(desiredLanguages,null);
    257         int bestDistance = Integer.MAX_VALUE;
    258         ULocale bestDesiredLocale = null;
    259         Collection<ULocale> bestSupportedLocales = null;
    260         int delta = 0;
    261         mainLoop:
    262             for (final Entry<LSR, ULocale> desiredLsrAndLocale : desiredLSRs.entries()) {
    263                 // quick check for exact match
    264                 ULocale desiredLocale = desiredLsrAndLocale.getValue();
    265                 LSR desiredLSR = desiredLsrAndLocale.getKey();
    266                 if (delta < bestDistance) {
    267                     if (exactSupportedLocales.contains(desiredLocale)) {
    268                         if (outputBestDesired != null) {
    269                             outputBestDesired.value = desiredLocale;
    270                         }
    271                         return desiredLocale;
    272                     }
    273                     // quick check for maximized locale
    274                     Collection<ULocale> found = supportedLanguages.get(desiredLSR);
    275                     if (found != null) {
    276                         // if we find one in the set, return first (lowest). We already know the exact one isn't there.
    277                         if (outputBestDesired != null) {
    278                             outputBestDesired.value = desiredLocale;
    279                         }
    280                         return found.iterator().next();
    281                     }
    282                 }
    283                 for (final Entry<LSR, Set<ULocale>> supportedLsrAndLocale : supportedLanguages.entrySet()) {
    284                     int distance = delta + localeDistance.distanceRaw(desiredLSR, supportedLsrAndLocale.getKey(),
    285                         thresholdDistance, distanceOption);
    286                     if (distance < bestDistance) {
    287                         bestDistance = distance;
    288                         bestDesiredLocale = desiredLocale;
    289                         bestSupportedLocales = supportedLsrAndLocale.getValue();
    290                         if (distance == 0) {
    291                             break mainLoop;
    292                         }
    293                     }
    294                 }
    295                 delta += demotionPerAdditionalDesiredLocale;
    296             }
    297         if (bestDistance >= thresholdDistance) {
    298             if (outputBestDesired != null) {
    299                 outputBestDesired.value = null;
    300             }
    301             return defaultLanguage;
    302         }
    303         if (outputBestDesired != null) {
    304             outputBestDesired.value = bestDesiredLocale;
    305         }
    306         // pick exact match if there is one
    307         if (bestSupportedLocales.contains(bestDesiredLocale)) {
    308             return bestDesiredLocale;
    309         }
    310         // otherwise return first supported, combining variants and extensions from bestDesired
    311         return bestSupportedLocales.iterator().next();
    312     }
    313 
    314     /**
    315      * Get the best match between the desired languages and supported languages
    316      * @param desiredLocale the supplied user's language.
    317      * @param outputBestDesired The one of the desired languages that matched best.
    318      * Set to null if the best match was not below the threshold distance.
    319      * @return the best match.
    320      */
    321     public ULocale getBestMatch(ULocale desiredLocale, Output<ULocale> outputBestDesired) {
    322         int bestDistance = Integer.MAX_VALUE;
    323         ULocale bestDesiredLocale = null;
    324         Collection<ULocale> bestSupportedLocales = null;
    325 
    326         // quick check for exact match, with hack for und
    327         final LSR desiredLSR = desiredLocale.equals(UND_LOCALE) ? UND : LSR.fromMaximalized(desiredLocale);
    328 
    329         if (exactSupportedLocales.contains(desiredLocale)) {
    330             if (outputBestDesired != null) {
    331                 outputBestDesired.value = desiredLocale;
    332             }
    333             return desiredLocale;
    334         }
    335         // quick check for maximized locale
    336         if (distanceOption == DistanceOption.NORMAL) {
    337             Collection<ULocale> found = supportedLanguages.get(desiredLSR);
    338             if (found != null) {
    339                 // if we find one in the set, return first (lowest). We already know the exact one isn't there.
    340                 if (outputBestDesired != null) {
    341                     outputBestDesired.value = desiredLocale;
    342                 }
    343                 return found.iterator().next();
    344             }
    345         }
    346         for (final Entry<LSR, Set<ULocale>> supportedLsrAndLocale : supportedLanguages.entrySet()) {
    347             int distance = localeDistance.distanceRaw(desiredLSR, supportedLsrAndLocale.getKey(),
    348                 thresholdDistance, distanceOption);
    349             if (distance < bestDistance) {
    350                 bestDistance = distance;
    351                 bestDesiredLocale = desiredLocale;
    352                 bestSupportedLocales = supportedLsrAndLocale.getValue();
    353                 if (distance == 0) {
    354                     break;
    355                 }
    356             }
    357         }
    358         if (bestDistance >= thresholdDistance) {
    359             if (outputBestDesired != null) {
    360                 outputBestDesired.value = null;
    361             }
    362             return defaultLanguage;
    363         }
    364         if (outputBestDesired != null) {
    365             outputBestDesired.value = bestDesiredLocale;
    366         }
    367         // pick exact match if there is one
    368         if (bestSupportedLocales.contains(bestDesiredLocale)) {
    369             return bestDesiredLocale;
    370         }
    371         // otherwise return first supported, combining variants and extensions from bestDesired
    372         return bestSupportedLocales.iterator().next();
    373     }
    374 
    375     /** Combine features of the desired locale into those of the supported, and return result. */
    376     public static ULocale combine(ULocale bestSupported, ULocale bestDesired) {
    377         // for examples of extensions, variants, see
    378         //  http://unicode.org/repos/cldr/tags/latest/common/bcp47/
    379         //  http://unicode.org/repos/cldr/tags/latest/common/validity/variant.xml
    380 
    381         if (!bestSupported.equals(bestDesired) && bestDesired != null) {
    382             // add region, variants, extensions
    383             ULocale.Builder b = new ULocale.Builder().setLocale(bestSupported);
    384 
    385             // copy the region from the desired, if there is one
    386             String region = bestDesired.getCountry();
    387             if (!region.isEmpty()) {
    388                 b.setRegion(region);
    389             }
    390 
    391             // copy the variants from desired, if there is one
    392             // note that this will override any subvariants. Eg "sco-ulster-fonipa" + "-fonupa" => "sco-fonupa" (nuking ulster)
    393             String variants = bestDesired.getVariant();
    394             if (!variants.isEmpty()) {
    395                 b.setVariant(variants);
    396             }
    397 
    398             // copy the extensions from desired, if there are any
    399             // note that this will override any subkeys. Eg "th-u-nu-latn-ca-buddhist" + "-u-nu-native" => "th-u-nu-native" (nuking calendar)
    400             for (char extensionKey : bestDesired.getExtensionKeys()) {
    401                 b.setExtension(extensionKey, bestDesired.getExtension(extensionKey));
    402             }
    403             bestSupported = b.build();
    404         }
    405         return bestSupported;
    406     }
    407 
    408     /** Returns the distance between the two languages. The values are not necessarily symmetric.
    409      * @param desired A locale desired by the user
    410      * @param supported A locale supported by a program.
    411      * @return A return of 0 is a complete match, and 100 is a failure case (above the thresholdDistance).
    412      * A language is first maximized with add likely subtags, then compared.
    413      */
    414     public int distance(ULocale desired, ULocale supported) {
    415         return localeDistance.distanceRaw(
    416             LSR.fromMaximalized(desired),
    417             LSR.fromMaximalized(supported), thresholdDistance, distanceOption);
    418     }
    419 
    420     /** Convenience method */
    421     public int distance(String desiredLanguage, String supportedLanguage) {
    422         return localeDistance.distanceRaw(
    423             LSR.fromMaximalized(new ULocale(desiredLanguage)),
    424             LSR.fromMaximalized(new ULocale(supportedLanguage)),
    425             thresholdDistance, distanceOption);
    426     }
    427 
    428     @Override
    429     public String toString() {
    430         return exactSupportedLocales.toString();
    431     }
    432 
    433     /** Return the inverse of the distance: that is, 1-distance(desired, supported) */
    434     public double match(ULocale desired, ULocale supported) {
    435         return (100-distance(desired, supported))/100.0;
    436     }
    437 
    438     /**
    439      * Returns a fraction between 0 and 1, where 1 means that the languages are a
    440      * perfect match, and 0 means that they are completely different. This is (100-distance(desired, supported))/100.0.
    441      * <br>Note that
    442      * the precise values may change over time; no code should be made dependent
    443      * on the values remaining constant.
    444      * @param desired Desired locale
    445      * @param desiredMax Maximized locale (using likely subtags)
    446      * @param supported Supported locale
    447      * @param supportedMax Maximized locale (using likely subtags)
    448      * @return value between 0 and 1, inclusive.
    449      * @deprecated Use the form with 2 parameters instead.
    450      */
    451     @Deprecated
    452     public double match(ULocale desired, ULocale desiredMax, ULocale supported, ULocale supportedMax) {
    453         return match(desired, supported);
    454     }
    455 
    456     /**
    457      * Canonicalize a locale (language). Note that for now, it is canonicalizing
    458      * according to CLDR conventions (he vs iw, etc), since that is what is needed
    459      * for likelySubtags.
    460      * @param ulocale language/locale code
    461      * @return ULocale with remapped subtags.
    462      */
    463     public ULocale canonicalize(ULocale ulocale) {
    464         // TODO
    465         return null;
    466     }
    467 
    468     /**
    469      * @return the thresholdDistance. Any distance above this value is treated as a match failure.
    470      */
    471     public int getThresholdDistance() {
    472         return thresholdDistance;
    473     }
    474 }
    475