Home | History | Annotate | Download | only in locale
      1 //  2017 and later: Unicode, Inc. and others.
      2 // License & terms of use: http://www.unicode.org/copyright.html#License
      3 package com.ibm.icu.impl.locale;
      4 
      5 import java.util.ArrayList;
      6 import java.util.Arrays;
      7 import java.util.Collection;
      8 import java.util.Collections;
      9 import java.util.Enumeration;
     10 import java.util.HashMap;
     11 import java.util.HashSet;
     12 import java.util.LinkedHashMap;
     13 import java.util.LinkedHashSet;
     14 import java.util.List;
     15 import java.util.Map;
     16 import java.util.Map.Entry;
     17 import java.util.Set;
     18 import java.util.TreeMap;
     19 import java.util.TreeSet;
     20 
     21 import com.ibm.icu.impl.ICUResourceBundle;
     22 import com.ibm.icu.impl.Row;
     23 import com.ibm.icu.impl.Row.R4;
     24 import com.ibm.icu.impl.Utility;
     25 import com.ibm.icu.impl.locale.XCldrStub.CollectionUtilities;
     26 import com.ibm.icu.impl.locale.XCldrStub.ImmutableMap;
     27 import com.ibm.icu.impl.locale.XCldrStub.ImmutableMultimap;
     28 import com.ibm.icu.impl.locale.XCldrStub.ImmutableSet;
     29 import com.ibm.icu.impl.locale.XCldrStub.LinkedHashMultimap;
     30 import com.ibm.icu.impl.locale.XCldrStub.Multimap;
     31 import com.ibm.icu.impl.locale.XCldrStub.Multimaps;
     32 import com.ibm.icu.impl.locale.XCldrStub.Predicate;
     33 import com.ibm.icu.impl.locale.XCldrStub.Splitter;
     34 import com.ibm.icu.impl.locale.XCldrStub.TreeMultimap;
     35 import com.ibm.icu.impl.locale.XLikelySubtags.LSR;
     36 import com.ibm.icu.impl.locale.XLocaleDistance.RegionMapper.Builder;
     37 import com.ibm.icu.text.LocaleDisplayNames;
     38 import com.ibm.icu.util.LocaleMatcher;
     39 import com.ibm.icu.util.Output;
     40 import com.ibm.icu.util.ULocale;
     41 import com.ibm.icu.util.UResourceBundleIterator;
     42 
     43 public class XLocaleDistance {
     44 
     45     static final boolean PRINT_OVERRIDES = false;
     46 
     47     public static final int ABOVE_THRESHOLD = 100;
     48 
     49     @Deprecated
     50     public static final String ANY = ""; // matches any character. Uses value above any subtag.
     51 
     52     private static String fixAny(String string) {
     53         return "*".equals(string) ? ANY : string;
     54     }
     55 
     56     static final LocaleDisplayNames english = LocaleDisplayNames.getInstance(ULocale.ENGLISH);
     57 
     58     private static List<R4<String, String, Integer, Boolean>> xGetLanguageMatcherData() {
     59         List<R4<String, String, Integer, Boolean>> distanceList = new ArrayList<R4<String, String, Integer, Boolean>>();
     60 
     61         ICUResourceBundle suppData = LocaleMatcher.getICUSupplementalData();
     62         ICUResourceBundle languageMatchingNew = suppData.findTopLevel("languageMatchingNew");
     63         ICUResourceBundle written = (ICUResourceBundle) languageMatchingNew.get("written");
     64 
     65         for(UResourceBundleIterator iter = written.getIterator(); iter.hasNext();) {
     66             ICUResourceBundle item = (ICUResourceBundle) iter.next();
     67             boolean oneway = item.getSize() > 3 && "1".equals(item.getString(3));
     68             distanceList.add(
     69                     (R4<String, String, Integer, Boolean>)            // note: .freeze returning wrong type, so casting.
     70                     Row.of(
     71                             item.getString(0),
     72                             item.getString(1),
     73                             Integer.parseInt(item.getString(2)),
     74                             oneway)
     75                     .freeze());
     76         }
     77         return Collections.unmodifiableList(distanceList);
     78     }
     79 
     80     @SuppressWarnings("unused")
     81     private static Set<String> xGetParadigmLocales() {
     82         ICUResourceBundle suppData = LocaleMatcher.getICUSupplementalData();
     83         ICUResourceBundle languageMatchingInfo = suppData.findTopLevel("languageMatchingInfo");
     84         ICUResourceBundle writtenParadigmLocales = (ICUResourceBundle) languageMatchingInfo.get("written")
     85                 .get("paradigmLocales");
     86         //      paradigmLocales{ "en", "en-GB",... }
     87         HashSet<String> paradigmLocales = new HashSet<String>(Arrays.asList(writtenParadigmLocales.getStringArray()));
     88         return Collections.unmodifiableSet(paradigmLocales);
     89     }
     90 
     91     @SuppressWarnings("unused")
     92     private static Map<String, String> xGetMatchVariables() {
     93         ICUResourceBundle suppData = LocaleMatcher.getICUSupplementalData();
     94         ICUResourceBundle languageMatchingInfo = suppData.findTopLevel("languageMatchingInfo");
     95         ICUResourceBundle writtenMatchVariables = (ICUResourceBundle) languageMatchingInfo.get("written")
     96                 .get("matchVariable");
     97         //        matchVariable{ americas{"019"} cnsar{"HK+MO"} ...}
     98 
     99         HashMap<String,String> matchVariables = new HashMap<String,String>();
    100         for (Enumeration<String> enumer = writtenMatchVariables.getKeys(); enumer.hasMoreElements(); ) {
    101             String key = enumer.nextElement();
    102             matchVariables.put(key, writtenMatchVariables.getString(key));
    103         }
    104         return Collections.unmodifiableMap(matchVariables);
    105     }
    106 
    107     private static Multimap<String, String> xGetContainment() {
    108         TreeMultimap<String,String> containment = TreeMultimap.create();
    109         containment
    110         .putAll("001", "019", "002", "150", "142", "009")
    111         .putAll("011", "BF", "BJ", "CI", "CV", "GH", "GM", "GN", "GW", "LR", "ML", "MR", "NE", "NG", "SH", "SL", "SN", "TG")
    112         .putAll("013", "BZ", "CR", "GT", "HN", "MX", "NI", "PA", "SV")
    113         .putAll("014", "BI", "DJ", "ER", "ET", "KE", "KM", "MG", "MU", "MW", "MZ", "RE", "RW", "SC", "SO", "SS", "TZ", "UG", "YT", "ZM", "ZW")
    114         .putAll("142", "145", "143", "030", "034", "035")
    115         .putAll("143", "TM", "TJ", "KG", "KZ", "UZ")
    116         .putAll("145", "AE", "AM", "AZ", "BH", "CY", "GE", "IL", "IQ", "JO", "KW", "LB", "OM", "PS", "QA", "SA", "SY", "TR", "YE", "NT", "YD")
    117         .putAll("015", "DZ", "EG", "EH", "LY", "MA", "SD", "TN", "EA", "IC")
    118         .putAll("150", "154", "155", "151", "039")
    119         .putAll("151", "BG", "BY", "CZ", "HU", "MD", "PL", "RO", "RU", "SK", "UA", "SU")
    120         .putAll("154", "GG", "IM", "JE", "AX", "DK", "EE", "FI", "FO", "GB", "IE", "IS", "LT", "LV", "NO", "SE", "SJ")
    121         .putAll("155", "AT", "BE", "CH", "DE", "FR", "LI", "LU", "MC", "NL", "DD", "FX")
    122         .putAll("017", "AO", "CD", "CF", "CG", "CM", "GA", "GQ", "ST", "TD", "ZR")
    123         .putAll("018", "BW", "LS", "NA", "SZ", "ZA")
    124         .putAll("019", "021", "013", "029", "005", "003", "419")
    125         .putAll("002", "015", "011", "017", "014", "018")
    126         .putAll("021", "BM", "CA", "GL", "PM", "US")
    127         .putAll("029", "AG", "AI", "AW", "BB", "BL", "BQ", "BS", "CU", "CW", "DM", "DO", "GD", "GP", "HT", "JM", "KN", "KY", "LC", "MF", "MQ", "MS", "PR", "SX", "TC", "TT", "VC", "VG", "VI", "AN")
    128         .putAll("003", "021", "013", "029")
    129         .putAll("030", "CN", "HK", "JP", "KP", "KR", "MN", "MO", "TW")
    130         .putAll("035", "BN", "ID", "KH", "LA", "MM", "MY", "PH", "SG", "TH", "TL", "VN", "BU", "TP")
    131         .putAll("039", "AD", "AL", "BA", "ES", "GI", "GR", "HR", "IT", "ME", "MK", "MT", "RS", "PT", "SI", "SM", "VA", "XK", "CS", "YU")
    132         .putAll("419", "013", "029", "005")
    133         .putAll("005", "AR", "BO", "BR", "CL", "CO", "EC", "FK", "GF", "GY", "PE", "PY", "SR", "UY", "VE")
    134         .putAll("053", "AU", "NF", "NZ")
    135         .putAll("054", "FJ", "NC", "PG", "SB", "VU")
    136         .putAll("057", "FM", "GU", "KI", "MH", "MP", "NR", "PW")
    137         .putAll("061", "AS", "CK", "NU", "PF", "PN", "TK", "TO", "TV", "WF", "WS")
    138         .putAll("034", "AF", "BD", "BT", "IN", "IR", "LK", "MV", "NP", "PK")
    139         .putAll("009", "053", "054", "057", "061", "QO")
    140         .putAll("QO", "AQ", "BV", "CC", "CX", "GS", "HM", "IO", "TF", "UM", "AC", "CP", "DG", "TA")
    141         ;
    142         //Can't use following, because data from CLDR is discarded
    143         //        ICUResourceBundle suppData = LocaleMatcher.getICUSupplementalData();
    144         //        UResourceBundle territoryContainment = suppData.get("territoryContainment");
    145         //        for (int i = 0 ; i < territoryContainment.getSize(); i++) {
    146         //            UResourceBundle mapping = territoryContainment.get(i);
    147         //            String parent = mapping.getKey();
    148         //            for (int j = 0 ; j < mapping.getSize(); j++) {
    149         //                String child = mapping.getString(j);
    150         //                containment.put(parent,child);
    151         //                System.out.println(parent + " => " + child);
    152         //            }
    153         //        }
    154         TreeMultimap<String,String> containmentResolved = TreeMultimap.create();
    155         fill("001", containment, containmentResolved);
    156         return ImmutableMultimap.copyOf(containmentResolved);
    157     }
    158 
    159     private static Set<String> fill(String region, TreeMultimap<String, String> containment, Multimap<String, String> toAddTo) {
    160         Set<String> contained = containment.get(region);
    161         if (contained == null) {
    162             return Collections.emptySet();
    163         }
    164         toAddTo.putAll(region, contained); // do top level
    165         // then recursively
    166         for (String subregion : contained) {
    167             toAddTo.putAll(region, fill(subregion, containment, toAddTo));
    168         }
    169         return toAddTo.get(region);
    170     }
    171 
    172 
    173     static final Multimap<String,String> CONTAINER_TO_CONTAINED;
    174     static final Multimap<String,String> CONTAINER_TO_CONTAINED_FINAL;
    175     static {
    176         //         Multimap<String, String> containerToContainedTemp = xGetContainment();
    177         //         fill(Region.getInstance("001"), containerToContainedTemp);
    178 
    179         CONTAINER_TO_CONTAINED = xGetContainment();
    180         Multimap<String, String> containerToFinalContainedBuilder = TreeMultimap.create();
    181         for (Entry<String, Set<String>> entry : CONTAINER_TO_CONTAINED.asMap().entrySet()) {
    182             String container = entry.getKey();
    183             for (String contained : entry.getValue()) {
    184                 if (CONTAINER_TO_CONTAINED.get(contained) == null) {
    185                     containerToFinalContainedBuilder.put(container, contained);
    186                 }
    187             }
    188         }
    189         CONTAINER_TO_CONTAINED_FINAL = ImmutableMultimap.copyOf(containerToFinalContainedBuilder);
    190     }
    191 
    192     final static private Set<String> ALL_FINAL_REGIONS = ImmutableSet.copyOf(CONTAINER_TO_CONTAINED_FINAL.get("001"));
    193 
    194     // end of data from CLDR
    195 
    196     private final DistanceTable languageDesired2Supported;
    197     private final RegionMapper regionMapper;
    198     private final int defaultLanguageDistance;
    199     private final int defaultScriptDistance;
    200     private final int defaultRegionDistance;
    201 
    202     @Deprecated
    203     public static abstract class DistanceTable {
    204         abstract int getDistance(String desiredLang, String supportedlang, Output<DistanceTable> table, boolean starEquals);
    205         abstract Set<String> getCloser(int threshold);
    206         abstract String toString(boolean abbreviate);
    207         public DistanceTable compact() {
    208             return this;
    209         }
    210         //        public Integer getInternalDistance(String a, String b) {
    211         //            return null;
    212         //        }
    213         public DistanceNode getInternalNode(String any, String any2) {
    214             return null;
    215         }
    216         public Map<String, Set<String>> getInternalMatches() {
    217             return null;
    218         }
    219         public boolean isEmpty() {
    220             return true;
    221         }
    222     }
    223 
    224     @Deprecated
    225     public static class DistanceNode {
    226         final int distance;
    227 
    228         public DistanceNode(int distance) {
    229             this.distance = distance;
    230         }
    231 
    232         public DistanceTable getDistanceTable() {
    233             return null;
    234         }
    235 
    236         @Override
    237         public boolean equals(Object obj) {
    238             return this == obj ||
    239                     (obj != null
    240                     && obj.getClass() == this.getClass()
    241                     && distance == ((DistanceNode) obj).distance);
    242         }
    243         @Override
    244         public int hashCode() {
    245             return distance;
    246         }
    247         @Override
    248         public String toString() {
    249             return "\ndistance: " + distance;
    250         }
    251     }
    252 
    253     private interface IdMapper<K,V> {
    254         public V toId(K source);
    255     }
    256 
    257     static class IdMakerFull<T> implements IdMapper<T,Integer> {
    258         private final Map<T, Integer> objectToInt = new HashMap<T, Integer>();
    259         private final List<T> intToObject = new ArrayList<T>();
    260         final String name; // for debugging
    261 
    262         IdMakerFull(String name) {
    263             this.name = name;
    264         }
    265 
    266         IdMakerFull() {
    267             this("unnamed");
    268         }
    269 
    270         IdMakerFull(String name, T zeroValue) {
    271             this(name);
    272             add(zeroValue);
    273         }
    274 
    275         /**
    276          * Return an id, making one if there wasn't one already.
    277          */
    278         public Integer add(T source) {
    279             Integer result = objectToInt.get(source);
    280             if (result == null) {
    281                 Integer newResult = intToObject.size();
    282                 objectToInt.put(source, newResult);
    283                 intToObject.add(source);
    284                 return newResult;
    285             } else {
    286                 return result;
    287             }
    288         }
    289 
    290         /**
    291          * Return an id, or null if there is none.
    292          */
    293         @Override
    294         public Integer toId(T source) {
    295             return objectToInt.get(source);
    296             //            return value == null ? 0 : value;
    297         }
    298 
    299         /**
    300          * Return the object for the id, or null if there is none.
    301          */
    302         public T fromId(int id) {
    303             return intToObject.get(id);
    304         }
    305 
    306         /**
    307          * Return interned object
    308          */
    309         public T intern(T source) {
    310             return fromId(add(source));
    311         }
    312 
    313         public int size() {
    314             return intToObject.size();
    315         }
    316         /**
    317          * Same as add, except if the object didn't have an id, return null;
    318          */
    319         public Integer getOldAndAdd(T source) {
    320             Integer result = objectToInt.get(source);
    321             if (result == null) {
    322                 Integer newResult = intToObject.size();
    323                 objectToInt.put(source, newResult);
    324                 intToObject.add(source);
    325             }
    326             return result;
    327         }
    328 
    329         @Override
    330         public String toString() {
    331             return size() + ": " + intToObject;
    332         }
    333         @Override
    334         public boolean equals(Object obj) {
    335             return this == obj ||
    336                     (obj != null
    337                     && obj.getClass() == this.getClass()
    338                     && intToObject.equals(((IdMakerFull<?>) obj).intToObject));
    339         }
    340         @Override
    341         public int hashCode() {
    342             return intToObject.hashCode();
    343         }
    344     }
    345 
    346     static class StringDistanceNode extends DistanceNode {
    347         final DistanceTable distanceTable;
    348 
    349         public StringDistanceNode(int distance, DistanceTable distanceTable) {
    350             super(distance);
    351             this.distanceTable = distanceTable;
    352         }
    353 
    354         @Override
    355         public boolean equals(Object obj) {
    356             StringDistanceNode other;
    357             return this == obj ||
    358                     (obj != null
    359                     && obj.getClass() == this.getClass()
    360                     && distance == (other = (StringDistanceNode) obj).distance
    361                     && Utility.equals(distanceTable, other.distanceTable)
    362                     && super.equals(other));
    363         }
    364         @Override
    365         public int hashCode() {
    366             return distance ^ Utility.hashCode(distanceTable);
    367         }
    368 
    369         StringDistanceNode(int distance) {
    370             this(distance, new StringDistanceTable());
    371         }
    372 
    373         public void addSubtables(String desiredSub, String supportedSub, CopyIfEmpty r) {
    374             ((StringDistanceTable) distanceTable).addSubtables(desiredSub, supportedSub, r);
    375         }
    376         @Override
    377         public String toString() {
    378             return "distance: " + distance + "\n" + distanceTable;
    379         }
    380 
    381         public void copyTables(StringDistanceTable value) {
    382             if (value != null) {
    383                 ((StringDistanceTable)distanceTable).copy(value);
    384             }
    385         }
    386 
    387         @Override
    388         public DistanceTable getDistanceTable() {
    389             return distanceTable;
    390         }
    391     }
    392 
    393     public XLocaleDistance(DistanceTable datadistancetable2, RegionMapper regionMapper) {
    394         languageDesired2Supported = datadistancetable2;
    395         this.regionMapper = regionMapper;
    396 
    397         StringDistanceNode languageNode = (StringDistanceNode) ((StringDistanceTable) languageDesired2Supported).subtables.get(ANY).get(ANY);
    398         defaultLanguageDistance = languageNode.distance;
    399         StringDistanceNode scriptNode = (StringDistanceNode) ((StringDistanceTable)languageNode.distanceTable).subtables.get(ANY).get(ANY);
    400         defaultScriptDistance = scriptNode.distance;
    401         DistanceNode regionNode = ((StringDistanceTable)scriptNode.distanceTable).subtables.get(ANY).get(ANY);
    402         defaultRegionDistance = regionNode.distance;
    403     }
    404 
    405     @SuppressWarnings("rawtypes")
    406     private static Map newMap() { // for debugging
    407         return new TreeMap();
    408     }
    409 
    410     /**
    411      * Internal class
    412      */
    413     @Deprecated
    414     public static class StringDistanceTable extends DistanceTable {
    415         final Map<String, Map<String, DistanceNode>> subtables;
    416 
    417         StringDistanceTable(Map<String, Map<String, DistanceNode>> tables) {
    418             subtables = tables;
    419         }
    420         @SuppressWarnings("unchecked")
    421         StringDistanceTable() {
    422             this(newMap());
    423         }
    424 
    425         @Override
    426         public boolean isEmpty() {
    427             return subtables.isEmpty();
    428         }
    429 
    430         @Override
    431         public boolean equals(Object obj) {
    432             return this == obj ||
    433                     (obj != null
    434                     && obj.getClass() == this.getClass()
    435                     && subtables.equals(((StringDistanceTable) obj).subtables));
    436         }
    437         @Override
    438         public int hashCode() {
    439             return subtables.hashCode();
    440         }
    441 
    442         @Override
    443         public int getDistance(String desired, String supported, Output<DistanceTable> distanceTable, boolean starEquals) {
    444             boolean star = false;
    445             Map<String, DistanceNode> sub2 = subtables.get(desired);
    446             if (sub2 == null) {
    447                 sub2 = subtables.get(ANY); // <*, supported>
    448                 star = true;
    449             }
    450             DistanceNode value = sub2.get(supported);   // <*/desired, supported>
    451             if (value == null) {
    452                 value = sub2.get(ANY);  // <*/desired, *>
    453                 if (value == null && !star) {
    454                     sub2 = subtables.get(ANY);   // <*, supported>
    455                     value = sub2.get(supported);
    456                     if (value == null) {
    457                         value = sub2.get(ANY);   // <*, *>
    458                     }
    459                 }
    460                 star = true;
    461             }
    462             if (distanceTable != null) {
    463                 distanceTable.value = ((StringDistanceNode) value).distanceTable;
    464             }
    465             return starEquals && star && desired.equals(supported) ? 0 : value.distance;
    466         }
    467 
    468         public void copy(StringDistanceTable other) {
    469             for (Entry<String, Map<String, DistanceNode>> e1 : other.subtables.entrySet()) {
    470                 for (Entry<String, DistanceNode> e2 : e1.getValue().entrySet()) {
    471                     DistanceNode value = e2.getValue();
    472                     @SuppressWarnings("unused")
    473                     DistanceNode subNode = addSubtable(e1.getKey(), e2.getKey(), value.distance);
    474                 }
    475             }
    476         }
    477 
    478         @SuppressWarnings("unchecked")
    479         DistanceNode addSubtable(String desired, String supported, int distance) {
    480             Map<String, DistanceNode> sub2 = subtables.get(desired);
    481             if (sub2 == null) {
    482                 subtables.put(desired, sub2 = newMap());
    483             }
    484             DistanceNode oldNode = sub2.get(supported);
    485             if (oldNode != null) {
    486                 return oldNode;
    487             }
    488 
    489             final StringDistanceNode newNode = new StringDistanceNode(distance);
    490             sub2.put(supported, newNode);
    491             return newNode;
    492         }
    493 
    494         /**
    495          * Return null if value doesn't exist
    496          */
    497         private DistanceNode getNode(String desired, String supported) {
    498             Map<String, DistanceNode> sub2 = subtables.get(desired);
    499             if (sub2 == null) {
    500                 return null;
    501             }
    502             return sub2.get(supported);
    503         }
    504 
    505 
    506         /** add table for each subitem that matches and doesn't have a table already
    507          */
    508         public void addSubtables(
    509                 String desired, String supported,
    510                 Predicate<DistanceNode> action) {
    511             DistanceNode node = getNode(desired, supported);
    512             if (node == null) {
    513                 // get the distance it would have
    514                 Output<DistanceTable> node2 = new Output<DistanceTable>();
    515                 int distance = getDistance(desired, supported, node2, true);
    516                 // now add it
    517                 node = addSubtable(desired, supported, distance);
    518                 if (node2.value != null) {
    519                     ((StringDistanceNode)node).copyTables((StringDistanceTable)(node2.value));
    520                 }
    521             }
    522             action.test(node);
    523         }
    524 
    525         public void addSubtables(String desiredLang, String supportedLang,
    526                 String desiredScript, String supportedScript,
    527                 int percentage) {
    528 
    529             // add to all the values that have the matching desiredLang and supportedLang
    530             @SuppressWarnings("unused")
    531             boolean haveKeys = false;
    532             for (Entry<String, Map<String, DistanceNode>> e1 : subtables.entrySet()) {
    533                 String key1 = e1.getKey();
    534                 final boolean desiredIsKey = desiredLang.equals(key1);
    535                 if (desiredIsKey || desiredLang.equals(ANY)) {
    536                     for (Entry<String, DistanceNode> e2 : e1.getValue().entrySet()) {
    537                         String key2 = e2.getKey();
    538                         final boolean supportedIsKey = supportedLang.equals(key2);
    539                         haveKeys |= (desiredIsKey && supportedIsKey);
    540                         if (supportedIsKey || supportedLang.equals(ANY)) {
    541                             DistanceNode value = e2.getValue();
    542                             ((StringDistanceTable)value.getDistanceTable()).addSubtable(desiredScript, supportedScript, percentage);
    543                         }
    544                     }
    545                 }
    546             }
    547             // now add the sequence explicitly
    548             StringDistanceTable dt = new StringDistanceTable();
    549             dt.addSubtable(desiredScript, supportedScript, percentage);
    550             CopyIfEmpty r = new CopyIfEmpty(dt);
    551             addSubtables(desiredLang, supportedLang, r);
    552         }
    553 
    554         public void addSubtables(String desiredLang, String supportedLang,
    555                 String desiredScript, String supportedScript,
    556                 String desiredRegion, String supportedRegion,
    557                 int percentage) {
    558 
    559             // add to all the values that have the matching desiredLang and supportedLang
    560             @SuppressWarnings("unused")
    561             boolean haveKeys = false;
    562             for (Entry<String, Map<String, DistanceNode>> e1 : subtables.entrySet()) {
    563                 String key1 = e1.getKey();
    564                 final boolean desiredIsKey = desiredLang.equals(key1);
    565                 if (desiredIsKey || desiredLang.equals(ANY)) {
    566                     for (Entry<String, DistanceNode> e2 : e1.getValue().entrySet()) {
    567                         String key2 = e2.getKey();
    568                         final boolean supportedIsKey = supportedLang.equals(key2);
    569                         haveKeys |= (desiredIsKey && supportedIsKey);
    570                         if (supportedIsKey || supportedLang.equals(ANY)) {
    571                             StringDistanceNode value = (StringDistanceNode) e2.getValue();
    572                             ((StringDistanceTable)value.distanceTable).addSubtables(desiredScript, supportedScript, desiredRegion, supportedRegion, percentage);
    573                         }
    574                     }
    575                 }
    576             }
    577             // now add the sequence explicitly
    578 
    579             StringDistanceTable dt = new StringDistanceTable();
    580             dt.addSubtable(desiredRegion, supportedRegion, percentage);
    581             AddSub r = new AddSub(desiredScript, supportedScript, dt);
    582             addSubtables(desiredLang,  supportedLang,  r);
    583         }
    584 
    585         @Override
    586         public String toString() {
    587             return toString(false);
    588         }
    589 
    590         @Override
    591         public String toString(boolean abbreviate) {
    592             return toString(abbreviate, "", new IdMakerFull<Object>("interner"), new StringBuilder()).toString();
    593         }
    594 
    595         public StringBuilder toString(boolean abbreviate, String indent, IdMakerFull<Object> intern, StringBuilder buffer) {
    596             String indent2 = indent.isEmpty() ? "" : "\t";
    597             Integer id = abbreviate ? intern.getOldAndAdd(subtables) : null;
    598             if (id != null) {
    599                 buffer.append(indent2).append('#').append(id).append('\n');
    600             } else for (Entry<String, Map<String, DistanceNode>> e1 : subtables.entrySet()) {
    601                 final Map<String, DistanceNode> subsubtable = e1.getValue();
    602                 buffer.append(indent2).append(e1.getKey());
    603                 String indent3 = "\t";
    604                 id = abbreviate ? intern.getOldAndAdd(subsubtable) : null;
    605                 if (id != null) {
    606                     buffer.append(indent3).append('#').append(id).append('\n');
    607                 } else for (Entry<String, DistanceNode> e2 : subsubtable.entrySet()) {
    608                     DistanceNode value = e2.getValue();
    609                     buffer.append(indent3).append(e2.getKey());
    610                     id = abbreviate ? intern.getOldAndAdd(value) : null;
    611                     if (id != null) {
    612                         buffer.append('\t').append('#').append(id).append('\n');
    613                     } else {
    614                         buffer.append('\t').append(value.distance);
    615                         final DistanceTable distanceTable = value.getDistanceTable();
    616                         if (distanceTable != null) {
    617                             id = abbreviate ? intern.getOldAndAdd(distanceTable) : null;
    618                             if (id != null) {
    619                                 buffer.append('\t').append('#').append(id).append('\n');
    620                             } else {
    621                                 ((StringDistanceTable)distanceTable).toString(abbreviate, indent+"\t\t\t", intern, buffer);
    622                             }
    623                         } else {
    624                             buffer.append('\n');
    625                         }
    626                     }
    627                     indent3 = indent+'\t';
    628                 }
    629                 indent2 = indent;
    630             }
    631             return buffer;
    632         }
    633 
    634         @Override
    635         public StringDistanceTable compact() {
    636             return new CompactAndImmutablizer().compact(this);
    637         }
    638 
    639         @Override
    640         public Set<String> getCloser(int threshold) {
    641             Set<String> result = new HashSet<String>();
    642             for (Entry<String, Map<String, DistanceNode>> e1 : subtables.entrySet()) {
    643                 String desired = e1.getKey();
    644                 for (Entry<String, DistanceNode> e2 : e1.getValue().entrySet()) {
    645                     if (e2.getValue().distance < threshold) {
    646                         result.add(desired);
    647                         break;
    648                     }
    649                 }
    650             }
    651             return result;
    652         }
    653 
    654         public Integer getInternalDistance(String a, String b) {
    655             Map<String, DistanceNode> subsub = subtables.get(a);
    656             if (subsub == null) {
    657                 return null;
    658             }
    659             DistanceNode dnode = subsub.get(b);
    660             return dnode == null ? null : dnode.distance;
    661         }
    662 
    663         @Override
    664         public DistanceNode getInternalNode(String a, String b) {
    665             Map<String, DistanceNode> subsub = subtables.get(a);
    666             if (subsub == null) {
    667                 return null;
    668             }
    669             return subsub.get(b);
    670         }
    671 
    672         @Override
    673         public Map<String, Set<String>> getInternalMatches() {
    674             Map<String, Set<String>> result = new LinkedHashMap<String, Set<String>>();
    675             for (Entry<String, Map<String, DistanceNode>> entry : subtables.entrySet()) {
    676                 result.put(entry.getKey(), new LinkedHashSet<String>(entry.getValue().keySet()));
    677             }
    678             return result;
    679         }
    680     }
    681 
    682     static class CopyIfEmpty implements Predicate<DistanceNode> {
    683         private final StringDistanceTable toCopy;
    684         CopyIfEmpty(StringDistanceTable resetIfNotNull) {
    685             this.toCopy = resetIfNotNull;
    686         }
    687         @Override
    688         public boolean test(DistanceNode node) {
    689             final StringDistanceTable subtables = (StringDistanceTable) node.getDistanceTable();
    690             if (subtables.subtables.isEmpty()) {
    691                 subtables.copy(toCopy);
    692             }
    693             return true;
    694         }
    695     }
    696 
    697     static class AddSub implements Predicate<DistanceNode> {
    698         private final String desiredSub;
    699         private final String supportedSub;
    700         private final CopyIfEmpty r;
    701 
    702         AddSub(String desiredSub, String supportedSub, StringDistanceTable distanceTableToCopy) {
    703             this.r = new CopyIfEmpty(distanceTableToCopy);
    704             this.desiredSub = desiredSub;
    705             this.supportedSub = supportedSub;
    706         }
    707         @Override
    708         public boolean test(DistanceNode node) {
    709             if (node == null) {
    710                 throw new IllegalArgumentException("bad structure");
    711             } else {
    712                 ((StringDistanceNode)node).addSubtables(desiredSub, supportedSub, r);
    713             }
    714             return true;
    715         }
    716     }
    717 
    718     public int distance(ULocale desired, ULocale supported, int threshold, DistanceOption distanceOption) {
    719         LSR supportedLSR = LSR.fromMaximalized(supported);
    720         LSR desiredLSR = LSR.fromMaximalized(desired);
    721         return distanceRaw(desiredLSR, supportedLSR, threshold, distanceOption);
    722     }
    723 
    724     /**
    725      * Returns distance, from 0 to ABOVE_THRESHOLD.
    726      * ULocales must be in canonical, addLikelySubtags format. Returns distance
    727      */
    728     public int distanceRaw(LSR desired, LSR supported, int threshold, DistanceOption distanceOption) {
    729         return distanceRaw(desired.language, supported.language,
    730                 desired.script, supported.script,
    731                 desired.region, supported.region,
    732                 threshold, distanceOption);
    733     }
    734 
    735     public enum DistanceOption {NORMAL, SCRIPT_FIRST}
    736 
    737     /**
    738      * Returns distance, from 0 to ABOVE_THRESHOLD.
    739      * ULocales must be in canonical, addLikelySubtags format. Returns distance
    740      */
    741     public int distanceRaw(
    742             String desiredLang, String supportedlang,
    743             String desiredScript, String supportedScript,
    744             String desiredRegion, String supportedRegion,
    745             int threshold,
    746             DistanceOption distanceOption) {
    747 
    748         Output<DistanceTable> subtable = new Output<DistanceTable>();
    749 
    750         int distance = languageDesired2Supported.getDistance(desiredLang, supportedlang, subtable, true);
    751         boolean scriptFirst = distanceOption == DistanceOption.SCRIPT_FIRST;
    752         if (scriptFirst) {
    753             distance >>= 2;
    754         }
    755         if (distance < 0) {
    756             distance = 0;
    757         } else if (distance >= threshold) {
    758             return ABOVE_THRESHOLD;
    759         }
    760 
    761         int scriptDistance = subtable.value.getDistance(desiredScript, supportedScript, subtable, true);
    762         if (scriptFirst) {
    763             scriptDistance >>= 1;
    764         }
    765         distance += scriptDistance;
    766         if (distance >= threshold) {
    767             return ABOVE_THRESHOLD;
    768         }
    769 
    770         if (desiredRegion.equals(supportedRegion)) {
    771             return distance;
    772         }
    773 
    774         // From here on we know the regions are not equal
    775 
    776         final String desiredPartition = regionMapper.toId(desiredRegion);
    777         final String supportedPartition = regionMapper.toId(supportedRegion);
    778         int subdistance;
    779 
    780         // check for macros. If one is found, we take the maximum distance
    781         // this could be optimized by adding some more structure, but probably not worth it.
    782 
    783         Collection<String> desiredPartitions = desiredPartition.isEmpty() ? regionMapper.macroToPartitions.get(desiredRegion) : null;
    784         Collection<String> supportedPartitions = supportedPartition.isEmpty() ? regionMapper.macroToPartitions.get(supportedRegion) : null;
    785         if (desiredPartitions != null || supportedPartitions != null) {
    786             subdistance = 0;
    787             // make the code simple for now
    788             if (desiredPartitions == null) {
    789                 desiredPartitions = Collections.singleton(desiredPartition);
    790             }
    791             if (supportedPartitions == null) {
    792                 supportedPartitions = Collections.singleton(supportedPartition);
    793             }
    794 
    795             for (String desiredPartition2 : desiredPartitions) {
    796                 for (String supportedPartition2 : supportedPartitions) {
    797                     int tempSubdistance = subtable.value.getDistance(desiredPartition2, supportedPartition2, null, false);
    798                     if (subdistance < tempSubdistance) {
    799                         subdistance = tempSubdistance;
    800                     }
    801                 }
    802             }
    803         } else {
    804             subdistance = subtable.value.getDistance(desiredPartition, supportedPartition, null, false);
    805         }
    806         distance += subdistance;
    807         return distance >= threshold ? ABOVE_THRESHOLD : distance;
    808     }
    809 
    810 
    811     private static final XLocaleDistance DEFAULT;
    812 
    813     public static XLocaleDistance getDefault() {
    814         return DEFAULT;
    815     }
    816 
    817     static {
    818         String[][] variableOverrides = {
    819                 {"$enUS", "AS+GU+MH+MP+PR+UM+US+VI"},
    820 
    821                 {"$cnsar", "HK+MO"},
    822 
    823                 {"$americas", "019"},
    824 
    825                 {"$maghreb", "MA+DZ+TN+LY+MR+EH"},
    826         };
    827         String[] paradigmRegions = {
    828                 "en", "en-GB", "es", "es-419", "pt-BR", "pt-PT"
    829         };
    830         String[][] regionRuleOverrides = {
    831                 {"ar_*_$maghreb", "ar_*_$maghreb", "96"},
    832                 {"ar_*_$!maghreb", "ar_*_$!maghreb", "96"},
    833                 {"ar_*_*", "ar_*_*", "95"},
    834 
    835                 {"en_*_$enUS", "en_*_$enUS", "96"},
    836                 {"en_*_$!enUS", "en_*_$!enUS", "96"},
    837                 {"en_*_*", "en_*_*", "95"},
    838 
    839                 {"es_*_$americas", "es_*_$americas", "96"},
    840                 {"es_*_$!americas", "es_*_$!americas", "96"},
    841                 {"es_*_*", "es_*_*", "95"},
    842 
    843                 {"pt_*_$americas", "pt_*_$americas", "96"},
    844                 {"pt_*_$!americas", "pt_*_$!americas", "96"},
    845                 {"pt_*_*", "pt_*_*", "95"},
    846 
    847                 {"zh_Hant_$cnsar", "zh_Hant_$cnsar", "96"},
    848                 {"zh_Hant_$!cnsar", "zh_Hant_$!cnsar", "96"},
    849                 {"zh_Hant_*", "zh_Hant_*", "95"},
    850 
    851                 {"*_*_*", "*_*_*", "96"},
    852         };
    853 
    854         Builder rmb = new RegionMapper.Builder().addParadigms(paradigmRegions);
    855         for (String[] variableRule : variableOverrides) {
    856             rmb.add(variableRule[0], variableRule[1]);
    857         }
    858         if (PRINT_OVERRIDES) {
    859             System.out.println("\t\t<languageMatches type=\"written\" alt=\"enhanced\">");
    860             System.out.println("\t\t\t<paradigmLocales locales=\"" + XCldrStub.join(paradigmRegions, " ")
    861             + "\"/>");
    862             for (String[] variableRule : variableOverrides) {
    863                 System.out.println("\t\t\t<matchVariable id=\"" + variableRule[0]
    864                         + "\" value=\""
    865                         + variableRule[1]
    866                                 + "\"/>");
    867             }
    868         }
    869 
    870         final StringDistanceTable defaultDistanceTable = new StringDistanceTable();
    871         final RegionMapper defaultRegionMapper = rmb.build();
    872 
    873         Splitter bar = Splitter.on('_');
    874 
    875         @SuppressWarnings({"unchecked", "rawtypes"})
    876         List<Row.R4<List<String>, List<String>, Integer, Boolean>>[] sorted = new ArrayList[3];
    877         sorted[0] = new ArrayList<Row.R4<List<String>, List<String>, Integer, Boolean>>();
    878         sorted[1] = new ArrayList<Row.R4<List<String>, List<String>, Integer, Boolean>>();
    879         sorted[2] = new ArrayList<Row.R4<List<String>, List<String>, Integer, Boolean>>();
    880 
    881         // sort the rules so that the language-only are first, then the language-script, and finally the language-script-region.
    882         for (R4<String, String, Integer, Boolean> info : xGetLanguageMatcherData()) {
    883             String desiredRaw = info.get0();
    884             String supportedRaw = info.get1();
    885             List<String> desired = bar.splitToList(desiredRaw);
    886             List<String> supported = bar.splitToList(supportedRaw);
    887             Boolean oneway = info.get3();
    888             int distance = desiredRaw.equals("*_*") ? 50 : info.get2();
    889             int size = desired.size();
    890 
    891             // for now, skip size == 3
    892             if (size == 3) continue;
    893 
    894             sorted[size-1].add(Row.of(desired, supported, distance, oneway));
    895         }
    896 
    897         for (List<Row.R4<List<String>, List<String>, Integer, Boolean>> item1 : sorted) {
    898             for (Row.R4<List<String>, List<String>, Integer, Boolean> item2 : item1) {
    899                 List<String> desired = item2.get0();
    900                 List<String> supported = item2.get1();
    901                 Integer distance = item2.get2();
    902                 Boolean oneway = item2.get3();
    903                 add(defaultDistanceTable, desired, supported, distance);
    904                 if (oneway != Boolean.TRUE && !desired.equals(supported)) {
    905                     add(defaultDistanceTable, supported, desired, distance);
    906                 }
    907                 printMatchXml(desired, supported, distance, oneway);
    908             }
    909         }
    910 
    911         // add new size=3
    912         for (String[] rule : regionRuleOverrides) {
    913             //            if (PRINT_OVERRIDES) System.out.println("\t\t\t<languageMatch desired=\""
    914             //                + rule[0]
    915             //                    + "\" supported=\""
    916             //                    + rule[1]
    917             //                        + "\" distance=\""
    918             //                        + rule[2]
    919             //                            + "\"/>");
    920             //            if (rule[0].equals("en_*_*") || rule[1].equals("*_*_*")) {
    921             //                int debug = 0;
    922             //            }
    923             List<String> desiredBase = new ArrayList<String>(bar.splitToList(rule[0]));
    924             List<String> supportedBase = new ArrayList<String>(bar.splitToList(rule[1]));
    925             Integer distance = 100-Integer.parseInt(rule[2]);
    926             printMatchXml(desiredBase, supportedBase, distance, false);
    927 
    928             Collection<String> desiredRegions = defaultRegionMapper.getIdsFromVariable(desiredBase.get(2));
    929             if (desiredRegions.isEmpty()) {
    930                 throw new IllegalArgumentException("Bad region variable: " + desiredBase.get(2));
    931             }
    932             Collection<String> supportedRegions = defaultRegionMapper.getIdsFromVariable(supportedBase.get(2));
    933             if (supportedRegions.isEmpty()) {
    934                 throw new IllegalArgumentException("Bad region variable: " + supportedBase.get(2));
    935             }
    936             for (String desiredRegion2 : desiredRegions) {
    937                 desiredBase.set(2, desiredRegion2.toString()); // fix later
    938                 for (String supportedRegion2 : supportedRegions) {
    939                     supportedBase.set(2, supportedRegion2.toString()); // fix later
    940                     add(defaultDistanceTable, desiredBase, supportedBase, distance);
    941                     add(defaultDistanceTable, supportedBase, desiredBase, distance);
    942                 }
    943             }
    944         }
    945         if (PRINT_OVERRIDES) {
    946             System.out.println("\t\t</languageMatches>");
    947         }
    948 
    949         DEFAULT = new XLocaleDistance(defaultDistanceTable.compact(), defaultRegionMapper);
    950 
    951         if (PRINT_OVERRIDES) {
    952             System.out.println(defaultRegionMapper);
    953             System.out.println(defaultDistanceTable);
    954             throw new IllegalArgumentException();
    955         }
    956     }
    957 
    958     private static void printMatchXml(List<String> desired, List<String> supported, Integer distance, Boolean oneway) {
    959         if (PRINT_OVERRIDES) {
    960             String desiredStr = CollectionUtilities.join(desired, "_");
    961             String supportedStr = CollectionUtilities.join(supported, "_");
    962             String desiredName = fixedName(desired);
    963             String supportedName = fixedName(supported);
    964             System.out.println("\t\t\t<languageMatch"
    965                     + " desired=\"" + desiredStr
    966                     + "\"\tsupported=\"" + supportedStr
    967                     + "\"\tdistance=\"" + distance
    968                     + (!oneway ? "" : "\"\toneway=\"true")
    969                     + "\"/>\t<!-- " + desiredName + "  " + supportedName + " -->");
    970         }
    971     }
    972 
    973     private static String fixedName(List<String> match) {
    974         List<String> alt = new ArrayList<String>(match);
    975         int size = alt.size();
    976         assert size >= 1 && size <= 3;
    977 
    978         StringBuilder result = new StringBuilder();
    979 
    980         if (size >= 3) {
    981             String region = alt.get(2);
    982             if (region.equals("*") || region.startsWith("$")) {
    983                 result.append(region);
    984             } else {
    985                 result.append(english.regionDisplayName(region));
    986             }
    987         }
    988         if (size >= 2) {
    989             String script = alt.get(1);
    990             if (script.equals("*")) {
    991                 result.insert(0, script);
    992             } else {
    993                 result.insert(0, english.scriptDisplayName(script));
    994             }
    995         }
    996         if (size >= 1) {
    997             String language = alt.get(0);
    998             if (language.equals("*")) {
    999                 result.insert(0, language);
   1000             } else {
   1001                 result.insert(0, english.languageDisplayName(language));
   1002             }
   1003         }
   1004         return CollectionUtilities.join(alt, "; ");
   1005     }
   1006 
   1007     static public void add(StringDistanceTable languageDesired2Supported, List<String> desired, List<String> supported, int percentage) {
   1008         int size = desired.size();
   1009         if (size != supported.size() || size < 1 || size > 3) {
   1010             throw new IllegalArgumentException();
   1011         }
   1012         final String desiredLang = fixAny(desired.get(0));
   1013         final String supportedLang = fixAny(supported.get(0));
   1014         if (size == 1) {
   1015             languageDesired2Supported.addSubtable(desiredLang, supportedLang, percentage);
   1016         } else {
   1017             final String desiredScript = fixAny(desired.get(1));
   1018             final String supportedScript = fixAny(supported.get(1));
   1019             if (size == 2) {
   1020                 languageDesired2Supported.addSubtables(desiredLang, supportedLang, desiredScript, supportedScript, percentage);
   1021             } else {
   1022                 final String desiredRegion = fixAny(desired.get(2));
   1023                 final String supportedRegion = fixAny(supported.get(2));
   1024                 languageDesired2Supported.addSubtables(desiredLang, supportedLang, desiredScript, supportedScript, desiredRegion, supportedRegion, percentage);
   1025             }
   1026         }
   1027     }
   1028 
   1029     @Override
   1030     public String toString() {
   1031         return toString(false);
   1032     }
   1033 
   1034     public String toString(boolean abbreviate) {
   1035         return regionMapper + "\n" + languageDesired2Supported.toString(abbreviate);
   1036     }
   1037 
   1038 
   1039     //    public static XLocaleDistance createDefaultInt() {
   1040     //        IntDistanceTable d = new IntDistanceTable(DEFAULT_DISTANCE_TABLE);
   1041     //        return new XLocaleDistance(d, DEFAULT_REGION_MAPPER);
   1042     //    }
   1043 
   1044     static Set<String> getContainingMacrosFor(Collection<String> input, Set<String> output) {
   1045         output.clear();
   1046         for (Entry<String, Set<String>> entry : CONTAINER_TO_CONTAINED.asMap().entrySet()) {
   1047             if (input.containsAll(entry.getValue())) { // example; if all southern Europe are contained, then add S. Europe
   1048                 output.add(entry.getKey());
   1049             }
   1050         }
   1051         return output;
   1052     }
   1053 
   1054     static class RegionMapper implements IdMapper<String,String> {
   1055         /**
   1056          * 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}
   1057          * When we hit a rule that contains a variable, we replace that rule by multiple rules for the partitions.
   1058          */
   1059         final Multimap<String,String> variableToPartition;
   1060         /**
   1061          * Used for executing the rules. We map a region to a partition before processing.
   1062          */
   1063         final Map<String,String> regionToPartition;
   1064         /**
   1065          * Used to support es_419 compared to es_AR, etc.
   1066          */
   1067         final Multimap<String,String> macroToPartitions;
   1068         /**
   1069          * Used to get the paradigm region for a cluster, if there is one
   1070          */
   1071         final Set<ULocale> paradigms;
   1072 
   1073         private RegionMapper(
   1074                 Multimap<String, String> variableToPartitionIn,
   1075                 Map<String, String> regionToPartitionIn,
   1076                 Multimap<String,String> macroToPartitionsIn,
   1077                 Set<ULocale> paradigmsIn) {
   1078             variableToPartition = ImmutableMultimap.copyOf(variableToPartitionIn);
   1079             regionToPartition = ImmutableMap.copyOf(regionToPartitionIn);
   1080             macroToPartitions = ImmutableMultimap.copyOf(macroToPartitionsIn);
   1081             paradigms = ImmutableSet.copyOf(paradigmsIn);
   1082         }
   1083 
   1084         @Override
   1085         public String toId(String region) {
   1086             String result = regionToPartition.get(region);
   1087             return result == null ? "" : result;
   1088         }
   1089 
   1090         public Collection<String> getIdsFromVariable(String variable) {
   1091             if (variable.equals("*")) {
   1092                 return Collections.singleton("*");
   1093             }
   1094             Collection<String> result = variableToPartition.get(variable);
   1095             if (result == null || result.isEmpty()) {
   1096                 throw new IllegalArgumentException("Variable not defined: " + variable);
   1097             }
   1098             return result;
   1099         }
   1100 
   1101         public Set<String> regions() {
   1102             return regionToPartition.keySet();
   1103         }
   1104 
   1105         public Set<String> variables() {
   1106             return variableToPartition.keySet();
   1107         }
   1108 
   1109         @Override
   1110         public String toString() {
   1111             TreeMultimap<String, String> partitionToVariables = Multimaps.invertFrom(variableToPartition,
   1112                     TreeMultimap.<String, String>create());
   1113             TreeMultimap<String, String> partitionToRegions = TreeMultimap.create();
   1114             for (Entry<String, String> e : regionToPartition.entrySet()) {
   1115                 partitionToRegions.put(e.getValue(), e.getKey());
   1116             }
   1117             StringBuilder buffer = new StringBuilder();
   1118             buffer.append("Partition  Variables  Regions (final)");
   1119             for (Entry<String, Set<String>> e : partitionToVariables.asMap().entrySet()) {
   1120                 buffer.append('\n');
   1121                 buffer.append(e.getKey() + "\t" + e.getValue() + "\t" + partitionToRegions.get(e.getKey()));
   1122             }
   1123             buffer.append("\nMacro  Partitions");
   1124             for (Entry<String, Set<String>> e : macroToPartitions.asMap().entrySet()) {
   1125                 buffer.append('\n');
   1126                 buffer.append(e.getKey() + "\t" + e.getValue());
   1127             }
   1128 
   1129             return buffer.toString();
   1130         }
   1131 
   1132         static class Builder {
   1133             final private Multimap<String, String> regionToRawPartition = TreeMultimap.create();
   1134             final private RegionSet regionSet = new RegionSet();
   1135             final private Set<ULocale> paradigms = new LinkedHashSet<ULocale>();
   1136 
   1137             void add(String variable, String barString) {
   1138                 Set<String> tempRegions = regionSet.parseSet(barString);
   1139 
   1140                 for (String region : tempRegions) {
   1141                     regionToRawPartition.put(region, variable);
   1142                 }
   1143 
   1144                 // now add the inverse variable
   1145 
   1146                 Set<String> inverse = regionSet.inverse();
   1147                 String inverseVariable = "$!" + variable.substring(1);
   1148                 for (String region : inverse) {
   1149                     regionToRawPartition.put(region, inverseVariable);
   1150                 }
   1151             }
   1152 
   1153             public Builder addParadigms(String... paradigmRegions) {
   1154                 for (String paradigm : paradigmRegions) {
   1155                     paradigms.add(new ULocale(paradigm));
   1156                 }
   1157                 return this;
   1158             }
   1159 
   1160             RegionMapper build() {
   1161                 final IdMakerFull<Collection<String>> id = new IdMakerFull<Collection<String>>("partition");
   1162                 Multimap<String,String> variableToPartitions = TreeMultimap.create();
   1163                 Map<String,String> regionToPartition = new TreeMap<String,String>();
   1164                 Multimap<String,String> partitionToRegions = TreeMultimap.create();
   1165 
   1166                 for (Entry<String, Set<String>> e : regionToRawPartition.asMap().entrySet()) {
   1167                     final String region = e.getKey();
   1168                     final Collection<String> rawPartition = e.getValue();
   1169                     String partition = String.valueOf((char)('' + id.add(rawPartition)));
   1170 
   1171                     regionToPartition.put(region, partition);
   1172                     partitionToRegions.put(partition, region);
   1173 
   1174                     for (String variable : rawPartition) {
   1175                         variableToPartitions.put(variable, partition);
   1176                     }
   1177                 }
   1178 
   1179                 // we get a mapping of each macro to the partitions it intersects with
   1180                 Multimap<String,String> macroToPartitions = TreeMultimap.create();
   1181                 for (Entry<String, Set<String>> e : CONTAINER_TO_CONTAINED.asMap().entrySet()) {
   1182                     String macro = e.getKey();
   1183                     for (Entry<String, Set<String>> e2 : partitionToRegions.asMap().entrySet()) {
   1184                         String partition = e2.getKey();
   1185                         if (!Collections.disjoint(e.getValue(), e2.getValue())) {
   1186                             macroToPartitions.put(macro, partition);
   1187                         }
   1188                     }
   1189                 }
   1190 
   1191                 return new RegionMapper(
   1192                         variableToPartitions,
   1193                         regionToPartition,
   1194                         macroToPartitions,
   1195                         paradigms);
   1196             }
   1197         }
   1198     }
   1199 
   1200     /**
   1201      * Parses a string of regions like "US+005-BR" and produces a set of resolved regions.
   1202      * All macroregions are fully resolved to sets of non-macro regions.
   1203      * <br>Syntax is simple for now:
   1204      * <pre>regionSet := region ([-+] region)*</pre>
   1205      * No precedence, so "x+y-y+z" is (((x+y)-y)+z) NOT (x+y)-(y+z)
   1206      */
   1207     private static class RegionSet {
   1208         private enum Operation {add, remove}
   1209         // temporaries used in processing
   1210         final private Set<String> tempRegions = new TreeSet<String>();
   1211         private Operation operation = null;
   1212 
   1213         private Set<String> parseSet(String barString) {
   1214             operation = Operation.add;
   1215             int last = 0;
   1216             tempRegions.clear();
   1217             int i = 0;
   1218             for (; i < barString.length(); ++i) {
   1219                 char c = barString.charAt(i); // UTF16 is ok, since syntax is only ascii
   1220                 switch(c) {
   1221                 case '+':
   1222                     add(barString, last, i);
   1223                     last = i+1;
   1224                     operation = Operation.add;
   1225                     break;
   1226                 case '-':
   1227                     add(barString, last, i);
   1228                     last = i+1;
   1229                     operation = Operation.remove;
   1230                     break;
   1231                 }
   1232             }
   1233             add(barString, last, i);
   1234             return tempRegions;
   1235         }
   1236 
   1237         private Set<String> inverse() {
   1238             TreeSet<String> result = new TreeSet<String>(ALL_FINAL_REGIONS);
   1239             result.removeAll(tempRegions);
   1240             return result;
   1241         }
   1242 
   1243         private void add(String barString, int last, int i) {
   1244             if (i > last) {
   1245                 String region = barString.substring(last,i);
   1246                 changeSet(operation, region);
   1247             }
   1248         }
   1249 
   1250         private void changeSet(Operation operation, String region) {
   1251             Collection<String> contained = CONTAINER_TO_CONTAINED_FINAL.get(region);
   1252             if (contained != null && !contained.isEmpty()) {
   1253                 if (Operation.add == operation) {
   1254                     tempRegions.addAll(contained);
   1255                 } else {
   1256                     tempRegions.removeAll(contained);
   1257                 }
   1258             } else if (Operation.add == operation) {
   1259                 tempRegions.add(region);
   1260             } else {
   1261                 tempRegions.remove(region);
   1262             }
   1263         }
   1264     }
   1265 
   1266     public static <K,V> Multimap<K,V> invertMap(Map<V,K> map) {
   1267         return Multimaps.invertFrom(Multimaps.forMap(map), LinkedHashMultimap.<K,V>create());
   1268     }
   1269 
   1270     public Set<ULocale> getParadigms() {
   1271         return regionMapper.paradigms;
   1272     }
   1273 
   1274     public int getDefaultLanguageDistance() {
   1275         return defaultLanguageDistance;
   1276     }
   1277 
   1278     public int getDefaultScriptDistance() {
   1279         return defaultScriptDistance;
   1280     }
   1281 
   1282     public int getDefaultRegionDistance() {
   1283         return defaultRegionDistance;
   1284     }
   1285 
   1286     static class CompactAndImmutablizer extends IdMakerFull<Object> {
   1287         StringDistanceTable compact(StringDistanceTable item) {
   1288             if (toId(item) != null) {
   1289                 return (StringDistanceTable) intern(item);
   1290             }
   1291             return new StringDistanceTable(compact(item.subtables, 0));
   1292         }
   1293         @SuppressWarnings({ "unchecked", "rawtypes" })
   1294         <K,T> Map<K,T> compact(Map<K,T> item, int level) {
   1295             if (toId(item) != null) {
   1296                 return (Map<K, T>) intern(item);
   1297             }
   1298             Map<K,T> copy = new LinkedHashMap<K,T>();
   1299             for (Entry<K,T> entry : item.entrySet()) {
   1300                 T value = entry.getValue();
   1301                 if (value instanceof Map) {
   1302                     copy.put(entry.getKey(), (T)compact((Map)value, level+1));
   1303                 } else {
   1304                     copy.put(entry.getKey(), (T)compact((DistanceNode)value));
   1305                 }
   1306             }
   1307             return ImmutableMap.copyOf(copy);
   1308         }
   1309         DistanceNode compact(DistanceNode item) {
   1310             if (toId(item) != null) {
   1311                 return (DistanceNode) intern(item);
   1312             }
   1313             final DistanceTable distanceTable = item.getDistanceTable();
   1314             if (distanceTable == null || distanceTable.isEmpty()) {
   1315                 return new DistanceNode(item.distance);
   1316             } else {
   1317                 return new StringDistanceNode(item.distance, compact((StringDistanceTable)((StringDistanceNode)item).distanceTable));
   1318             }
   1319         }
   1320     }
   1321 
   1322     @Deprecated
   1323     public StringDistanceTable internalGetDistanceTable() {
   1324         return (StringDistanceTable) languageDesired2Supported;
   1325     }
   1326 
   1327     public static void main(String[] args) {
   1328         //      for (Entry<String, Collection<String>> entry : containerToContained.asMap().entrySet()) {
   1329         //          System.out.println(entry.getKey() + "\t" + entry.getValue() + "; " + containerToFinalContained.get(entry.getKey()));
   1330         //      }
   1331         //      final Multimap<String,String> regionToMacros = ImmutableMultimap.copyOf(Multimaps.invertFrom(containerToContained, TreeMultimap.create()));
   1332         //      for (Entry<String, Collection<String>> entry : regionToMacros.asMap().entrySet()) {
   1333         //          System.out.println(entry.getKey() + "\t " + entry.getValue());
   1334         //      }
   1335         if (PRINT_OVERRIDES) {
   1336             System.out.println(getDefault().toString(true));
   1337         }
   1338         DistanceTable table = getDefault().languageDesired2Supported;
   1339         DistanceTable compactedTable = table.compact();
   1340         if (!table.equals(compactedTable)) {
   1341             throw new IllegalArgumentException("Compaction isn't equal");
   1342         }
   1343     }
   1344 }
   1345