1 /* GENERATED SOURCE. DO NOT MODIFY. */ 2 // 2016 and later: Unicode, Inc. and others. 3 // License & terms of use: http://www.unicode.org/copyright.html#License 4 /* 5 **************************************************************************************** 6 * Copyright (C) 2009-2016, Google, Inc.; International Business Machines Corporation 7 * and others. All Rights Reserved. 8 **************************************************************************************** 9 */ 10 package android.icu.util; 11 12 import java.util.HashMap; 13 import java.util.HashSet; 14 import java.util.Iterator; 15 import java.util.LinkedHashMap; 16 import java.util.LinkedHashSet; 17 import java.util.Map; 18 import java.util.Map.Entry; 19 import java.util.Set; 20 import java.util.regex.Matcher; 21 import java.util.regex.Pattern; 22 23 import android.icu.impl.ICUData; 24 import android.icu.impl.ICUResourceBundle; 25 import android.icu.impl.Relation; 26 import android.icu.impl.Row; 27 import android.icu.impl.Row.R3; 28 import android.icu.impl.Utility; 29 import android.icu.impl.locale.XLocaleDistance.DistanceOption; 30 import android.icu.impl.locale.XLocaleMatcher; 31 import android.icu.impl.locale.XLocaleMatcher.Builder; 32 33 /** 34 * Provides a way to match the languages (locales) supported by a product to the 35 * languages (locales) acceptable to a user, and get the best match. For 36 * example: 37 * 38 * <pre> 39 * LocaleMatcher matcher = new LocaleMatcher("fr, en-GB, en"); 40 * 41 * // afterwards: 42 * matcher.getBestMatch("en-US").toLanguageTag() => "en" 43 * </pre> 44 * 45 * It takes into account when languages are close to one another, such as fil 46 * and tl, and when language regional variants are close, like en-GB and en-AU. 47 * It also handles scripts, like zh-Hant vs zh-TW. For examples, see the test 48 * file. 49 * <p>All classes implementing this interface should be immutable. Often a 50 * product will just need one static instance, built with the languages 51 * that it supports. However, it may want multiple instances with different 52 * default languages based on additional information, such as the domain. 53 * 54 * @author markdavis (at) google.com 55 * @hide Only a subset of ICU is exposed in Android 56 */ 57 public class LocaleMatcher { 58 59 /** 60 * @deprecated This API is ICU internal only. 61 * @hide draft / provisional / internal are hidden on Android 62 */ 63 @Deprecated 64 public static final boolean DEBUG = false; 65 66 private static final ULocale UNKNOWN_LOCALE = new ULocale("und"); 67 68 /** 69 * Threshold for falling back to the default (first) language. May make this 70 * a parameter in the future. 71 */ 72 private static final double DEFAULT_THRESHOLD = 0.5; 73 74 /** 75 * The default language, in case the threshold is not met. 76 */ 77 private final ULocale defaultLanguage; 78 79 /** 80 * The default language, in case the threshold is not met. 81 */ 82 private final double threshold; 83 84 /** 85 * Create a new language matcher. The highest-weighted language is the 86 * default. That means that if no other language is matches closer than a given 87 * threshold, that default language is chosen. Typically the default is English, 88 * but it could be different based on additional information, such as the domain 89 * of the page. 90 * 91 * @param languagePriorityList weighted list 92 */ 93 public LocaleMatcher(LocalePriorityList languagePriorityList) { 94 this(languagePriorityList, defaultWritten); 95 } 96 97 /** 98 * Create a new language matcher from a String form. The highest-weighted 99 * language is the default. 100 * 101 * @param languagePriorityListString String form of LanguagePriorityList 102 */ 103 public LocaleMatcher(String languagePriorityListString) { 104 this(LocalePriorityList.add(languagePriorityListString).build()); 105 } 106 107 /** 108 * Internal testing function; may expose API later. 109 * @param languagePriorityList LocalePriorityList to match 110 * @param matcherData Internal matching data 111 * @deprecated This API is ICU internal only. 112 * @hide draft / provisional / internal are hidden on Android 113 */ 114 @Deprecated 115 public LocaleMatcher(LocalePriorityList languagePriorityList, LanguageMatcherData matcherData) { 116 this(languagePriorityList, matcherData, DEFAULT_THRESHOLD); 117 } 118 119 /** 120 * Internal testing function; may expose API later. 121 * @param languagePriorityList LocalePriorityList to match 122 * @param matcherData Internal matching data 123 * @deprecated This API is ICU internal only. 124 * @hide draft / provisional / internal are hidden on Android 125 */ 126 @Deprecated 127 public LocaleMatcher(LocalePriorityList languagePriorityList, LanguageMatcherData matcherData, double threshold) { 128 this.matcherData = matcherData == null ? defaultWritten : matcherData.freeze(); 129 this.languagePriorityList = languagePriorityList; 130 for (final ULocale language : languagePriorityList) { 131 add(language, languagePriorityList.getWeight(language)); 132 } 133 processMapping(); 134 Iterator<ULocale> it = languagePriorityList.iterator(); 135 defaultLanguage = it.hasNext() ? it.next() : null; 136 this.threshold = threshold; 137 } 138 139 140 /** 141 * Returns a fraction between 0 and 1, where 1 means that the languages are a 142 * perfect match, and 0 means that they are completely different. Note that 143 * the precise values may change over time; no code should be made dependent 144 * on the values remaining constant. 145 * @param desired Desired locale 146 * @param desiredMax Maximized locale (using likely subtags) 147 * @param supported Supported locale 148 * @param supportedMax Maximized locale (using likely subtags) 149 * @return value between 0 and 1, inclusive. 150 */ 151 public double match(ULocale desired, ULocale desiredMax, ULocale supported, ULocale supportedMax) { 152 return matcherData.match(desired, desiredMax, supported, supportedMax); 153 } 154 155 156 /** 157 * Canonicalize a locale (language). Note that for now, it is canonicalizing 158 * according to CLDR conventions (he vs iw, etc), since that is what is needed 159 * for likelySubtags. 160 * @param ulocale language/locale code 161 * @return ULocale with remapped subtags. 162 */ 163 public ULocale canonicalize(ULocale ulocale) { 164 // TODO Get the data from CLDR, use Java conventions. 165 String lang = ulocale.getLanguage(); 166 String lang2 = canonicalMap.get(lang); 167 String script = ulocale.getScript(); 168 String script2 = canonicalMap.get(script); 169 String region = ulocale.getCountry(); 170 String region2 = canonicalMap.get(region); 171 if (lang2 != null || script2 != null || region2 != null) { 172 return new ULocale( 173 lang2 == null ? lang : lang2, 174 script2 == null ? script : script2, 175 region2 == null ? region : region2 176 ); 177 } 178 return ulocale; 179 } 180 181 /** 182 * Get the best match for a LanguagePriorityList 183 * 184 * @param languageList list to match 185 * @return best matching language code 186 */ 187 public ULocale getBestMatch(LocalePriorityList languageList) { 188 double bestWeight = 0; 189 ULocale bestTableMatch = null; 190 double penalty = 0; 191 OutputDouble matchWeight = new OutputDouble(); 192 for (final ULocale language : languageList) { 193 final ULocale matchLocale = getBestMatchInternal(language, matchWeight); 194 final double weight = matchWeight.value * languageList.getWeight(language) - penalty; 195 if (weight > bestWeight) { 196 bestWeight = weight; 197 bestTableMatch = matchLocale; 198 } 199 penalty += 0.07000001; 200 } 201 if (bestWeight < threshold) { 202 bestTableMatch = defaultLanguage; 203 } 204 return bestTableMatch; 205 } 206 207 /** 208 * Convenience method: Get the best match for a LanguagePriorityList 209 * 210 * @param languageList String form of language priority list 211 * @return best matching language code 212 */ 213 public ULocale getBestMatch(String languageList) { 214 return getBestMatch(LocalePriorityList.add(languageList).build()); 215 } 216 217 /** 218 * Get the best match for an individual language code. 219 * 220 * @param ulocale locale/language code to match 221 * @return best matching language code 222 */ 223 public ULocale getBestMatch(ULocale ulocale) { 224 return getBestMatchInternal(ulocale, null); 225 } 226 227 /** 228 * @deprecated This API is ICU internal only. 229 * @hide draft / provisional / internal are hidden on Android 230 */ 231 @Deprecated 232 public ULocale getBestMatch(ULocale... ulocales) { 233 return getBestMatch(LocalePriorityList.add(ulocales).build()); 234 } 235 236 /** 237 * {@inheritDoc} 238 */ 239 @Override 240 public String toString() { 241 return "{" + defaultLanguage + ", " 242 + localeToMaxLocaleAndWeight + "}"; 243 } 244 // ================= Privates ===================== 245 246 /** 247 * Get the best match for an individual language code. 248 * 249 * @param languageCode 250 * @return best matching language code and weight (as per 251 * {@link #match(ULocale, ULocale)}) 252 */ 253 private ULocale getBestMatchInternal(ULocale languageCode, OutputDouble outputWeight) { 254 languageCode = canonicalize(languageCode); 255 final ULocale maximized = addLikelySubtags(languageCode); 256 if (DEBUG) { 257 System.out.println("\ngetBestMatchInternal: " + languageCode + ";\t" + maximized); 258 } 259 double bestWeight = 0; 260 ULocale bestTableMatch = null; 261 String baseLanguage = maximized.getLanguage(); 262 Set<R3<ULocale, ULocale, Double>> searchTable = desiredLanguageToPossibleLocalesToMaxLocaleToData.get(baseLanguage); 263 if (searchTable != null) { // we preprocessed the table so as to filter by lanugage 264 if (DEBUG) System.out.println("\tSearching: " + searchTable); 265 for (final R3<ULocale, ULocale, Double> tableKeyValue : searchTable) { 266 ULocale tableKey = tableKeyValue.get0(); 267 ULocale maxLocale = tableKeyValue.get1(); 268 Double matchedWeight = tableKeyValue.get2(); 269 final double match = match(languageCode, maximized, tableKey, maxLocale); 270 if (DEBUG) { 271 System.out.println("\t" + tableKeyValue + ";\t" + match + "\n"); 272 } 273 final double weight = match * matchedWeight; 274 if (weight > bestWeight) { 275 bestWeight = weight; 276 bestTableMatch = tableKey; 277 if (weight > 0.999d) { // bail on good enough match. 278 break; 279 } 280 } 281 } 282 } 283 if (bestWeight < threshold) { 284 bestTableMatch = defaultLanguage; 285 } 286 if (outputWeight != null) { 287 outputWeight.value = bestWeight; // only return the weight when needed 288 } 289 return bestTableMatch; 290 } 291 292 /** 293 * @deprecated This API is ICU internal only. 294 * @hide draft / provisional / internal are hidden on Android 295 */ 296 @Deprecated 297 private static class OutputDouble { // TODO, move to where OutputInt is 298 double value; 299 } 300 301 private void add(ULocale language, Double weight) { 302 language = canonicalize(language); 303 R3<ULocale, ULocale, Double> row = Row.of(language, addLikelySubtags(language), weight); 304 row.freeze(); 305 localeToMaxLocaleAndWeight.add(row); 306 } 307 308 /** 309 * We preprocess the data to get just the possible matches for each desired base language. 310 */ 311 private void processMapping() { 312 for (Entry<String, Set<String>> desiredToMatchingLanguages : matcherData.matchingLanguages().keyValuesSet()) { 313 String desired = desiredToMatchingLanguages.getKey(); 314 Set<String> supported = desiredToMatchingLanguages.getValue(); 315 for (R3<ULocale, ULocale, Double> localeToMaxAndWeight : localeToMaxLocaleAndWeight) { 316 final ULocale key = localeToMaxAndWeight.get0(); 317 String lang = key.getLanguage(); 318 if (supported.contains(lang)) { 319 addFiltered(desired, localeToMaxAndWeight); 320 } 321 } 322 } 323 // now put in the values directly, since languages always map to themselves 324 for (R3<ULocale, ULocale, Double> localeToMaxAndWeight : localeToMaxLocaleAndWeight) { 325 final ULocale key = localeToMaxAndWeight.get0(); 326 String lang = key.getLanguage(); 327 addFiltered(lang, localeToMaxAndWeight); 328 } 329 } 330 331 private void addFiltered(String desired, R3<ULocale, ULocale, Double> localeToMaxAndWeight) { 332 Set<R3<ULocale, ULocale, Double>> map = desiredLanguageToPossibleLocalesToMaxLocaleToData.get(desired); 333 if (map == null) { 334 desiredLanguageToPossibleLocalesToMaxLocaleToData.put(desired, map = new LinkedHashSet<R3<ULocale, ULocale, Double>>()); 335 } 336 map.add(localeToMaxAndWeight); 337 if (DEBUG) { 338 System.out.println(desired + ", " + localeToMaxAndWeight); 339 } 340 } 341 342 Set<Row.R3<ULocale, ULocale, Double>> localeToMaxLocaleAndWeight = new LinkedHashSet<Row.R3<ULocale, ULocale, Double>>(); 343 Map<String,Set<Row.R3<ULocale, ULocale, Double>>> desiredLanguageToPossibleLocalesToMaxLocaleToData 344 = new LinkedHashMap<String,Set<Row.R3<ULocale, ULocale, Double>>>(); 345 346 // =============== Special Mapping Information ============== 347 348 /** 349 * We need to add another method to addLikelySubtags that doesn't return 350 * null, but instead substitutes Zzzz and ZZ if unknown. There are also 351 * a few cases where addLikelySubtags needs to have expanded data, to handle 352 * all deprecated codes. 353 * @param languageCode 354 * @return "fixed" addLikelySubtags 355 */ 356 private ULocale addLikelySubtags(ULocale languageCode) { 357 // max("und") = "en_Latn_US", and since matching is based on maximized tags, the undefined 358 // language would normally match English. But that would produce the counterintuitive results 359 // that getBestMatch("und", LocaleMatcher("it,en")) would be "en", and 360 // getBestMatch("en", LocaleMatcher("it,und")) would be "und". 361 // 362 // To avoid that, we change the matcher's definitions of max (AddLikelySubtagsWithDefaults) 363 // so that max("und")="und". That produces the following, more desirable results: 364 if (languageCode.equals(UNKNOWN_LOCALE)) { 365 return UNKNOWN_LOCALE; 366 } 367 final ULocale result = ULocale.addLikelySubtags(languageCode); 368 // should have method on getLikelySubtags for this 369 if (result == null || result.equals(languageCode)) { 370 final String language = languageCode.getLanguage(); 371 final String script = languageCode.getScript(); 372 final String region = languageCode.getCountry(); 373 return new ULocale((language.length()==0 ? "und" 374 : language) 375 + "_" 376 + (script.length()==0 ? "Zzzz" : script) 377 + "_" 378 + (region.length()==0 ? "ZZ" : region)); 379 } 380 return result; 381 } 382 383 private static class LocalePatternMatcher { 384 // a value of null means a wildcard; matches any. 385 private String lang; 386 private String script; 387 private String region; 388 private Level level; 389 static Pattern pattern = Pattern.compile( 390 "([a-z]{1,8}|\\*)" 391 + "(?:[_-]([A-Z][a-z]{3}|\\*))?" 392 + "(?:[_-]([A-Z]{2}|[0-9]{3}|\\*))?"); 393 394 public LocalePatternMatcher(String toMatch) { 395 Matcher matcher = pattern.matcher(toMatch); 396 if (!matcher.matches()) { 397 throw new IllegalArgumentException("Bad pattern: " + toMatch); 398 } 399 lang = matcher.group(1); 400 script = matcher.group(2); 401 region = matcher.group(3); 402 level = region != null ? Level.region : script != null ? Level.script : Level.language; 403 404 if (lang.equals("*")) { 405 lang = null; 406 } 407 if (script != null && script.equals("*")) { 408 script = null; 409 } 410 if (region != null && region.equals("*")) { 411 region = null; 412 } 413 } 414 415 boolean matches(ULocale ulocale) { 416 if (lang != null && !lang.equals(ulocale.getLanguage())) { 417 return false; 418 } 419 if (script != null && !script.equals(ulocale.getScript())) { 420 return false; 421 } 422 if (region != null && !region.equals(ulocale.getCountry())) { 423 return false; 424 } 425 return true; 426 } 427 428 public Level getLevel() { 429 return level; 430 } 431 432 public String getLanguage() { 433 return (lang == null ? "*" : lang); 434 } 435 436 public String getScript() { 437 return (script == null ? "*" : script); 438 } 439 440 public String getRegion() { 441 return (region == null ? "*" : region); 442 } 443 444 @Override 445 public String toString() { 446 String result = getLanguage(); 447 if (level != Level.language) { 448 result += "-" + getScript(); 449 if (level != Level.script) { 450 result += "-" + getRegion(); 451 } 452 } 453 return result; 454 } 455 456 /* (non-Javadoc) 457 * @see java.lang.Object#equals(java.lang.Object) 458 */ 459 @Override 460 public boolean equals(Object obj) { 461 if (obj == this) { 462 return true; 463 } 464 if (obj == null || !(obj instanceof LocalePatternMatcher)) { 465 return false; 466 } 467 LocalePatternMatcher other = (LocalePatternMatcher) obj; 468 return Utility.objectEquals(level, other.level) 469 && Utility.objectEquals(lang, other.lang) 470 && Utility.objectEquals(script, other.script) 471 && Utility.objectEquals(region, other.region); 472 } 473 474 /* (non-Javadoc) 475 * @see java.lang.Object#hashCode() 476 */ 477 @Override 478 public int hashCode() { 479 return level.ordinal() 480 ^ (lang == null ? 0 : lang.hashCode()) 481 ^ (script == null ? 0 : script.hashCode()) 482 ^ (region == null ? 0 : region.hashCode()); 483 } 484 } 485 486 enum Level { 487 language(0.99), 488 script(0.2), 489 region(0.04); 490 491 final double worst; 492 493 Level(double d) { 494 worst = d; 495 } 496 } 497 498 private static class ScoreData implements Freezable<ScoreData> { 499 @SuppressWarnings("unused") 500 private static final double maxUnequal_changeD_sameS = 0.5; 501 502 @SuppressWarnings("unused") 503 private static final double maxUnequal_changeEqual = 0.75; 504 505 LinkedHashSet<Row.R3<LocalePatternMatcher,LocalePatternMatcher,Double>> scores = new LinkedHashSet<R3<LocalePatternMatcher, LocalePatternMatcher, Double>>(); 506 final Level level; 507 508 public ScoreData(Level level) { 509 this.level = level; 510 } 511 512 void addDataToScores(String desired, String supported, R3<LocalePatternMatcher,LocalePatternMatcher,Double> data) { 513 // Map<String, Set<R3<LocalePatternMatcher,LocalePatternMatcher,Double>>> lang_result = scores.get(desired); 514 // if (lang_result == null) { 515 // scores.put(desired, lang_result = new HashMap()); 516 // } 517 // Set<R3<LocalePatternMatcher,LocalePatternMatcher,Double>> result = lang_result.get(supported); 518 // if (result == null) { 519 // lang_result.put(supported, result = new LinkedHashSet()); 520 // } 521 // result.add(data); 522 boolean added = scores.add(data); 523 if (!added) { 524 throw new ICUException("trying to add duplicate data: " + data); 525 } 526 } 527 528 double getScore(ULocale dMax, String desiredRaw, String desiredMax, 529 ULocale sMax, String supportedRaw, String supportedMax) { 530 double distance = 0; 531 if (!desiredMax.equals(supportedMax)) { 532 distance = getRawScore(dMax, sMax); 533 } else if (!desiredRaw.equals(supportedRaw)) { // maxes are equal, changes are equal 534 distance += 0.001; 535 } 536 return distance; 537 } 538 539 private double getRawScore(ULocale desiredLocale, ULocale supportedLocale) { 540 if (DEBUG) { 541 System.out.println("\t\t\t" + level + " Raw Score:\t" + desiredLocale + ";\t" + supportedLocale); 542 } 543 for (R3<LocalePatternMatcher,LocalePatternMatcher,Double> datum : scores) { // : result 544 if (datum.get0().matches(desiredLocale) 545 && datum.get1().matches(supportedLocale)) { 546 if (DEBUG) { 547 System.out.println("\t\t\t\tFOUND\t" + datum); 548 } 549 return datum.get2(); 550 } 551 } 552 if (DEBUG) { 553 System.out.println("\t\t\t\tNOTFOUND\t" + level.worst); 554 } 555 return level.worst; 556 } 557 558 @Override 559 public String toString() { 560 StringBuilder result = new StringBuilder().append(level); 561 for (R3<LocalePatternMatcher, LocalePatternMatcher, Double> score : scores) { 562 result.append("\n\t\t").append(score); 563 } 564 return result.toString(); 565 } 566 567 568 @Override 569 @SuppressWarnings("unchecked") 570 public ScoreData cloneAsThawed() { 571 try { 572 ScoreData result = (ScoreData) clone(); 573 result.scores = (LinkedHashSet<R3<LocalePatternMatcher, LocalePatternMatcher, Double>>) result.scores.clone(); 574 result.frozen = false; 575 return result; 576 } catch (CloneNotSupportedException e) { 577 throw new ICUCloneNotSupportedException(e); // will never happen 578 } 579 580 } 581 582 private volatile boolean frozen = false; 583 584 @Override 585 public ScoreData freeze() { 586 return this; 587 } 588 589 @Override 590 public boolean isFrozen() { 591 return frozen; 592 } 593 594 public Relation<String,String> getMatchingLanguages() { 595 Relation<String,String> desiredToSupported = Relation.of(new LinkedHashMap<String,Set<String>>(), HashSet.class); 596 for (R3<LocalePatternMatcher, LocalePatternMatcher, Double> item : scores) { 597 LocalePatternMatcher desired = item.get0(); 598 LocalePatternMatcher supported = item.get1(); 599 if (desired.lang != null && supported.lang != null) { // explicitly mentioned languages must have reasonable distance 600 desiredToSupported.put(desired.lang, supported.lang); 601 } 602 } 603 desiredToSupported.freeze(); 604 return desiredToSupported; 605 } 606 } 607 608 /** 609 * Only for testing and use by tools. Interface may change!! 610 * @deprecated This API is ICU internal only. 611 * @hide draft / provisional / internal are hidden on Android 612 */ 613 @Deprecated 614 public static class LanguageMatcherData implements Freezable<LanguageMatcherData> { 615 private ScoreData languageScores = new ScoreData(Level.language); 616 private ScoreData scriptScores = new ScoreData(Level.script); 617 private ScoreData regionScores = new ScoreData(Level.region); 618 private Relation<String, String> matchingLanguages; 619 private volatile boolean frozen = false; 620 621 622 /** 623 * @deprecated This API is ICU internal only. 624 * @hide draft / provisional / internal are hidden on Android 625 */ 626 @Deprecated 627 public LanguageMatcherData() { 628 } 629 630 /** 631 * @deprecated This API is ICU internal only. 632 * @hide draft / provisional / internal are hidden on Android 633 */ 634 @Deprecated 635 public Relation<String, String> matchingLanguages() { 636 return matchingLanguages; 637 } 638 639 /** 640 * @deprecated This API is ICU internal only. 641 * @hide draft / provisional / internal are hidden on Android 642 */ 643 @Override 644 @Deprecated 645 public String toString() { 646 return languageScores + "\n\t" + scriptScores + "\n\t" + regionScores; 647 } 648 649 /** 650 * @deprecated This API is ICU internal only. 651 * @hide draft / provisional / internal are hidden on Android 652 */ 653 @Deprecated 654 public double match(ULocale a, ULocale aMax, ULocale b, ULocale bMax) { 655 double diff = 0; 656 diff += languageScores.getScore(aMax, a.getLanguage(), aMax.getLanguage(), bMax, b.getLanguage(), bMax.getLanguage()); 657 if (diff > 0.999d) { // with no language match, we bail 658 return 0.0d; 659 } 660 diff += scriptScores.getScore(aMax, a.getScript(), aMax.getScript(), bMax, b.getScript(), bMax.getScript()); 661 diff += regionScores.getScore(aMax, a.getCountry(), aMax.getCountry(), bMax, b.getCountry(), bMax.getCountry()); 662 663 if (!a.getVariant().equals(b.getVariant())) { 664 diff += 0.01; 665 } 666 if (diff < 0.0d) { 667 diff = 0.0d; 668 } else if (diff > 1.0d) { 669 diff = 1.0d; 670 } 671 if (DEBUG) { 672 System.out.println("\t\t\tTotal Distance\t" + diff); 673 } 674 return 1.0 - diff; 675 } 676 677 /** 678 * @deprecated This API is ICU internal only. 679 * @hide draft / provisional / internal are hidden on Android 680 */ 681 @Deprecated 682 public LanguageMatcherData addDistance(String desired, String supported, int percent, String comment) { 683 return addDistance(desired, supported, percent, false, comment); 684 } 685 /** 686 * @deprecated This API is ICU internal only. 687 * @hide draft / provisional / internal are hidden on Android 688 */ 689 @Deprecated 690 public LanguageMatcherData addDistance(String desired, String supported, int percent, boolean oneway) { 691 return addDistance(desired, supported, percent, oneway, null); 692 } 693 694 private LanguageMatcherData addDistance(String desired, String supported, int percent, boolean oneway, String comment) { 695 if (DEBUG) { 696 System.out.println("\t<languageMatch desired=\"" + desired + "\"" + 697 " supported=\"" + supported + "\"" + 698 " percent=\"" + percent + "\"" 699 + (oneway ? " oneway=\"true\"" : "") 700 + "/>" 701 + (comment == null ? "" : "\t<!-- " + comment + " -->")); 702 // // .addDistance("nn", "nb", 4, true) 703 // System.out.println(".addDistance(\"" + desired + "\"" + 704 // ", \"" + supported + "\"" + 705 // ", " + percent + "" 706 // + (oneway ? "" : ", true") 707 // + (comment == null ? "" : ", \"" + comment + "\"") 708 // + ")" 709 // ); 710 711 } 712 double score = 1-percent/100.0; // convert from percentage 713 LocalePatternMatcher desiredMatcher = new LocalePatternMatcher(desired); 714 Level desiredLen = desiredMatcher.getLevel(); 715 LocalePatternMatcher supportedMatcher = new LocalePatternMatcher(supported); 716 Level supportedLen = supportedMatcher.getLevel(); 717 if (desiredLen != supportedLen) { 718 throw new IllegalArgumentException("Lengths unequal: " + desired + ", " + supported); 719 } 720 R3<LocalePatternMatcher,LocalePatternMatcher,Double> data = Row.of(desiredMatcher, supportedMatcher, score); 721 R3<LocalePatternMatcher,LocalePatternMatcher,Double> data2 = oneway ? null : Row.of(supportedMatcher, desiredMatcher, score); 722 boolean desiredEqualsSupported = desiredMatcher.equals(supportedMatcher); 723 switch (desiredLen) { 724 case language: 725 String dlanguage = desiredMatcher.getLanguage(); 726 String slanguage = supportedMatcher.getLanguage(); 727 languageScores.addDataToScores(dlanguage, slanguage, data); 728 if (!oneway && !desiredEqualsSupported) { 729 languageScores.addDataToScores(slanguage, dlanguage, data2); 730 } 731 break; 732 case script: 733 String dscript = desiredMatcher.getScript(); 734 String sscript = supportedMatcher.getScript(); 735 scriptScores.addDataToScores(dscript, sscript, data); 736 if (!oneway && !desiredEqualsSupported) { 737 scriptScores.addDataToScores(sscript, dscript, data2); 738 } 739 break; 740 case region: 741 String dregion = desiredMatcher.getRegion(); 742 String sregion = supportedMatcher.getRegion(); 743 regionScores.addDataToScores(dregion, sregion, data); 744 if (!oneway && !desiredEqualsSupported) { 745 regionScores.addDataToScores(sregion, dregion, data2); 746 } 747 break; 748 } 749 return this; 750 } 751 752 /** 753 * {@inheritDoc} 754 * @deprecated This API is ICU internal only. 755 * @hide draft / provisional / internal are hidden on Android 756 */ 757 @Override 758 @Deprecated 759 public LanguageMatcherData cloneAsThawed() { 760 LanguageMatcherData result; 761 try { 762 result = (LanguageMatcherData) clone(); 763 result.languageScores = languageScores.cloneAsThawed(); 764 result.scriptScores = scriptScores.cloneAsThawed(); 765 result.regionScores = regionScores.cloneAsThawed(); 766 result.frozen = false; 767 return result; 768 } catch (CloneNotSupportedException e) { 769 throw new ICUCloneNotSupportedException(e); // will never happen 770 } 771 } 772 773 /** 774 * {@inheritDoc} 775 * @deprecated This API is ICU internal only. 776 * @hide draft / provisional / internal are hidden on Android 777 */ 778 @Override 779 @Deprecated 780 public LanguageMatcherData freeze() { 781 languageScores.freeze(); 782 regionScores.freeze(); 783 scriptScores.freeze(); 784 matchingLanguages = languageScores.getMatchingLanguages(); 785 frozen = true; 786 return this; 787 } 788 789 /** 790 * {@inheritDoc} 791 * @deprecated This API is ICU internal only. 792 * @hide draft / provisional / internal are hidden on Android 793 */ 794 @Override 795 @Deprecated 796 public boolean isFrozen() { 797 return frozen; 798 } 799 } 800 801 LanguageMatcherData matcherData; 802 LocalePriorityList languagePriorityList; 803 804 private static final LanguageMatcherData defaultWritten; 805 806 private static HashMap<String,String> canonicalMap = new HashMap<String, String>(); 807 808 809 static { 810 canonicalMap.put("iw", "he"); 811 canonicalMap.put("mo", "ro"); 812 canonicalMap.put("tl", "fil"); 813 814 ICUResourceBundle suppData = getICUSupplementalData(); 815 ICUResourceBundle languageMatching = suppData.findTopLevel("languageMatching"); 816 ICUResourceBundle written = (ICUResourceBundle) languageMatching.get("written"); 817 defaultWritten = new LanguageMatcherData(); 818 819 for(UResourceBundleIterator iter = written.getIterator(); iter.hasNext();) { 820 ICUResourceBundle item = (ICUResourceBundle) iter.next(); 821 /* 822 "*_*_*", 823 "*_*_*", 824 "96", 825 */ 826 // <languageMatch desired="gsw" supported="de" percent="96" oneway="true" /> 827 boolean oneway = item.getSize() > 3 && "1".equals(item.getString(3)); 828 defaultWritten.addDistance(item.getString(0), item.getString(1), Integer.parseInt(item.getString(2)), oneway); 829 } 830 defaultWritten.freeze(); 831 } 832 833 /** 834 * @deprecated This API is ICU internal only. 835 * @hide draft / provisional / internal are hidden on Android 836 */ 837 @Deprecated 838 public static ICUResourceBundle getICUSupplementalData() { 839 ICUResourceBundle suppData = (ICUResourceBundle) UResourceBundle.getBundleInstance( 840 ICUData.ICU_BASE_NAME, 841 "supplementalData", 842 ICUResourceBundle.ICU_DATA_CLASS_LOADER); 843 return suppData; 844 } 845 846 /** 847 * @deprecated This API is ICU internal only. 848 * @hide draft / provisional / internal are hidden on Android 849 */ 850 @Deprecated 851 public static double match(ULocale a, ULocale b) { 852 final LocaleMatcher matcher = new LocaleMatcher(""); 853 return matcher.match(a, matcher.addLikelySubtags(a), b, matcher.addLikelySubtags(b)); 854 } 855 856 transient XLocaleMatcher xLocaleMatcher = null; 857 transient ULocale xDefaultLanguage = null; 858 transient boolean xFavorScript = false; 859 860 /** 861 * Returns the distance between the two languages, using the new CLDR syntax (see getBestMatch). 862 * The values are not necessarily symmetric. 863 * @param desired A locale desired by the user 864 * @param supported A locale supported by a program. 865 * @return A return of 0 is a complete match, and 100 is a complete mismatch (above the thresholdDistance). 866 * A language is first maximized with add likely subtags, then compared. 867 * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release. 868 * @hide draft / provisional / internal are hidden on Android 869 */ 870 @Deprecated 871 public int distance(ULocale desired, ULocale supported) { 872 return getLocaleMatcher().distance(desired, supported); 873 } 874 875 private synchronized XLocaleMatcher getLocaleMatcher() { 876 if (xLocaleMatcher == null) { 877 Builder builder = XLocaleMatcher.builder(); 878 builder.setSupportedLocales(languagePriorityList); 879 if (xDefaultLanguage != null) { 880 builder.setDefaultLanguage(xDefaultLanguage); 881 } 882 if (xFavorScript) { 883 builder.setDistanceOption(DistanceOption.SCRIPT_FIRST); 884 } 885 xLocaleMatcher = builder.build(); 886 } 887 return xLocaleMatcher; 888 } 889 890 /** 891 * Get the best match between the desired languages and supported languages 892 * This supports the new CLDR syntax to provide for better matches within 893 * regional clusters (such as maghreb Arabic vs non-maghreb Arabic, or regions that use en-GB vs en-US) 894 * and also matching between regions and macroregions, such as comparing es-419 to es-AR). 895 * @param desiredLanguages Typically the supplied user's languages, in order of preference, with best first. 896 * @param outputBestDesired The one of the desired languages that matched best. 897 * Set to null if the best match was not below the threshold distance. 898 * @return best-match supported language 899 * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release. 900 * @hide draft / provisional / internal are hidden on Android 901 */ 902 @Deprecated 903 public ULocale getBestMatch(LinkedHashSet<ULocale> desiredLanguages, Output<ULocale> outputBestDesired) { 904 return getLocaleMatcher().getBestMatch(desiredLanguages, outputBestDesired); 905 } 906 907 /** 908 * Set the default language, with null = default = first supported language 909 * @param defaultLanguage Language to use in case the threshold for distance is exceeded. 910 * @return this, for chaining 911 * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release. 912 * @hide draft / provisional / internal are hidden on Android 913 */ 914 @Deprecated 915 public synchronized LocaleMatcher setDefaultLanguage(ULocale defaultLanguage) { 916 this.xDefaultLanguage = defaultLanguage; 917 xLocaleMatcher = null; 918 return this; 919 } 920 921 /** 922 * If true, then the language differences are smaller than than script differences. 923 * This is used in situations (such as maps) where it is better to fall back to the same script than a similar language. 924 * @param favorScript Set to true to treat script as most important. 925 * @return this, for chaining. 926 * @deprecated ICU 59: This API is a technical preview. It may change in an upcoming release. 927 * @hide draft / provisional / internal are hidden on Android 928 */ 929 @Deprecated 930 public synchronized LocaleMatcher setFavorScript(boolean favorScript) { 931 this.xFavorScript = favorScript; 932 xLocaleMatcher = null; 933 return this; 934 } 935 } 936