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