1 package org.unicode.cldr.draft; 2 3 import java.util.ArrayList; 4 import java.util.Collection; 5 import java.util.Collections; 6 import java.util.HashMap; 7 import java.util.HashSet; 8 import java.util.LinkedHashMap; 9 import java.util.LinkedHashSet; 10 import java.util.List; 11 import java.util.Map; 12 import java.util.Map.Entry; 13 import java.util.Set; 14 import java.util.TreeMap; 15 import java.util.TreeSet; 16 17 import org.unicode.cldr.draft.XLikelySubtags.LSR; 18 import org.unicode.cldr.draft.XLocaleDistance.RegionMapper.Builder; 19 import org.unicode.cldr.util.CLDRConfig; 20 import org.unicode.cldr.util.CLDRFile; 21 import org.unicode.cldr.util.SupplementalDataInfo; 22 23 import com.google.common.base.Joiner; 24 import com.google.common.base.Objects; 25 import com.google.common.base.Predicate; 26 import com.google.common.base.Splitter; 27 import com.google.common.collect.ArrayListMultimap; 28 import com.google.common.collect.ImmutableMap; 29 import com.google.common.collect.ImmutableMultimap; 30 import com.google.common.collect.ImmutableSet; 31 import com.google.common.collect.Multimap; 32 import com.google.common.collect.Multimaps; 33 import com.google.common.collect.TreeMultimap; 34 import com.ibm.icu.impl.Row; 35 import com.ibm.icu.impl.Row.R4; 36 import com.ibm.icu.util.Output; 37 import com.ibm.icu.util.ULocale; 38 39 public class XLocaleDistance { 40 41 static final boolean PRINT_OVERRIDES = true; 42 43 public static final int ABOVE_THRESHOLD = 100; 44 45 @Deprecated 46 public static final String ANY = "�"; // matches any character. Uses value above any subtag. 47 fixAny(String string)48 private static String fixAny(String string) { 49 return "*".equals(string) ? ANY : string; 50 } 51 52 // For now, get data directly from CLDR 53 54 private static final CLDRConfig CONFIG = CLDRConfig.getInstance(); 55 private static final CLDRFile english = CONFIG.getEnglish(); 56 static final SupplementalDataInfo SDI = CONFIG.getSupplementalDataInfo(); 57 xGetLanguageMatcherData()58 private static List<R4<String, String, Integer, Boolean>> xGetLanguageMatcherData() { 59 return SDI.getLanguageMatcherData("written_new"); 60 } 61 62 static final Multimap<String, String> CONTAINER_TO_CONTAINED; 63 static final Multimap<String, String> CONTAINER_TO_CONTAINED_FINAL; 64 static { 65 TreeMultimap<String, String> containerToContainedTemp = TreeMultimap.create(); 66 fill("001", containerToContainedTemp); 67 CONTAINER_TO_CONTAINED = ImmutableMultimap.copyOf(containerToContainedTemp); 68 // get hard-coded data because ICU fails to copy information 69 70 for (String container : SDI.getContainers()) { 71 Set<String> contained = SDI.getContained(container); 72 System.out.println(".putAll(\"" + container + "\", \"" + Joiner.on("\", \"") 73 .join(contained) + "\")"); 74 } 75 76 ImmutableMultimap.Builder<String, String> containerToFinalContainedBuilder = new ImmutableMultimap.Builder<>(); 77 for (Entry<String, Collection<String>> entry : CONTAINER_TO_CONTAINED.asMap().entrySet()) { 78 String container = entry.getKey(); 79 for (String contained : entry.getValue()) { 80 if (SDI.getContained(contained) == null) { containerToFinalContainedBuilder.put(container, contained)81 containerToFinalContainedBuilder.put(container, contained); 82 } 83 } 84 } 85 CONTAINER_TO_CONTAINED_FINAL = containerToFinalContainedBuilder.build(); 86 } 87 88 // TODO make this a single pass fill(String region, Multimap<String, String> toAddTo)89 private static Collection<String> fill(String region, Multimap<String, String> toAddTo) { 90 Set<String> contained = SDI.getContained(region); 91 if (contained != null) { 92 toAddTo.putAll(region, contained); 93 for (String subregion : contained) { 94 toAddTo.putAll(region, fill(subregion, toAddTo)); 95 } 96 return toAddTo.get(region); 97 } 98 return Collections.emptySet(); 99 } 100 101 final static private Set<String> ALL_FINAL_REGIONS = ImmutableSet.copyOf(CONTAINER_TO_CONTAINED_FINAL.get("001")); 102 103 // end of data from CLDR 104 105 private final DistanceTable languageDesired2Supported; 106 private final RegionMapper regionMapper; 107 private final int defaultLanguageDistance; 108 private final int defaultScriptDistance; 109 private final int defaultRegionDistance; 110 111 @Deprecated 112 public static abstract class DistanceTable { getDistance(String desiredLang, String supportedlang, Output<DistanceTable> table, boolean starEquals)113 abstract int getDistance(String desiredLang, String supportedlang, Output<DistanceTable> table, boolean starEquals); 114 getCloser(int threshold)115 abstract Set<String> getCloser(int threshold); 116 toString(boolean abbreviate)117 abstract String toString(boolean abbreviate); 118 compact()119 public DistanceTable compact() { 120 return this; 121 } 122 123 // public Integer getInternalDistance(String a, String b) { 124 // return null; 125 // } getInternalNode(String any, String any2)126 public DistanceNode getInternalNode(String any, String any2) { 127 return null; 128 } 129 getInternalMatches()130 public Map<String, Set<String>> getInternalMatches() { 131 return null; 132 } 133 isEmpty()134 public boolean isEmpty() { 135 return true; 136 } 137 } 138 139 @Deprecated 140 public static class DistanceNode { 141 final int distance; 142 DistanceNode(int distance)143 public DistanceNode(int distance) { 144 this.distance = distance; 145 } 146 getDistanceTable()147 public DistanceTable getDistanceTable() { 148 return null; 149 } 150 151 @Override equals(Object obj)152 public boolean equals(Object obj) { 153 if (!(obj instanceof DistanceNode)) { 154 return false; 155 } 156 DistanceNode other = (DistanceNode) obj; 157 return distance == other.distance; 158 } 159 160 @Override hashCode()161 public int hashCode() { 162 return distance; 163 } 164 165 @Override toString()166 public String toString() { 167 return "\ndistance: " + distance; 168 } 169 } 170 171 private interface IdMapper<K, V> { toId(K source)172 public V toId(K source); 173 } 174 175 static class IdMakerFull<T> implements IdMapper<T, Integer> { 176 private final Map<T, Integer> objectToInt = new HashMap<>(); 177 private final List<T> intToObject = new ArrayList<>(); 178 final String name; // for debugging 179 IdMakerFull(String name)180 IdMakerFull(String name) { 181 this.name = name; 182 } 183 IdMakerFull()184 IdMakerFull() { 185 this("unnamed"); 186 } 187 IdMakerFull(String name, T zeroValue)188 IdMakerFull(String name, T zeroValue) { 189 this(name); 190 add(zeroValue); 191 } 192 193 /** 194 * Return an id, making one if there wasn't one already. 195 */ add(T source)196 public Integer add(T source) { 197 Integer result = objectToInt.get(source); 198 if (result == null) { 199 Integer newResult = intToObject.size(); 200 objectToInt.put(source, newResult); 201 intToObject.add(source); 202 return newResult; 203 } else { 204 return result; 205 } 206 } 207 208 /** 209 * Return an id, or null if there is none. 210 */ 211 @Override toId(T source)212 public Integer toId(T source) { 213 return objectToInt.get(source); 214 // return value == null ? 0 : value; 215 } 216 217 /** 218 * Return the object for the id, or null if there is none. 219 */ fromId(int id)220 public T fromId(int id) { 221 return intToObject.get(id); 222 } 223 224 /** 225 * Return interned object 226 */ intern(T source)227 public T intern(T source) { 228 return fromId(add(source)); 229 } 230 size()231 public int size() { 232 return intToObject.size(); 233 } 234 235 /** 236 * Same as add, except if the object didn't have an id, return null; 237 */ getOldAndAdd(T source)238 public Integer getOldAndAdd(T source) { 239 Integer result = objectToInt.get(source); 240 if (result == null) { 241 Integer newResult = intToObject.size(); 242 objectToInt.put(source, newResult); 243 intToObject.add(source); 244 } 245 return result; 246 } 247 248 @Override toString()249 public String toString() { 250 return size() + ": " + intToObject; 251 } 252 253 @Override equals(Object obj)254 public boolean equals(Object obj) { 255 if (!(obj instanceof IdMakerFull)) { 256 return false; 257 } 258 IdMakerFull<T> other = (IdMakerFull) obj; 259 return intToObject.equals(other.intToObject); 260 } 261 262 @Override hashCode()263 public int hashCode() { 264 return intToObject.hashCode(); 265 } 266 } 267 268 static class StringDistanceNode extends DistanceNode { 269 final DistanceTable distanceTable; 270 StringDistanceNode(int distance, DistanceTable distanceTable)271 public StringDistanceNode(int distance, DistanceTable distanceTable) { 272 super(distance); 273 this.distanceTable = distanceTable; 274 } 275 276 @Override equals(Object obj)277 public boolean equals(Object obj) { 278 if (!(obj instanceof StringDistanceNode)) { 279 return false; 280 } 281 StringDistanceNode other = (StringDistanceNode) obj; 282 return distance == other.distance && Objects.equal(distanceTable, other.distanceTable); 283 } 284 285 @Override hashCode()286 public int hashCode() { 287 return distance ^ Objects.hashCode(distanceTable); 288 } 289 StringDistanceNode(int distance)290 StringDistanceNode(int distance) { 291 this(distance, new StringDistanceTable()); 292 } 293 addSubtables(String desiredSub, String supportedSub, CopyIfEmpty r)294 public void addSubtables(String desiredSub, String supportedSub, CopyIfEmpty r) { 295 ((StringDistanceTable) distanceTable).addSubtables(desiredSub, supportedSub, r); 296 } 297 298 @Override toString()299 public String toString() { 300 return "distance: " + distance + "\n" + distanceTable; 301 } 302 copyTables(StringDistanceTable value)303 public void copyTables(StringDistanceTable value) { 304 if (value != null) { 305 ((StringDistanceTable) distanceTable).copy(value); 306 } 307 } 308 309 @Override getDistanceTable()310 public DistanceTable getDistanceTable() { 311 return distanceTable; 312 } 313 } 314 XLocaleDistance(DistanceTable datadistancetable2, RegionMapper regionMapper)315 public XLocaleDistance(DistanceTable datadistancetable2, RegionMapper regionMapper) { 316 languageDesired2Supported = datadistancetable2; 317 this.regionMapper = regionMapper; 318 319 StringDistanceNode languageNode = (StringDistanceNode) ((StringDistanceTable) languageDesired2Supported).subtables.get(ANY).get(ANY); 320 defaultLanguageDistance = languageNode.distance; 321 StringDistanceNode scriptNode = (StringDistanceNode) ((StringDistanceTable) languageNode.distanceTable).subtables.get(ANY).get(ANY); 322 defaultScriptDistance = scriptNode.distance; 323 DistanceNode regionNode = ((StringDistanceTable) scriptNode.distanceTable).subtables.get(ANY).get(ANY); 324 defaultRegionDistance = regionNode.distance; 325 } 326 newMap()327 private static Map newMap() { // for debugging 328 return new TreeMap(); 329 } 330 331 /** 332 * Internal class 333 */ 334 @Deprecated 335 public static class StringDistanceTable extends DistanceTable { 336 final Map<String, Map<String, DistanceNode>> subtables; 337 StringDistanceTable(Map<String, Map<String, DistanceNode>> tables)338 StringDistanceTable(Map<String, Map<String, DistanceNode>> tables) { 339 subtables = tables; 340 } 341 StringDistanceTable()342 StringDistanceTable() { 343 this(newMap()); 344 } 345 346 @Override isEmpty()347 public boolean isEmpty() { 348 return subtables.isEmpty(); 349 } 350 351 @Override equals(Object obj)352 public boolean equals(Object obj) { 353 if (!(obj instanceof StringDistanceTable)) { 354 return false; 355 } 356 StringDistanceTable other = (StringDistanceTable) obj; 357 return subtables.equals(other.subtables); 358 } 359 360 @Override hashCode()361 public int hashCode() { 362 return subtables.hashCode(); 363 } 364 365 @Override getDistance(String desired, String supported, Output<DistanceTable> distanceTable, boolean starEquals)366 public int getDistance(String desired, String supported, Output<DistanceTable> distanceTable, boolean starEquals) { 367 boolean star = false; 368 Map<String, DistanceNode> sub2 = subtables.get(desired); 369 if (sub2 == null) { 370 sub2 = subtables.get(ANY); // <*, supported> 371 star = true; 372 } 373 DistanceNode value = sub2.get(supported); // <*/desired, supported> 374 if (value == null) { 375 value = sub2.get(ANY); // <*/desired, *> 376 if (value == null && !star) { 377 sub2 = subtables.get(ANY); // <*, supported> 378 value = sub2.get(supported); 379 if (value == null) { 380 value = sub2.get(ANY); // <*, *> 381 } 382 } 383 star = true; 384 } 385 if (distanceTable != null) { 386 distanceTable.value = ((StringDistanceNode) value).distanceTable; 387 } 388 return starEquals && star && desired.equals(supported) ? 0 : value.distance; 389 } 390 copy(StringDistanceTable other)391 public void copy(StringDistanceTable other) { 392 for (Entry<String, Map<String, DistanceNode>> e1 : other.subtables.entrySet()) { 393 for (Entry<String, DistanceNode> e2 : e1.getValue().entrySet()) { 394 DistanceNode value = e2.getValue(); 395 DistanceNode subNode = addSubtable(e1.getKey(), e2.getKey(), value.distance); 396 } 397 } 398 } 399 addSubtable(String desired, String supported, int distance)400 DistanceNode addSubtable(String desired, String supported, int distance) { 401 Map<String, DistanceNode> sub2 = subtables.get(desired); 402 if (sub2 == null) { 403 subtables.put(desired, sub2 = newMap()); 404 } 405 DistanceNode oldNode = sub2.get(supported); 406 if (oldNode != null) { 407 return oldNode; 408 } 409 410 final StringDistanceNode newNode = new StringDistanceNode(distance); 411 sub2.put(supported, newNode); 412 return newNode; 413 } 414 415 /** 416 * Return null if value doesn't exist 417 */ getNode(String desired, String supported)418 private DistanceNode getNode(String desired, String supported) { 419 Map<String, DistanceNode> sub2 = subtables.get(desired); 420 if (sub2 == null) { 421 return null; 422 } 423 return sub2.get(supported); 424 } 425 426 /** add table for each subitem that matches and doesn't have a table already 427 */ addSubtables( String desired, String supported, Predicate<DistanceNode> action)428 public void addSubtables( 429 String desired, String supported, 430 Predicate<DistanceNode> action) { 431 int count = 0; 432 DistanceNode node = getNode(desired, supported); 433 if (node == null) { 434 // get the distance it would have 435 Output<DistanceTable> node2 = new Output<>(); 436 int distance = getDistance(desired, supported, node2, true); 437 // now add it 438 node = addSubtable(desired, supported, distance); 439 if (node2.value != null) { 440 ((StringDistanceNode) node).copyTables((StringDistanceTable) (node2.value)); 441 } 442 } 443 action.apply(node); 444 } 445 addSubtables(String desiredLang, String supportedLang, String desiredScript, String supportedScript, int percentage)446 public void addSubtables(String desiredLang, String supportedLang, 447 String desiredScript, String supportedScript, 448 int percentage) { 449 450 // add to all the values that have the matching desiredLang and supportedLang 451 boolean haveKeys = false; 452 for (Entry<String, Map<String, DistanceNode>> e1 : subtables.entrySet()) { 453 String key1 = e1.getKey(); 454 final boolean desiredIsKey = desiredLang.equals(key1); 455 if (desiredIsKey || desiredLang.equals(ANY)) { 456 for (Entry<String, DistanceNode> e2 : e1.getValue().entrySet()) { 457 String key2 = e2.getKey(); 458 final boolean supportedIsKey = supportedLang.equals(key2); 459 haveKeys |= (desiredIsKey && supportedIsKey); 460 if (supportedIsKey || supportedLang.equals(ANY)) { 461 DistanceNode value = e2.getValue(); 462 ((StringDistanceTable) value.getDistanceTable()).addSubtable(desiredScript, supportedScript, percentage); 463 } 464 } 465 } 466 } 467 // now add the sequence explicitly 468 StringDistanceTable dt = new StringDistanceTable(); 469 dt.addSubtable(desiredScript, supportedScript, percentage); 470 CopyIfEmpty r = new CopyIfEmpty(dt); 471 addSubtables(desiredLang, supportedLang, r); 472 } 473 addSubtables(String desiredLang, String supportedLang, String desiredScript, String supportedScript, String desiredRegion, String supportedRegion, int percentage)474 public void addSubtables(String desiredLang, String supportedLang, 475 String desiredScript, String supportedScript, 476 String desiredRegion, String supportedRegion, 477 int percentage) { 478 479 // add to all the values that have the matching desiredLang and supportedLang 480 boolean haveKeys = false; 481 for (Entry<String, Map<String, DistanceNode>> e1 : subtables.entrySet()) { 482 String key1 = e1.getKey(); 483 final boolean desiredIsKey = desiredLang.equals(key1); 484 if (desiredIsKey || desiredLang.equals(ANY)) { 485 for (Entry<String, DistanceNode> e2 : e1.getValue().entrySet()) { 486 String key2 = e2.getKey(); 487 final boolean supportedIsKey = supportedLang.equals(key2); 488 haveKeys |= (desiredIsKey && supportedIsKey); 489 if (supportedIsKey || supportedLang.equals(ANY)) { 490 StringDistanceNode value = (StringDistanceNode) e2.getValue(); 491 ((StringDistanceTable) value.distanceTable).addSubtables(desiredScript, supportedScript, desiredRegion, supportedRegion, 492 percentage); 493 } 494 } 495 } 496 } 497 // now add the sequence explicitly 498 499 StringDistanceTable dt = new StringDistanceTable(); 500 dt.addSubtable(desiredRegion, supportedRegion, percentage); 501 AddSub r = new AddSub(desiredScript, supportedScript, dt); 502 addSubtables(desiredLang, supportedLang, r); 503 } 504 505 @Override toString()506 public String toString() { 507 return toString(false); 508 } 509 510 @Override toString(boolean abbreviate)511 public String toString(boolean abbreviate) { 512 return toString(abbreviate, "", new IdMakerFull<>("interner"), new StringBuilder()).toString(); 513 } 514 toString(boolean abbreviate, String indent, IdMakerFull<Object> intern, StringBuilder buffer)515 public StringBuilder toString(boolean abbreviate, String indent, IdMakerFull<Object> intern, StringBuilder buffer) { 516 String indent2 = indent.isEmpty() ? "" : "\t"; 517 Integer id = abbreviate ? intern.getOldAndAdd(subtables) : null; 518 if (id != null) { 519 buffer.append(indent2).append('#').append(id).append('\n'); 520 } else 521 for (Entry<String, Map<String, DistanceNode>> e1 : subtables.entrySet()) { 522 final Map<String, DistanceNode> subsubtable = e1.getValue(); 523 buffer.append(indent2).append(e1.getKey()); 524 String indent3 = "\t"; 525 id = abbreviate ? intern.getOldAndAdd(subsubtable) : null; 526 if (id != null) { 527 buffer.append(indent3).append('#').append(id).append('\n'); 528 } else 529 for (Entry<String, DistanceNode> e2 : subsubtable.entrySet()) { 530 DistanceNode value = e2.getValue(); 531 buffer.append(indent3).append(e2.getKey()); 532 id = abbreviate ? intern.getOldAndAdd(value) : null; 533 if (id != null) { 534 buffer.append('\t').append('#').append(id).append('\n'); 535 } else { 536 buffer.append('\t').append(value.distance); 537 final DistanceTable distanceTable = value.getDistanceTable(); 538 if (distanceTable != null) { 539 id = abbreviate ? intern.getOldAndAdd(distanceTable) : null; 540 if (id != null) { 541 buffer.append('\t').append('#').append(id).append('\n'); 542 } else { 543 ((StringDistanceTable) distanceTable).toString(abbreviate, indent + "\t\t\t", intern, buffer); 544 } 545 } else { 546 buffer.append('\n'); 547 } 548 } 549 indent3 = indent + '\t'; 550 } 551 indent2 = indent; 552 } 553 return buffer; 554 } 555 556 @Override compact()557 public StringDistanceTable compact() { 558 return new CompactAndImmutablizer().compact(this); 559 } 560 561 @Override getCloser(int threshold)562 public Set<String> getCloser(int threshold) { 563 Set<String> result = new HashSet<>(); 564 for (Entry<String, Map<String, DistanceNode>> e1 : subtables.entrySet()) { 565 String desired = e1.getKey(); 566 for (Entry<String, DistanceNode> e2 : e1.getValue().entrySet()) { 567 if (e2.getValue().distance < threshold) { 568 result.add(desired); 569 break; 570 } 571 } 572 } 573 return result; 574 } 575 getInternalDistance(String a, String b)576 public Integer getInternalDistance(String a, String b) { 577 Map<String, DistanceNode> subsub = subtables.get(a); 578 if (subsub == null) { 579 return null; 580 } 581 DistanceNode dnode = subsub.get(b); 582 return dnode == null ? null : dnode.distance; 583 } 584 585 @Override getInternalNode(String a, String b)586 public DistanceNode getInternalNode(String a, String b) { 587 Map<String, DistanceNode> subsub = subtables.get(a); 588 if (subsub == null) { 589 return null; 590 } 591 return subsub.get(b); 592 } 593 594 @Override getInternalMatches()595 public Map<String, Set<String>> getInternalMatches() { 596 Map<String, Set<String>> result = new LinkedHashMap<>(); 597 for (Entry<String, Map<String, DistanceNode>> entry : subtables.entrySet()) { 598 result.put(entry.getKey(), new LinkedHashSet<>(entry.getValue().keySet())); 599 } 600 return result; 601 } 602 } 603 604 static class CopyIfEmpty implements Predicate<DistanceNode> { 605 private final StringDistanceTable toCopy; 606 CopyIfEmpty(StringDistanceTable resetIfNotNull)607 CopyIfEmpty(StringDistanceTable resetIfNotNull) { 608 this.toCopy = resetIfNotNull; 609 } 610 611 @Override apply(DistanceNode node)612 public boolean apply(DistanceNode node) { 613 final StringDistanceTable subtables = (StringDistanceTable) node.getDistanceTable(); 614 if (subtables.subtables.isEmpty()) { 615 subtables.copy(toCopy); 616 } 617 return true; 618 } 619 } 620 621 static class AddSub implements Predicate<DistanceNode> { 622 private final String desiredSub; 623 private final String supportedSub; 624 private final CopyIfEmpty r; 625 AddSub(String desiredSub, String supportedSub, StringDistanceTable distanceTableToCopy)626 AddSub(String desiredSub, String supportedSub, StringDistanceTable distanceTableToCopy) { 627 this.r = new CopyIfEmpty(distanceTableToCopy); 628 this.desiredSub = desiredSub; 629 this.supportedSub = supportedSub; 630 } 631 632 @Override apply(DistanceNode node)633 public boolean apply(DistanceNode node) { 634 if (node == null) { 635 throw new IllegalArgumentException("bad structure"); 636 } else { 637 ((StringDistanceNode) node).addSubtables(desiredSub, supportedSub, r); 638 } 639 return true; 640 } 641 } 642 distance(ULocale desired, ULocale supported, int threshold, DistanceOption distanceOption)643 public int distance(ULocale desired, ULocale supported, int threshold, DistanceOption distanceOption) { 644 LSR supportedLSR = LSR.fromMaximalized(supported); 645 LSR desiredLSR = LSR.fromMaximalized(desired); 646 return distanceRaw(desiredLSR, supportedLSR, threshold, distanceOption); 647 } 648 649 /** 650 * Returns distance, from 0 to ABOVE_THRESHOLD. 651 * ULocales must be in canonical, addLikelySubtags format. Returns distance 652 * @param desired 653 * @param supported 654 * @param distanceOption 655 * @return 656 */ distanceRaw(LSR desired, LSR supported, int threshold, DistanceOption distanceOption)657 public int distanceRaw(LSR desired, LSR supported, int threshold, DistanceOption distanceOption) { 658 return distanceRaw(desired.language, supported.language, 659 desired.script, supported.script, 660 desired.region, supported.region, 661 threshold, distanceOption); 662 } 663 664 public enum DistanceOption { 665 NORMAL, SCRIPT_FIRST 666 } 667 668 /** 669 * Returns distance, from 0 to ABOVE_THRESHOLD. 670 * ULocales must be in canonical, addLikelySubtags format. Returns distance 671 */ distanceRaw( String desiredLang, String supportedlang, String desiredScript, String supportedScript, String desiredRegion, String supportedRegion, int threshold, DistanceOption distanceOption)672 public int distanceRaw( 673 String desiredLang, String supportedlang, 674 String desiredScript, String supportedScript, 675 String desiredRegion, String supportedRegion, 676 int threshold, 677 DistanceOption distanceOption) { 678 679 Output<DistanceTable> subtable = new Output<>(); 680 681 int distance = languageDesired2Supported.getDistance(desiredLang, supportedlang, subtable, true); 682 boolean scriptFirst = distanceOption == DistanceOption.SCRIPT_FIRST; 683 if (scriptFirst) { 684 distance >>= 2; 685 } 686 if (distance < 0) { 687 distance = 0; 688 } else if (distance >= threshold) { 689 return ABOVE_THRESHOLD; 690 } 691 692 int scriptDistance = subtable.value.getDistance(desiredScript, supportedScript, subtable, true); 693 if (scriptFirst) { 694 scriptDistance >>= 1; 695 } 696 distance += scriptDistance; 697 if (distance >= threshold) { 698 return ABOVE_THRESHOLD; 699 } 700 701 if (desiredRegion.equals(supportedRegion)) { 702 return distance; 703 } 704 705 // From here on we know the regions are not equal 706 707 final String desiredPartition = regionMapper.toId(desiredRegion); 708 final String supportedPartition = regionMapper.toId(supportedRegion); 709 int subdistance; 710 711 // check for macros. If one is found, we take the maximum distance 712 // this could be optimized by adding some more structure, but probably not worth it. 713 714 Collection<String> desiredPartitions = desiredPartition.isEmpty() ? regionMapper.macroToPartitions.get(desiredRegion) : null; 715 Collection<String> supportedPartitions = supportedPartition.isEmpty() ? regionMapper.macroToPartitions.get(supportedRegion) : null; 716 if (desiredPartitions != null || supportedPartitions != null) { 717 subdistance = 0; 718 // make the code simple for now 719 if (desiredPartitions == null) { 720 desiredPartitions = Collections.singleton(desiredPartition); 721 } 722 if (supportedPartitions == null) { 723 supportedPartitions = Collections.singleton(supportedPartition); 724 } 725 726 for (String desiredPartition2 : desiredPartitions) { 727 for (String supportedPartition2 : supportedPartitions) { 728 int tempSubdistance = subtable.value.getDistance(desiredPartition2, supportedPartition2, null, false); 729 if (subdistance < tempSubdistance) { 730 subdistance = tempSubdistance; 731 } 732 } 733 } 734 } else { 735 subdistance = subtable.value.getDistance(desiredPartition, supportedPartition, null, false); 736 } 737 distance += subdistance; 738 return distance >= threshold ? ABOVE_THRESHOLD : distance; 739 } 740 741 private static final XLocaleDistance DEFAULT; 742 getDefault()743 public static XLocaleDistance getDefault() { 744 return DEFAULT; 745 } 746 747 static { 748 String[][] variableOverrides = { 749 { "$enUS", "AS+GU+MH+MP+PR+UM+US+VI" }, 750 751 { "$cnsar", "HK+MO" }, 752 753 { "$americas", "019" }, 754 755 { "$maghreb", "MA+DZ+TN+LY+MR+EH" }, 756 }; 757 String[] paradigmRegions = { 758 "en", "en-GB", "es", "es-419", "pt-BR", "pt-PT" 759 }; 760 String[][] regionRuleOverrides = { 761 { "ar_*_$maghreb", "ar_*_$maghreb", "96" }, 762 { "ar_*_$!maghreb", "ar_*_$!maghreb", "96" }, 763 { "ar_*_*", "ar_*_*", "95" }, 764 765 { "en_*_$enUS", "en_*_$enUS", "96" }, 766 { "en_*_$!enUS", "en_*_$!enUS", "96" }, 767 { "en_*_*", "en_*_*", "95" }, 768 769 { "es_*_$americas", "es_*_$americas", "96" }, 770 { "es_*_$!americas", "es_*_$!americas", "96" }, 771 { "es_*_*", "es_*_*", "95" }, 772 773 { "pt_*_$americas", "pt_*_$americas", "96" }, 774 { "pt_*_$!americas", "pt_*_$!americas", "96" }, 775 { "pt_*_*", "pt_*_*", "95" }, 776 777 { "zh_Hant_$cnsar", "zh_Hant_$cnsar", "96" }, 778 { "zh_Hant_$!cnsar", "zh_Hant_$!cnsar", "96" }, 779 { "zh_Hant_*", "zh_Hant_*", "95" }, 780 781 { "*_*_*", "*_*_*", "96" }, 782 }; 783 784 Builder rmb = new RegionMapper.Builder().addParadigms(paradigmRegions); 785 for (String[] variableRule : variableOverrides) { rmb.add(variableRule[0], variableRule[1])786 rmb.add(variableRule[0], variableRule[1]); 787 } 788 if (PRINT_OVERRIDES) { 789 System.out.println("\t\t<languageMatches type=\"written\" alt=\"enhanced\">"); 790 System.out.println("\t\t\t<paradigmLocales locales=\"" + String 791 .join(" ", paradigmRegions) 792 + "\"/>"); 793 for (String[] variableRule : variableOverrides) { 794 System.out.println("\t\t\t<matchVariable id=\"" + variableRule[0] 795 + "\" value=\"" 796 + variableRule[1] 797 + "\"/>"); 798 } 799 } 800 801 final StringDistanceTable defaultDistanceTable = new StringDistanceTable(); 802 final RegionMapper defaultRegionMapper = rmb.build(); 803 804 Splitter bar = Splitter.on('_'); 805 806 List<Row.R4<List<String>, List<String>, Integer, Boolean>>[] sorted = new ArrayList[3]; 807 sorted[0] = new ArrayList<>(); 808 sorted[1] = new ArrayList<>(); 809 sorted[2] = new ArrayList<>(); 810 811 // sort the rules so that the language-only are first, then the language-script, and finally the language-script-region. 812 for (R4<String, String, Integer, Boolean> info : xGetLanguageMatcherData()) { 813 String desiredRaw = info.get0(); 814 String supportedRaw = info.get1(); 815 List<String> desired = bar.splitToList(desiredRaw); 816 List<String> supported = bar.splitToList(supportedRaw); 817 Boolean oneway = info.get3(); 818 Integer percentRaw = desiredRaw.equals("*_*") ? 50 : info.get2(); 819 final int distance = 100 - percentRaw; 820 int size = desired.size(); 821 822 // for now, skip size == 3 823 if (size == 3) continue; 824 Row.of(desired, supported, distance, oneway)825 sorted[size - 1].add(Row.of(desired, supported, distance, oneway)); 826 } 827 828 for (List<Row.R4<List<String>, List<String>, Integer, Boolean>> item1 : sorted) { 829 int debug = 0; 830 for (Row.R4<List<String>, List<String>, Integer, Boolean> item2 : item1) { 831 List<String> desired = item2.get0(); 832 List<String> supported = item2.get1(); 833 Integer distance = item2.get2(); 834 Boolean oneway = item2.get3(); add(defaultDistanceTable, desired, supported, distance)835 add(defaultDistanceTable, desired, supported, distance); 836 if (oneway != Boolean.TRUE && !desired.equals(supported)) { add(defaultDistanceTable, supported, desired, distance)837 add(defaultDistanceTable, supported, desired, distance); 838 } printMatchXml(desired, supported, distance, oneway)839 printMatchXml(desired, supported, distance, oneway); 840 } 841 } 842 843 // add new size=3 844 for (String[] rule : regionRuleOverrides) { 845 // if (PRINT_OVERRIDES) System.out.println("\t\t\t<languageMatch desired=\"" 846 // + rule[0] 847 // + "\" supported=\"" 848 // + rule[1] 849 // + "\" distance=\"" 850 // + rule[2] 851 // + "\"/>"); 852 if (rule[0].equals("en_*_*") || rule[1].equals("*_*_*")) { 853 int debug = 0; 854 } 855 List<String> desiredBase = new ArrayList<>(bar.splitToList(rule[0])); 856 List<String> supportedBase = new ArrayList<>(bar.splitToList(rule[1])); 857 Integer distance = 100 - Integer.parseInt(rule[2]); printMatchXml(desiredBase, supportedBase, distance, false)858 printMatchXml(desiredBase, supportedBase, distance, false); 859 860 Collection<String> desiredRegions = defaultRegionMapper.getIdsFromVariable(desiredBase.get(2)); 861 if (desiredRegions.isEmpty()) { 862 throw new IllegalArgumentException("Bad region variable: " + desiredBase.get(2)); 863 } 864 Collection<String> supportedRegions = defaultRegionMapper.getIdsFromVariable(supportedBase.get(2)); 865 if (supportedRegions.isEmpty()) { 866 throw new IllegalArgumentException("Bad region variable: " + supportedBase.get(2)); 867 } 868 for (String desiredRegion2 : desiredRegions) { 869 desiredBase.set(2, desiredRegion2.toString()); // fix later 870 for (String supportedRegion2 : supportedRegions) { 871 supportedBase.set(2, supportedRegion2.toString()); // fix later add(defaultDistanceTable, desiredBase, supportedBase, distance)872 add(defaultDistanceTable, desiredBase, supportedBase, distance); add(defaultDistanceTable, supportedBase, desiredBase, distance)873 add(defaultDistanceTable, supportedBase, desiredBase, distance); 874 } 875 } 876 } 877 if (PRINT_OVERRIDES) { 878 System.out.println("\t\t</languageMatches>"); 879 } 880 881 DEFAULT = new XLocaleDistance(defaultDistanceTable.compact(), defaultRegionMapper); 882 883 if (false && PRINT_OVERRIDES) { 884 System.out.println(defaultRegionMapper); 885 System.out.println(defaultDistanceTable); IllegalArgumentException()886 throw new IllegalArgumentException(); 887 } 888 } 889 printMatchXml(List<String> desired, List<String> supported, Integer distance, Boolean oneway)890 private static void printMatchXml(List<String> desired, List<String> supported, Integer distance, Boolean oneway) { 891 if (PRINT_OVERRIDES) { 892 String desiredStr = Joiner.on("_").join(desired); 893 String supportedStr = Joiner.on("_").join(supported); 894 String desiredName = fixedName(desired); 895 String supportedName = fixedName(supported); 896 System.out.println("\t\t\t<languageMatch" 897 + " desired=\"" + desiredStr 898 + "\"\tsupported=\"" + supportedStr 899 + "\"\tdistance=\"" + distance 900 + (!oneway ? "" : "\"\toneway=\"true") 901 + "\"/>\t<!-- " + desiredName + " ⇒ " + supportedName + " -->"); 902 } 903 } 904 fixedName(List<String> match)905 private static String fixedName(List<String> match) { 906 List<String> alt = new ArrayList<>(match); 907 StringBuilder result = new StringBuilder(); 908 switch (alt.size()) { 909 case 3: 910 String region = alt.get(2); 911 if (region.equals("*") || region.startsWith("$")) { 912 result.append(region); 913 } else { 914 result.append(english.getName(CLDRFile.TERRITORY_NAME, region)); 915 } 916 case 2: 917 String script = alt.get(1); 918 if (script.equals("*")) { 919 result.insert(0, script); 920 } else { 921 result.insert(0, english.getName(CLDRFile.TERRITORY_NAME, script)); 922 } 923 case 1: 924 String language = alt.get(0); 925 if (language.equals("*")) { 926 result.insert(0, language); 927 } else { 928 result.insert(0, english.getName(CLDRFile.TERRITORY_NAME, language)); 929 } 930 } 931 return Joiner.on("; ").join(alt); 932 } 933 add(StringDistanceTable languageDesired2Supported, List<String> desired, List<String> supported, int percentage)934 static public void add(StringDistanceTable languageDesired2Supported, List<String> desired, List<String> supported, int percentage) { 935 int size = desired.size(); 936 if (size != supported.size() || size < 1 || size > 3) { 937 throw new IllegalArgumentException(); 938 } 939 final String desiredLang = fixAny(desired.get(0)); 940 final String supportedLang = fixAny(supported.get(0)); 941 if (size == 1) { 942 languageDesired2Supported.addSubtable(desiredLang, supportedLang, percentage); 943 } else { 944 final String desiredScript = fixAny(desired.get(1)); 945 final String supportedScript = fixAny(supported.get(1)); 946 if (size == 2) { 947 languageDesired2Supported.addSubtables(desiredLang, supportedLang, desiredScript, supportedScript, percentage); 948 } else { 949 final String desiredRegion = fixAny(desired.get(2)); 950 final String supportedRegion = fixAny(supported.get(2)); 951 languageDesired2Supported.addSubtables(desiredLang, supportedLang, desiredScript, supportedScript, desiredRegion, supportedRegion, percentage); 952 } 953 } 954 } 955 956 @Override toString()957 public String toString() { 958 return toString(false); 959 } 960 toString(boolean abbreviate)961 public String toString(boolean abbreviate) { 962 return regionMapper + "\n" + languageDesired2Supported.toString(abbreviate); 963 } 964 965 // public static XLocaleDistance createDefaultInt() { 966 // IntDistanceTable d = new IntDistanceTable(DEFAULT_DISTANCE_TABLE); 967 // return new XLocaleDistance(d, DEFAULT_REGION_MAPPER); 968 // } 969 getContainingMacrosFor(Collection<String> input, Set<String> output)970 static Set<String> getContainingMacrosFor(Collection<String> input, Set<String> output) { 971 output.clear(); 972 for (Entry<String, Collection<String>> entry : CONTAINER_TO_CONTAINED.asMap().entrySet()) { 973 if (input.containsAll(entry.getValue())) { // example; if all southern Europe are contained, then add S. Europe 974 output.add(entry.getKey()); 975 } 976 } 977 return output; 978 } 979 980 static class RegionMapper implements IdMapper<String, String> { 981 /** 982 * Used for processing rules. At the start we have a variable setting like $A1=US+CA+MX. We generate a mapping from $A1 to a set of partitions {P1, P2} 983 * When we hit a rule that contains a variable, we replace that rule by multiple rules for the partitions. 984 */ 985 final Multimap<String, String> variableToPartition; 986 /** 987 * Used for executing the rules. We map a region to a partition before processing. 988 */ 989 final Map<String, String> regionToPartition; 990 /** 991 * Used to support es_419 compared to es_AR, etc. 992 * @param variableToPartitionIn 993 * @param regionToPartitionIn 994 */ 995 final Multimap<String, String> macroToPartitions; 996 /** 997 * Used to get the paradigm region for a cluster, if there is one 998 */ 999 final ImmutableSet<ULocale> paradigms; 1000 RegionMapper( Multimap<String, String> variableToPartitionIn, Map<String, String> regionToPartitionIn, Multimap<String, String> macroToPartitionsIn, Set<ULocale> paradigmsIn)1001 private RegionMapper( 1002 Multimap<String, String> variableToPartitionIn, 1003 Map<String, String> regionToPartitionIn, 1004 Multimap<String, String> macroToPartitionsIn, 1005 Set<ULocale> paradigmsIn) { 1006 variableToPartition = ImmutableMultimap.copyOf(variableToPartitionIn); 1007 regionToPartition = ImmutableMap.copyOf(regionToPartitionIn); 1008 macroToPartitions = ImmutableMultimap.copyOf(macroToPartitionsIn); 1009 paradigms = ImmutableSet.copyOf(paradigmsIn); 1010 } 1011 1012 @Override toId(String region)1013 public String toId(String region) { 1014 String result = regionToPartition.get(region); 1015 return result == null ? "" : result; 1016 } 1017 getIdsFromVariable(String variable)1018 public Collection<String> getIdsFromVariable(String variable) { 1019 if (variable.equals("*")) { 1020 return Collections.singleton("*"); 1021 } 1022 Collection<String> result = variableToPartition.get(variable); 1023 if (result == null || result.isEmpty()) { 1024 throw new IllegalArgumentException("Variable not defined: " + variable); 1025 } 1026 return result; 1027 } 1028 regions()1029 public Set<String> regions() { 1030 return regionToPartition.keySet(); 1031 } 1032 variables()1033 public Set<String> variables() { 1034 return variableToPartition.keySet(); 1035 } 1036 1037 @Override toString()1038 public String toString() { 1039 TreeMultimap<String, String> partitionToVariables = Multimaps.invertFrom(variableToPartition, TreeMultimap.create()); 1040 TreeMultimap<String, String> partitionToRegions = TreeMultimap.create(); 1041 for (Entry<String, String> e : regionToPartition.entrySet()) { 1042 partitionToRegions.put(e.getValue(), e.getKey()); 1043 } 1044 StringBuilder buffer = new StringBuilder(); 1045 buffer.append("Partition ➠ Variables ➠ Regions (final)"); 1046 for (Entry<String, Collection<String>> e : partitionToVariables.asMap().entrySet()) { 1047 buffer.append('\n'); 1048 buffer.append(e.getKey() + "\t" + e.getValue() + "\t" + partitionToRegions.get(e.getKey())); 1049 } 1050 buffer.append("\nMacro ➠ Partitions"); 1051 for (Entry<String, Collection<String>> e : macroToPartitions.asMap().entrySet()) { 1052 buffer.append('\n'); 1053 buffer.append(e.getKey() + "\t" + e.getValue()); 1054 } 1055 1056 return buffer.toString(); 1057 } 1058 1059 static class Builder { 1060 final private Multimap<String, String> regionToRawPartition = TreeMultimap.create(); 1061 final private RegionSet regionSet = new RegionSet(); 1062 final private Set<ULocale> paradigms = new LinkedHashSet<>(); 1063 add(String variable, String barString)1064 void add(String variable, String barString) { 1065 Set<String> tempRegions = regionSet.parseSet(barString); 1066 1067 for (String region : tempRegions) { 1068 regionToRawPartition.put(region, variable); 1069 } 1070 1071 // now add the inverse variable 1072 1073 Set<String> inverse = regionSet.inverse(); 1074 String inverseVariable = "$!" + variable.substring(1); 1075 for (String region : inverse) { 1076 regionToRawPartition.put(region, inverseVariable); 1077 } 1078 } 1079 addParadigms(String... paradigmRegions)1080 public Builder addParadigms(String... paradigmRegions) { 1081 for (String paradigm : paradigmRegions) { 1082 paradigms.add(new ULocale(paradigm)); 1083 } 1084 return this; 1085 } 1086 build()1087 RegionMapper build() { 1088 final IdMakerFull<Collection<String>> id = new IdMakerFull<>("partition"); 1089 Multimap<String, String> variableToPartitions = TreeMultimap.create(); 1090 Map<String, String> regionToPartition = new TreeMap<>(); 1091 Multimap<String, String> partitionToRegions = TreeMultimap.create(); 1092 1093 for (Entry<String, Collection<String>> e : regionToRawPartition.asMap().entrySet()) { 1094 final String region = e.getKey(); 1095 final Collection<String> rawPartition = e.getValue(); 1096 String partition = String.valueOf((char) ('α' + id.add(rawPartition))); 1097 1098 regionToPartition.put(region, partition); 1099 partitionToRegions.put(partition, region); 1100 1101 for (String variable : rawPartition) { 1102 variableToPartitions.put(variable, partition); 1103 } 1104 } 1105 1106 // we get a mapping of each macro to the partitions it intersects with 1107 Multimap<String, String> macroToPartitions = TreeMultimap.create(); 1108 for (Entry<String, Collection<String>> e : CONTAINER_TO_CONTAINED.asMap().entrySet()) { 1109 String macro = e.getKey(); 1110 for (Entry<String, Collection<String>> e2 : partitionToRegions.asMap().entrySet()) { 1111 String partition = e2.getKey(); 1112 if (!Collections.disjoint(e.getValue(), e2.getValue())) { 1113 macroToPartitions.put(macro, partition); 1114 } 1115 } 1116 } 1117 1118 return new RegionMapper( 1119 variableToPartitions, 1120 regionToPartition, 1121 macroToPartitions, 1122 paradigms); 1123 } 1124 } 1125 } 1126 1127 /** 1128 * Parses a string of regions like "US+005-BR" and produces a set of resolved regions. 1129 * All macroregions are fully resolved to sets of non-macro regions. 1130 * <br>Syntax is simple for now: 1131 * <pre>regionSet := region ([-+] region)*</pre> 1132 * No precedence, so "x+y-y+z" is (((x+y)-y)+z) NOT (x+y)-(y+z) 1133 */ 1134 private static class RegionSet { 1135 private enum Operation { 1136 add, remove 1137 } 1138 1139 // temporaries used in processing 1140 final private Set<String> tempRegions = new TreeSet<>(); 1141 private Operation operation = null; 1142 parseSet(String barString)1143 private Set<String> parseSet(String barString) { 1144 operation = Operation.add; 1145 int last = 0; 1146 tempRegions.clear(); 1147 int i = 0; 1148 for (; i < barString.length(); ++i) { 1149 char c = barString.charAt(i); // UTF16 is ok, since syntax is only ascii 1150 switch (c) { 1151 case '+': 1152 add(barString, last, i); 1153 last = i + 1; 1154 operation = Operation.add; 1155 break; 1156 case '-': 1157 add(barString, last, i); 1158 last = i + 1; 1159 operation = Operation.remove; 1160 break; 1161 } 1162 } 1163 add(barString, last, i); 1164 return tempRegions; 1165 } 1166 inverse()1167 private Set<String> inverse() { 1168 TreeSet<String> result = new TreeSet<>(ALL_FINAL_REGIONS); 1169 result.removeAll(tempRegions); 1170 return result; 1171 } 1172 add(String barString, int last, int i)1173 private void add(String barString, int last, int i) { 1174 if (i > last) { 1175 String region = barString.substring(last, i); 1176 changeSet(operation, region); 1177 } 1178 } 1179 changeSet(Operation operation, String region)1180 private void changeSet(Operation operation, String region) { 1181 Collection<String> contained = CONTAINER_TO_CONTAINED_FINAL.get(region); 1182 if (contained != null && !contained.isEmpty()) { 1183 if (Operation.add == operation) { 1184 tempRegions.addAll(contained); 1185 } else { 1186 tempRegions.removeAll(contained); 1187 } 1188 } else if (Operation.add == operation) { 1189 tempRegions.add(region); 1190 } else { 1191 tempRegions.remove(region); 1192 } 1193 } 1194 } 1195 invertMap(Map<V, K> map)1196 public static <K, V> Multimap<K, V> invertMap(Map<V, K> map) { 1197 return Multimaps.invertFrom(Multimaps.forMap(map), ArrayListMultimap.create()); 1198 } 1199 getParadigms()1200 public Set<ULocale> getParadigms() { 1201 return regionMapper.paradigms; 1202 } 1203 getDefaultLanguageDistance()1204 public int getDefaultLanguageDistance() { 1205 return defaultLanguageDistance; 1206 } 1207 getDefaultScriptDistance()1208 public int getDefaultScriptDistance() { 1209 return defaultScriptDistance; 1210 } 1211 getDefaultRegionDistance()1212 public int getDefaultRegionDistance() { 1213 return defaultRegionDistance; 1214 } 1215 1216 static class CompactAndImmutablizer extends IdMakerFull<Object> { compact(StringDistanceTable item)1217 StringDistanceTable compact(StringDistanceTable item) { 1218 if (toId(item) != null) { 1219 return (StringDistanceTable) intern(item); 1220 } 1221 return new StringDistanceTable(compact(item.subtables, 0)); 1222 } 1223 compact(Map<K, T> item, int level)1224 <K, T> Map<K, T> compact(Map<K, T> item, int level) { 1225 if (toId(item) != null) { 1226 return (Map<K, T>) intern(item); 1227 } 1228 Map<K, T> copy = new LinkedHashMap<>(); 1229 for (Entry<K, T> entry : item.entrySet()) { 1230 T value = entry.getValue(); 1231 if (value instanceof Map) { 1232 copy.put(entry.getKey(), (T) compact((Map) value, level + 1)); 1233 } else { 1234 copy.put(entry.getKey(), (T) compact((DistanceNode) value)); 1235 } 1236 } 1237 return ImmutableMap.copyOf(copy); 1238 } 1239 compact(DistanceNode item)1240 DistanceNode compact(DistanceNode item) { 1241 if (toId(item) != null) { 1242 return (DistanceNode) intern(item); 1243 } 1244 final DistanceTable distanceTable = item.getDistanceTable(); 1245 if (distanceTable == null || distanceTable.isEmpty()) { 1246 return new DistanceNode(item.distance); 1247 } else { 1248 return new StringDistanceNode(item.distance, compact((StringDistanceTable) ((StringDistanceNode) item).distanceTable)); 1249 } 1250 } 1251 } 1252 1253 @Deprecated internalGetDistanceTable()1254 public StringDistanceTable internalGetDistanceTable() { 1255 return (StringDistanceTable) languageDesired2Supported; 1256 } 1257 main(String[] args)1258 public static void main(String[] args) { 1259 // for (Entry<String, Collection<String>> entry : containerToContained.asMap().entrySet()) { 1260 // System.out.println(entry.getKey() + "\t⥢" + entry.getValue() + "; " + containerToFinalContained.get(entry.getKey())); 1261 // } 1262 // final Multimap<String,String> regionToMacros = ImmutableMultimap.copyOf(Multimaps.invertFrom(containerToContained, TreeMultimap.create())); 1263 // for (Entry<String, Collection<String>> entry : regionToMacros.asMap().entrySet()) { 1264 // System.out.println(entry.getKey() + "\t⥤ " + entry.getValue()); 1265 // } 1266 if (PRINT_OVERRIDES) { 1267 System.out.println(getDefault().toString(true)); 1268 } 1269 DistanceTable table = getDefault().languageDesired2Supported; 1270 DistanceTable compactedTable = table.compact(); 1271 if (!table.equals(compactedTable)) { 1272 throw new IllegalArgumentException("Compaction isn't equal"); 1273 } 1274 } 1275 } 1276