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