• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.unicode.cldr.util;
2 
3 import java.io.File;
4 import java.io.IOException;
5 import java.text.ParseException;
6 import java.util.ArrayList;
7 import java.util.Arrays;
8 import java.util.Collection;
9 import java.util.Collections;
10 import java.util.Comparator;
11 import java.util.Date;
12 import java.util.Deque;
13 import java.util.EnumMap;
14 import java.util.EnumSet;
15 import java.util.HashMap;
16 import java.util.HashSet;
17 import java.util.Iterator;
18 import java.util.LinkedHashMap;
19 import java.util.LinkedHashSet;
20 import java.util.LinkedList;
21 import java.util.List;
22 import java.util.Locale;
23 import java.util.Map;
24 import java.util.Map.Entry;
25 import java.util.Objects;
26 import java.util.Set;
27 import java.util.SortedSet;
28 import java.util.TreeMap;
29 import java.util.TreeSet;
30 import java.util.concurrent.ConcurrentHashMap;
31 import java.util.regex.Matcher;
32 import java.util.regex.Pattern;
33 
34 import org.unicode.cldr.test.CoverageLevel2;
35 import org.unicode.cldr.tool.LikelySubtags;
36 import org.unicode.cldr.tool.SubdivisionNames;
37 import org.unicode.cldr.util.Builder.CBuilder;
38 import org.unicode.cldr.util.CldrUtility.VariableReplacer;
39 import org.unicode.cldr.util.DayPeriodInfo.DayPeriod;
40 import org.unicode.cldr.util.StandardCodes.LstrType;
41 import org.unicode.cldr.util.SupplementalDataInfo.BasicLanguageData.Type;
42 import org.unicode.cldr.util.SupplementalDataInfo.NumberingSystemInfo.NumberingSystemType;
43 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo.Count;
44 import org.unicode.cldr.util.Validity.Status;
45 
46 import com.google.common.base.Splitter;
47 import com.google.common.collect.ImmutableList;
48 import com.google.common.collect.ImmutableSet;
49 import com.google.common.collect.ImmutableSetMultimap;
50 import com.google.common.collect.Multimap;
51 import com.google.common.collect.TreeMultimap;
52 import com.ibm.icu.dev.util.CollectionUtilities;
53 import com.ibm.icu.impl.IterableComparator;
54 import com.ibm.icu.impl.Relation;
55 import com.ibm.icu.impl.Row;
56 import com.ibm.icu.impl.Row.R2;
57 import com.ibm.icu.impl.Row.R4;
58 import com.ibm.icu.text.DateFormat;
59 import com.ibm.icu.text.MessageFormat;
60 import com.ibm.icu.text.NumberFormat;
61 import com.ibm.icu.text.PluralRules;
62 import com.ibm.icu.text.PluralRules.FixedDecimal;
63 import com.ibm.icu.text.PluralRules.FixedDecimalRange;
64 import com.ibm.icu.text.PluralRules.FixedDecimalSamples;
65 import com.ibm.icu.text.PluralRules.SampleType;
66 import com.ibm.icu.text.SimpleDateFormat;
67 import com.ibm.icu.text.UnicodeSet;
68 import com.ibm.icu.util.Freezable;
69 import com.ibm.icu.util.ICUUncheckedIOException;
70 import com.ibm.icu.util.Output;
71 import com.ibm.icu.util.TimeZone;
72 import com.ibm.icu.util.ULocale;
73 import com.ibm.icu.util.VersionInfo;
74 
75 /**
76  * Singleton class to provide API access to supplemental data -- in all the supplemental data files.
77  * <p>
78  * To create, use SupplementalDataInfo.getInstance
79  * <p>
80  * To add API for new structure, you will generally:
81  * <ul>
82  * <li>add a Map or Relation as a data member,
83  * <li>put a check and handler in MyHandler for the paths that you consume,
84  * <li>make the data member immutable in makeStuffSave, and
85  * <li>add a getter for the data member
86  * </ul>
87  *
88  * @author markdavis
89  */
90 
91 public class SupplementalDataInfo {
92     private static final boolean DEBUG = false;
93     private static final StandardCodes sc = StandardCodes.make();
94     private static final String UNKNOWN_SCRIPT = "Zzzz";
95 
96     // TODO add structure for items shown by TestSupplementalData to be missing
97     /*
98      * [calendarData/calendar,
99      * characters/character-fallback,
100      * measurementData/measurementSystem, measurementData/paperSize,
101      * metadata/attributeOrder, metadata/blocking, metadata/deprecated,
102      * metadata/distinguishing, metadata/elementOrder, metadata/serialElements, metadata/skipDefaultLocale,
103      * metadata/suppress, metadata/validity, metazoneInfo/timezone,
104      * timezoneData/mapTimezones,
105      * weekData/firstDay, weekData/minDays, weekData/weekendEnd, weekData/weekendStart]
106      */
107     // TODO: verify that we get everything by writing the files solely from the API, and verifying identity.
108 
109     /**
110      * Official status of languages
111      */
112     public enum OfficialStatus {
113         unknown("U", 1), recognized("R", 1), official_minority("OM", 2), official_regional("OR", 3), de_facto_official("OD", 10), official("O", 10);
114 
115         private final String shortName;
116         private final int weight;
117 
OfficialStatus(String shortName, int weight)118         private OfficialStatus(String shortName, int weight) {
119             this.shortName = shortName;
120             this.weight = weight;
121         }
122 
toShortString()123         public String toShortString() {
124             return shortName;
125         }
126 
getWeight()127         public int getWeight() {
128             return weight;
129         }
130 
isMajor()131         public boolean isMajor() {
132             return compareTo(OfficialStatus.de_facto_official) >= 0;
133         }
134 
isOfficial()135         public boolean isOfficial() {
136             return compareTo(OfficialStatus.official_regional) >= 0;
137         }
138     };
139 
140     /**
141      * Population data for different languages.
142      */
143     public static final class PopulationData implements Freezable<PopulationData> {
144         private double population = Double.NaN;
145 
146         private double literatePopulation = Double.NaN;
147 
148         private double writingPopulation = Double.NaN;
149 
150         private double gdp = Double.NaN;
151 
152         private OfficialStatus officialStatus = OfficialStatus.unknown;
153 
getGdp()154         public double getGdp() {
155             return gdp;
156         }
157 
getLiteratePopulation()158         public double getLiteratePopulation() {
159             return literatePopulation;
160         }
161 
getLiteratePopulationPercent()162         public double getLiteratePopulationPercent() {
163             return 100 * literatePopulation / population;
164         }
165 
getWritingPopulation()166         public double getWritingPopulation() {
167             return writingPopulation;
168         }
169 
getWritingPercent()170         public double getWritingPercent() {
171             return 100 * writingPopulation / population;
172         }
173 
getPopulation()174         public double getPopulation() {
175             return population;
176         }
177 
setGdp(double gdp)178         public PopulationData setGdp(double gdp) {
179             if (frozen) {
180                 throw new UnsupportedOperationException(
181                     "Attempt to modify frozen object");
182             }
183             this.gdp = gdp;
184             return this;
185         }
186 
setLiteratePopulation(double literatePopulation)187         public PopulationData setLiteratePopulation(double literatePopulation) {
188             if (frozen) {
189                 throw new UnsupportedOperationException(
190                     "Attempt to modify frozen object");
191             }
192             this.literatePopulation = literatePopulation;
193             return this;
194         }
195 
setPopulation(double population)196         public PopulationData setPopulation(double population) {
197             if (frozen) {
198                 throw new UnsupportedOperationException(
199                     "Attempt to modify frozen object");
200             }
201             this.population = population;
202             return this;
203         }
204 
set(PopulationData other)205         public PopulationData set(PopulationData other) {
206             if (frozen) {
207                 throw new UnsupportedOperationException(
208                     "Attempt to modify frozen object");
209             }
210             if (other == null) {
211                 population = literatePopulation = gdp = Double.NaN;
212             } else {
213                 population = other.population;
214                 literatePopulation = other.literatePopulation;
215                 writingPopulation = other.writingPopulation;
216                 gdp = other.gdp;
217             }
218             return this;
219         }
220 
add(PopulationData other)221         public void add(PopulationData other) {
222             if (frozen) {
223                 throw new UnsupportedOperationException(
224                     "Attempt to modify frozen object");
225             }
226             population += other.population;
227             literatePopulation += other.literatePopulation;
228             writingPopulation += other.writingPopulation;
229             gdp += other.gdp;
230         }
231 
toString()232         public String toString() {
233             return MessageFormat
234                 .format(
235                     "[pop: {0,number,#,##0},\t lit: {1,number,#,##0.00},\t gdp: {2,number,#,##0},\t status: {3}]",
236                     new Object[] { population, literatePopulation, gdp, officialStatus });
237         }
238 
239         private boolean frozen;
240 
isFrozen()241         public boolean isFrozen() {
242             return frozen;
243         }
244 
freeze()245         public PopulationData freeze() {
246             frozen = true;
247             return this;
248         }
249 
cloneAsThawed()250         public PopulationData cloneAsThawed() {
251             throw new UnsupportedOperationException("not yet implemented");
252         }
253 
getOfficialStatus()254         public OfficialStatus getOfficialStatus() {
255             return officialStatus;
256         }
257 
setOfficialStatus(OfficialStatus officialStatus)258         public PopulationData setOfficialStatus(OfficialStatus officialStatus) {
259             if (frozen) {
260                 throw new UnsupportedOperationException(
261                     "Attempt to modify frozen object");
262             }
263             this.officialStatus = officialStatus;
264             return this;
265         }
266 
setWritingPopulation(double writingPopulation)267         public PopulationData setWritingPopulation(double writingPopulation) {
268             if (frozen) {
269                 throw new UnsupportedOperationException(
270                     "Attempt to modify frozen object");
271             }
272             this.writingPopulation = writingPopulation;
273             return this;
274         }
275     }
276 
277     static final Pattern WHITESPACE_PATTERN = PatternCache.get("\\s+");
278 
279     /**
280      * Simple language/script/region information
281      */
282     public static class BasicLanguageData implements Comparable<BasicLanguageData>,
283         com.ibm.icu.util.Freezable<BasicLanguageData> {
284         public enum Type {
285             primary, secondary
286         };
287 
288         private Type type = Type.primary;
289 
290         private Set<String> scripts = Collections.emptySet();
291 
292         private Set<String> territories = Collections.emptySet();
293 
getType()294         public Type getType() {
295             return type;
296         }
297 
setType(Type type)298         public BasicLanguageData setType(Type type) {
299             this.type = type;
300             return this;
301         }
302 
setScripts(String scriptTokens)303         public BasicLanguageData setScripts(String scriptTokens) {
304             return setScripts(scriptTokens == null ? null : Arrays
305                 .asList(WHITESPACE_PATTERN.split(scriptTokens)));
306         }
307 
setTerritories(String territoryTokens)308         public BasicLanguageData setTerritories(String territoryTokens) {
309             return setTerritories(territoryTokens == null ? null : Arrays
310                 .asList(WHITESPACE_PATTERN.split(territoryTokens)));
311         }
312 
setScripts(Collection<String> scriptTokens)313         public BasicLanguageData setScripts(Collection<String> scriptTokens) {
314             if (frozen) {
315                 throw new UnsupportedOperationException();
316             }
317             // TODO add error checking
318             scripts = Collections.emptySet();
319             if (scriptTokens != null) {
320                 for (String script : scriptTokens) {
321                     addScript(script);
322                 }
323             }
324             return this;
325         }
326 
setTerritories(Collection<String> territoryTokens)327         public BasicLanguageData setTerritories(Collection<String> territoryTokens) {
328             if (frozen) {
329                 throw new UnsupportedOperationException();
330             }
331             territories = Collections.emptySet();
332             if (territoryTokens != null) {
333                 for (String territory : territoryTokens) {
334                     addTerritory(territory);
335                 }
336             }
337             return this;
338         }
339 
set(BasicLanguageData other)340         public BasicLanguageData set(BasicLanguageData other) {
341             scripts = other.scripts;
342             territories = other.territories;
343             return this;
344         }
345 
getScripts()346         public Set<String> getScripts() {
347             return scripts;
348         }
349 
getTerritories()350         public Set<String> getTerritories() {
351             return territories;
352         }
353 
toString(String languageSubtag)354         public String toString(String languageSubtag) {
355             if (scripts.size() == 0 && territories.size() == 0)
356                 return "";
357             return "\t\t<language type=\""
358                 + languageSubtag
359                 + "\""
360                 + (scripts.size() == 0 ? "" : " scripts=\""
361                     + CldrUtility.join(scripts, " ") + "\"")
362                 + (territories.size() == 0 ? "" : " territories=\""
363                     + CldrUtility.join(territories, " ") + "\"")
364                 + (type == Type.primary ? "" : " alt=\"" + type + "\"") + "/>";
365         }
366 
toString()367         public String toString() {
368             return "[" + type
369                 + (scripts.isEmpty() ? "" : "; scripts=" + CollectionUtilities.join(scripts, " "))
370                 + (scripts.isEmpty() ? "" : "; territories=" + CollectionUtilities.join(territories, " "))
371                 + "]";
372         }
373 
compareTo(BasicLanguageData o)374         public int compareTo(BasicLanguageData o) {
375             int result;
376             if (0 != (result = type.compareTo(o.type)))
377                 return result;
378             if (0 != (result = IterableComparator.compareIterables(scripts, o.scripts)))
379                 return result;
380             if (0 != (result = IterableComparator.compareIterables(territories, o.territories)))
381                 return result;
382             return 0;
383         }
384 
equals(Object input)385         public boolean equals(Object input) {
386             return compareTo((BasicLanguageData) input) == 0;
387         }
388 
389         @Override
hashCode()390         public int hashCode() {
391             // TODO Auto-generated method stub
392             return ((type.ordinal() * 37 + scripts.hashCode()) * 37) + territories.hashCode();
393         }
394 
addScript(String script)395         public BasicLanguageData addScript(String script) {
396             // simple error checking
397             if (script.length() != 4) {
398                 throw new IllegalArgumentException("Illegal Script: " + script);
399             }
400             if (scripts == Collections.EMPTY_SET) {
401                 scripts = new TreeSet<String>();
402             }
403             scripts.add(script);
404             return this;
405         }
406 
addTerritory(String territory)407         public BasicLanguageData addTerritory(String territory) {
408             // simple error checking
409             if (territory.length() != 2) {
410                 throw new IllegalArgumentException("Illegal Territory: " + territory);
411             }
412             if (territories == Collections.EMPTY_SET) {
413                 territories = new TreeSet<String>();
414             }
415             territories.add(territory);
416             return this;
417         }
418 
419         boolean frozen = false;
420 
isFrozen()421         public boolean isFrozen() {
422             return frozen;
423         }
424 
freeze()425         public BasicLanguageData freeze() {
426             frozen = true;
427             if (scripts != Collections.EMPTY_SET) {
428                 scripts = Collections.unmodifiableSet(scripts);
429             }
430             if (territories != Collections.EMPTY_SET) {
431                 territories = Collections.unmodifiableSet(territories);
432             }
433             return this;
434         }
435 
cloneAsThawed()436         public BasicLanguageData cloneAsThawed() {
437             throw new UnsupportedOperationException();
438         }
439 
addScripts(Set<String> scripts2)440         public void addScripts(Set<String> scripts2) {
441             for (String script : scripts2) {
442                 addScript(script);
443             }
444         }
445     }
446 
447     /**
448      * Information about currency digits and rounding.
449      */
450     public static class CurrencyNumberInfo {
451         public final int digits;
452         public final int rounding;
453         public final double roundingIncrement;
454         public final int cashDigits;
455         public final int cashRounding;
456         public final double cashRoundingIncrement;
457 
getDigits()458         public int getDigits() {
459             return digits;
460         }
461 
getRounding()462         public int getRounding() {
463             return rounding;
464         }
465 
getRoundingIncrement()466         public double getRoundingIncrement() {
467             return roundingIncrement;
468         }
469 
CurrencyNumberInfo(int _digits, int _rounding, int _cashDigits, int _cashRounding)470         public CurrencyNumberInfo(int _digits, int _rounding, int _cashDigits, int _cashRounding) {
471             digits = _digits;
472             rounding = _rounding < 0 ? 0 : _rounding;
473             roundingIncrement = rounding * Math.pow(10.0, -digits);
474             // if the values are not set, use the above values
475             cashDigits = _cashDigits < 0 ? digits : _cashDigits;
476             cashRounding = _cashRounding < 0 ? rounding : _cashRounding;
477             cashRoundingIncrement = this.cashRounding * Math.pow(10.0, -digits);
478         }
479     }
480 
481     public static class NumberingSystemInfo {
482         public enum NumberingSystemType {
483             algorithmic, numeric, unknown
484         };
485 
486         public final String name;
487         public final NumberingSystemType type;
488         public final String digits;
489         public final String rules;
490 
491         public NumberingSystemInfo(XPathParts parts) {
492             name = parts.getAttributeValue(-1, "id");
493             digits = parts.getAttributeValue(-1, "digits");
494             rules = parts.getAttributeValue(-1, "rules");
495             type = NumberingSystemType.valueOf(parts.getAttributeValue(-1, "type"));
496         }
497 
498     }
499 
500     /**
501      * Class for a range of two dates, refactored to share code.
502      *
503      * @author markdavis
504      */
505     public static final class DateRange implements Comparable<DateRange> {
506         public static final long START_OF_TIME = Long.MIN_VALUE;
507         public static final long END_OF_TIME = Long.MAX_VALUE;
508         public final long from;
509         public final long to;
510 
511         public DateRange(String fromString, String toString) {
512             from = parseDate(fromString, START_OF_TIME);
513             to = parseDate(toString, END_OF_TIME);
514         }
515 
516         public long getFrom() {
517             return from;
518         }
519 
520         public long getTo() {
521             return to;
522         }
523 
524         static final DateFormat[] simpleFormats = {
525             new SimpleDateFormat("yyyy-MM-dd HH:mm"),
526             new SimpleDateFormat("yyyy-MM-dd"),
527             new SimpleDateFormat("yyyy-MM"),
528             new SimpleDateFormat("yyyy"),
529         };
530         static {
531             TimeZone gmt = TimeZone.getTimeZone("GMT");
532             for (DateFormat format : simpleFormats) {
533                 format.setTimeZone(gmt);
534             }
535         }
536 
537         long parseDate(String dateString, long defaultDate) {
538             if (dateString == null) {
539                 return defaultDate;
540             }
541             ParseException e2 = null;
542             for (int i = 0; i < simpleFormats.length; ++i) {
543                 try {
544                     synchronized (simpleFormats[i]) {
545                         Date result = simpleFormats[i].parse(dateString);
546                         return result.getTime();
547                     }
548                 } catch (ParseException e) {
549                     if (e2 == null) {
550                         e2 = e;
551                     }
552                 }
553             }
554             throw new IllegalArgumentException(e2);
555         }
556 
557         public String toString() {
558             return "{" + formatDate(from)
559                 + ", "
560                 + formatDate(to) + "}";
561         }
562 
563         public static String formatDate(long date) {
564             if (date == START_OF_TIME) {
565                 return "-∞";
566             }
567             if (date == END_OF_TIME) {
568                 return "∞";
569             }
570             synchronized (simpleFormats[0]) {
571                 return simpleFormats[0].format(date);
572             }
573         }
574 
575         @Override
576         public int compareTo(DateRange arg0) {
577             return to > arg0.to ? 1 : to < arg0.to ? -1 : from > arg0.from ? 1 : from < arg0.from ? -1 : 0;
578         }
579     }
580 
581     /**
582      * Information about when currencies are in use in territories
583      */
584     public static class CurrencyDateInfo implements Comparable<CurrencyDateInfo> {
585 
586         public static final Date END_OF_TIME = new Date(DateRange.END_OF_TIME);
587         public static final Date START_OF_TIME = new Date(DateRange.START_OF_TIME);
588 
589         private String currency;
590         private DateRange dateRange;
591         private boolean isLegalTender;
592         private String errors = "";
593 
594         public CurrencyDateInfo(String currency, String startDate, String endDate, String tender) {
595             this.currency = currency;
596             this.dateRange = new DateRange(startDate, endDate);
597             this.isLegalTender = (tender == null || !tender.equals("false"));
598         }
599 
600         public String getCurrency() {
601             return currency;
602         }
603 
604         public Date getStart() {
605             return new Date(dateRange.getFrom());
606         }
607 
608         public Date getEnd() {
609             return new Date(dateRange.getTo());
610         }
611 
612         public String getErrors() {
613             return errors;
614         }
615 
616         public boolean isLegalTender() {
617             return isLegalTender;
618         }
619 
620         public int compareTo(CurrencyDateInfo o) {
621             int result = dateRange.compareTo(o.dateRange);
622             if (result != 0) return result;
623             return currency.compareTo(o.currency);
624         }
625 
626         public String toString() {
627             return "{" + dateRange + ", " + currency + "}";
628         }
629 
630         public static String formatDate(Date date) {
631             return DateRange.formatDate(date.getTime());
632         }
633 
634     }
635 
636     public static final class MetaZoneRange implements Comparable<MetaZoneRange> {
637         public final DateRange dateRange;
638         public final String metazone;
639 
640         /**
641          * @param metazone
642          * @param from
643          * @param to
644          */
645         public MetaZoneRange(String metazone, String fromString, String toString) {
646             super();
647             this.metazone = metazone;
648             dateRange = new DateRange(fromString, toString);
649         }
650 
651         @Override
652         public int compareTo(MetaZoneRange arg0) {
653             int result;
654             if (0 != (result = dateRange.compareTo(arg0.dateRange))) {
655                 return result;
656             }
657             return metazone.compareTo(arg0.metazone);
658         }
659 
660         public String toString() {
661             return "{" + dateRange + ", " + metazone + "}";
662         }
663     }
664 
665     /**
666      * Information about telephone code(s) for a given territory
667      */
668     public static class TelephoneCodeInfo implements Comparable<TelephoneCodeInfo> {
669         public static final Date END_OF_TIME = new Date(Long.MAX_VALUE);
670         public static final Date START_OF_TIME = new Date(Long.MIN_VALUE);
671         private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
672 
673         private String code;
674         private Date start;
675         private Date end;
676         private String alt;
677         private String errors = "";
678 
679         // code must not be null, the others can be
680         public TelephoneCodeInfo(String code, String startDate, String endDate, String alt) {
681             if (code == null)
682                 throw new NullPointerException();
683             this.code = code; // code will not be null
684             this.start = parseDate(startDate, START_OF_TIME); // start will not be null
685             this.end = parseDate(endDate, END_OF_TIME); // end willl not be null
686             this.alt = (alt == null) ? "" : alt; // alt will not be null
687         }
688 
689         static DateFormat[] simpleFormats = {
690             new SimpleDateFormat("yyyy-MM-dd"),
691             new SimpleDateFormat("yyyy-MM"),
692             new SimpleDateFormat("yyyy"), };
693 
694         Date parseDate(String dateString, Date defaultDate) {
695             if (dateString == null) {
696                 return defaultDate;
697             }
698             ParseException e2 = null;
699             for (int i = 0; i < simpleFormats.length; ++i) {
700                 try {
701                     Date result = simpleFormats[i].parse(dateString);
702                     return result;
703                 } catch (ParseException e) {
704                     if (i == 0) {
705                         errors += dateString + " ";
706                     }
707                     if (e2 == null) {
708                         e2 = e;
709                     }
710                 }
711             }
712             throw (IllegalArgumentException) new IllegalArgumentException().initCause(e2);
713         }
714 
715         public String getCode() {
716             return code;
717         }
718 
719         public Date getStart() {
720             return start;
721         }
722 
723         public Date getEnd() {
724             return end;
725         }
726 
727         public String getAlt() {
728             return alt; // may return null
729         }
730 
731         public String getErrors() {
732             return errors;
733         }
734 
735         public boolean equals(Object o) {
736             if (!(o instanceof TelephoneCodeInfo))
737                 return false;
738             TelephoneCodeInfo tc = (TelephoneCodeInfo) o;
739             return tc.code.equals(code) && tc.start.equals(start) && tc.end.equals(end) && tc.alt.equals(alt);
740         }
741 
742         public int hashCode() {
743             return 31 * code.hashCode() + start.hashCode() + end.hashCode() + alt.hashCode();
744         }
745 
746         public int compareTo(TelephoneCodeInfo o) {
747             int result = code.compareTo(o.code);
748             if (result != 0) return result;
749             result = start.compareTo(o.start);
750             if (result != 0) return result;
751             result = end.compareTo(o.end);
752             if (result != 0) return result;
753             return alt.compareTo(o.alt);
754         }
755 
756         public String toString() {
757             return "{" + code + ", " + formatDate(start) + ", " + formatDate(end) + ", " + alt + "}";
758         }
759 
760         public static String formatDate(Date date) {
761             if (date.equals(START_OF_TIME)) return "-∞";
762             if (date.equals(END_OF_TIME)) return "∞";
763             return dateFormat.format(date);
764         }
765     }
766 
767     public static class CoverageLevelInfo implements Comparable<CoverageLevelInfo> {
768         public final String match;
769         public final Level value;
770         public final Pattern inLanguage;
771         public final String inScript;
772         public final Set<String> inScriptSet;
773         public final String inTerritory;
774         public final Set<String> inTerritorySet;
775         private Set<String> inTerritorySetInternal;
776 
777         public CoverageLevelInfo(String match, int value, String language, String script, String territory) {
778             this.inLanguage = language != null ? PatternCache.get(language) : null;
779             this.inScript = script;
780             this.inTerritory = territory;
781             this.inScriptSet = toSet(script);
782             this.inTerritorySet = toSet(territory); // MUST BE LAST, sets inTerritorySetInternal
783             this.match = match;
784             this.value = Level.fromLevel(value);
785         }
786 
787         public static final Pattern NON_ASCII_LETTER = PatternCache.get("[^A-Za-z]+");
788 
789         private Set<String> toSet(String source) {
790             if (source == null) {
791                 return null;
792             }
793             Set<String> result = new HashSet<String>(Arrays.asList(NON_ASCII_LETTER.split(source)));
794             result.remove("");
795             inTerritorySetInternal = result;
796             return Collections.unmodifiableSet(result);
797         }
798 
799         public int compareTo(CoverageLevelInfo o) {
800             if (value == o.value) {
801                 return match.compareTo(o.match);
802             } else {
803                 return value.compareTo(o.value);
804             }
805         }
806 
807         static void fixEU(Collection<CoverageLevelInfo> targets, SupplementalDataInfo info) {
808             Set<String> euCountries = info.getContained("EU");
809             for (CoverageLevelInfo item : targets) {
810                 if (item.inTerritorySet != null
811                     && item.inTerritorySet.contains("EU")) {
812                     item.inTerritorySetInternal.addAll(euCountries);
813                 }
814             }
815         }
816     }
817 
818     public static final String STAR = "*";
819     public static final Set<String> STAR_SET = Builder.with(new HashSet<String>()).add("*").freeze();
820 
821     private VersionInfo cldrVersion;
822 
823     private Map<String, PopulationData> territoryToPopulationData = new TreeMap<String, PopulationData>();
824 
825     private Map<String, Map<String, PopulationData>> territoryToLanguageToPopulationData = new TreeMap<String, Map<String, PopulationData>>();
826 
827     private Map<String, PopulationData> languageToPopulation = new TreeMap<String, PopulationData>();
828 
829     private Map<String, PopulationData> baseLanguageToPopulation = new TreeMap<String, PopulationData>();
830 
831     private Relation<String, String> languageToScriptVariants = Relation.of(new TreeMap<String, Set<String>>(),
832         TreeSet.class);
833 
834     private Relation<String, String> languageToTerritories = Relation.of(new TreeMap<String, Set<String>>(),
835         LinkedHashSet.class);
836 
837     transient private Relation<String, Pair<Boolean, Pair<Double, String>>> languageToTerritories2 = Relation
838         .of(new TreeMap<String, Set<Pair<Boolean, Pair<Double, String>>>>(), TreeSet.class);
839 
840     private Map<String, Map<BasicLanguageData.Type, BasicLanguageData>> languageToBasicLanguageData = new TreeMap<String, Map<BasicLanguageData.Type, BasicLanguageData>>();
841 
842     // private Map<String, BasicLanguageData> languageToBasicLanguageData2 = new
843     // TreeMap();
844 
845     // Relation(new TreeMap(), TreeSet.class, null);
846 
847     private Set<String> allLanguages = new TreeSet<String>();
848     final private List<String> approvalRequirements = new LinkedList<String>(); // xpath array
849 
850     private Relation<String, String> containment = Relation.of(new LinkedHashMap<String, Set<String>>(),
851         LinkedHashSet.class);
852     private Relation<String, String> containmentCore = Relation.of(new LinkedHashMap<String, Set<String>>(),
853         LinkedHashSet.class);
854     //    private Relation<String, String> containmentNonDeprecated = Relation.of(new LinkedHashMap<String, Set<String>>(),
855     //        LinkedHashSet.class);
856     private Relation<String, String> containmentGrouping = Relation.of(new LinkedHashMap<String, Set<String>>(),
857         LinkedHashSet.class);
858     private Relation<String, String> containmentDeprecated = Relation.of(new LinkedHashMap<String, Set<String>>(),
859         LinkedHashSet.class);
860     private Relation<String, String> containerToSubdivision = Relation.of(new LinkedHashMap<String, Set<String>>(),
861         LinkedHashSet.class);
862 
863     private Map<String, CurrencyNumberInfo> currencyToCurrencyNumberInfo = new TreeMap<String, CurrencyNumberInfo>();
864 
865     private Relation<String, CurrencyDateInfo> territoryToCurrencyDateInfo = Relation.of(
866         new TreeMap<String, Set<CurrencyDateInfo>>(), LinkedHashSet.class);
867 
868     // private Relation<String, TelephoneCodeInfo> territoryToTelephoneCodeInfo = new Relation(new TreeMap(),
869     // LinkedHashSet.class);
870     private Map<String, Set<TelephoneCodeInfo>> territoryToTelephoneCodeInfo = new TreeMap<String, Set<TelephoneCodeInfo>>();
871 
872     private Set<String> multizone = new TreeSet<String>();
873 
874     private Map<String, String> zone_territory = new TreeMap<String, String>();
875 
876     private Relation<String, String> zone_aliases = Relation
877         .of(new TreeMap<String, Set<String>>(), LinkedHashSet.class);
878 
879     private Map<String, Map<String, Map<String, String>>> typeToZoneToRegionToZone = new TreeMap<String, Map<String, Map<String, String>>>();
880     private Relation<String, MetaZoneRange> zoneToMetaZoneRanges = Relation.of(
881         new TreeMap<String, Set<MetaZoneRange>>(), TreeSet.class);
882 //    private Map<String, Map<String, Relation<String, String>>> deprecated = new HashMap<String, Map<String, Relation<String, String>>>();
883 
884     private Map<String, String> metazoneContinentMap = new HashMap<String, String>();
885     private Set<String> allMetazones = new TreeSet<String>();
886 
887     private Map<String, String> alias_zone = new TreeMap<String, String>();
888 
889     public Relation<String, Integer> numericTerritoryMapping = Relation.of(new HashMap<String, Set<Integer>>(),
890         HashSet.class);
891 
892     public Relation<String, String> alpha3TerritoryMapping = Relation.of(new HashMap<String, Set<String>>(),
893         HashSet.class);
894 
895     public Relation<String, Integer> numericCurrencyCodeMapping = Relation.of(new HashMap<String, Set<Integer>>(),
896         HashSet.class);
897 
898     static Map<String, SupplementalDataInfo> directory_instance = new HashMap<String, SupplementalDataInfo>();
899 
900     public Map<String, Map<String, Row.R2<List<String>, String>>> typeToTagToReplacement = new TreeMap<String, Map<String, Row.R2<List<String>, String>>>();
901 
902     Map<String, List<Row.R4<String, String, Integer, Boolean>>> languageMatch = new HashMap<String, List<Row.R4<String, String, Integer, Boolean>>>();
903 
904     public Relation<String, String> bcp47Key2Subtypes = Relation.of(new TreeMap<String, Set<String>>(), TreeSet.class);
905     public Relation<String, String> bcp47Extension2Keys = Relation
906         .of(new TreeMap<String, Set<String>>(), TreeSet.class);
907     public Relation<Row.R2<String, String>, String> bcp47Aliases = Relation.of(
908         new TreeMap<Row.R2<String, String>, Set<String>>(), LinkedHashSet.class);
909     public Map<Row.R2<String, String>, String> bcp47Descriptions = new TreeMap<Row.R2<String, String>, String>();
910     public Map<Row.R2<String, String>, String> bcp47Since = new TreeMap<Row.R2<String, String>, String>();
911     public Map<Row.R2<String, String>, String> bcp47Preferred = new TreeMap<Row.R2<String, String>, String>();
912     public Map<Row.R2<String, String>, String> bcp47Deprecated = new TreeMap<Row.R2<String, String>, String>();
913 
914     public Map<String, Row.R2<String, String>> validityInfo = new LinkedHashMap<String, Row.R2<String, String>>();
915     public Map<AttributeValidityInfo, String> attributeValidityInfo = new LinkedHashMap<>();
916 
917     public Multimap<String, String> languageGroups = TreeMultimap.create();
918 
919     public enum MeasurementType {
920         measurementSystem, paperSize
921     }
922 
923     Map<MeasurementType, Map<String, String>> measurementData = new HashMap<MeasurementType, Map<String, String>>();
924     Map<String, PreferredAndAllowedHour> timeData = new HashMap<String, PreferredAndAllowedHour>();
925 
926     public Relation<String, String> getAlpha3TerritoryMapping() {
927         return alpha3TerritoryMapping;
928     }
929 
930     public Relation<String, Integer> getNumericTerritoryMapping() {
931         return numericTerritoryMapping;
932     }
933 
934     public Relation<String, Integer> getNumericCurrencyCodeMapping() {
935         return numericCurrencyCodeMapping;
936     }
937 
938     /**
939      * Returns type -> tag -> <replacementList, reason>, like "language" -> "sh" -> <{"sr_Latn"}, reason>
940      *
941      * @return
942      */
943     public Map<String, Map<String, R2<List<String>, String>>> getLocaleAliasInfo() {
944         return typeToTagToReplacement;
945     }
946 
947     public R2<List<String>, String> getDeprecatedInfo(String type, String code) {
948         return typeToTagToReplacement.get(type).get(code);
949     }
950 
951     public static SupplementalDataInfo getInstance(File supplementalDirectory) {
952         try {
953             return getInstance(supplementalDirectory.getCanonicalPath());
954         } catch (IOException e) {
955             throw new ICUUncheckedIOException(e);
956 //            throw (IllegalArgumentException) new IllegalArgumentException()
957 //            .initCause(e);
958         }
959     }
960 
961     static private SupplementalDataInfo defaultInstance = null;
962     /**
963      * Which directory did we come from?
964      */
965     final private File directory;
966 
967     /**
968      * Get an instance chosen using setAsDefaultInstance(), otherwise return an instance using the default directory
969      * CldrUtility.SUPPLEMENTAL_DIRECTORY
970      *
971      * @return
972      */
973     public static SupplementalDataInfo getInstance() {
974         if (defaultInstance != null) return defaultInstance;
975         return CLDRConfig.getInstance().getSupplementalDataInfo();
976         // return getInstance(CldrUtility.SUPPLEMENTAL_DIRECTORY);
977     }
978 
979     /**
980      * Mark this as the default instance to be returned by getInstance()
981      */
982     public void setAsDefaultInstance() {
983         defaultInstance = this;
984     }
985 
986     public static SupplementalDataInfo getInstance(String supplementalDirectory) {
987         synchronized (SupplementalDataInfo.class) {
988             // Sanity checks - not null, not empty
989             if (supplementalDirectory == null) {
990                 throw new IllegalArgumentException("Error: null supplemental directory.");
991             }
992             if (supplementalDirectory.isEmpty()) {
993                 throw new IllegalArgumentException("Error: The string passed as a parameter resolves to the empty string.");
994             }
995             // canonicalize path
996             String canonicalpath = null;
997             try {
998                 canonicalpath = new File(supplementalDirectory).getCanonicalPath();
999             } catch (IOException e) {
1000                 throw new ICUUncheckedIOException(e);
1001             }
1002             SupplementalDataInfo instance = directory_instance.get(canonicalpath);
1003             if (instance != null) {
1004                 return instance;
1005             }
1006             //            if (!canonicalpath.equals(supplementalDirectory)) {
1007             //                instance = directory_instance.get(canonicalpath);
1008             //                if (instance != null) {
1009             //                    directory_instance.put(supplementalDirectory, instance);
1010             //                    directory_instance.put(canonicalpath,instance);
1011             //                    return instance;
1012             //                }
1013             //            }
1014             // reaching here means we have not cached the entry
1015             File directory = new File(canonicalpath);
1016             instance = new SupplementalDataInfo(directory);
1017             MyHandler myHandler = instance.new MyHandler();
1018             XMLFileReader xfr = new XMLFileReader().setHandler(myHandler);
1019             File files1[] = directory.listFiles();
1020             if (files1 == null || files1.length == 0) {
1021                 throw new ICUUncheckedIOException("Error: Supplemental files missing from " + directory.getAbsolutePath());
1022             }
1023             // get bcp47 files also
1024             File bcp47dir = instance.getBcp47Directory();
1025             if (!bcp47dir.isDirectory()) {
1026                 throw new ICUUncheckedIOException("Error: BCP47 dir is not a directory: " + bcp47dir.getAbsolutePath());
1027             }
1028             File files2[] = bcp47dir.listFiles();
1029             if (files2 == null || files2.length == 0) {
1030                 throw new ICUUncheckedIOException("Error: BCP47 files missing from " + bcp47dir.getAbsolutePath());
1031             }
1032 
1033             CBuilder<File, ArrayList<File>> builder = Builder.with(new ArrayList<File>());
1034             builder.addAll(files1);
1035             builder.addAll(files2);
1036             for (File file : builder.get()) {
1037                 if (DEBUG) {
1038                     try {
1039                         System.out.println(file.getCanonicalPath());
1040                     } catch (IOException e) {
1041                     }
1042                 }
1043                 String name = file.toString();
1044                 String shortName = file.getName();
1045                 if (!shortName.endsWith(".xml") || // skip non-XML
1046                     shortName.startsWith("#") || // skip other junk files
1047                     shortName.startsWith(".")) continue; // skip dot files (backups, etc)
1048                 xfr.read(name, -1, true);
1049                 myHandler.cleanup();
1050             }
1051 
1052             // xfr = new XMLFileReader().setHandler(instance.new MyHandler());
1053             // .xfr.read(canonicalpath + "/supplementalMetadata.xml", -1, true);
1054 
1055             instance.makeStuffSafe();
1056             // cache
1057             //            directory_instance.put(supplementalDirectory, instance);
1058             directory_instance.put(canonicalpath, instance);
1059             //            if (!canonicalpath.equals(supplementalDirectory)) {
1060             //                directory_instance.put(canonicalpath, instance);
1061             //            }
1062             return instance;
1063         }
1064     }
1065 
1066     private File getBcp47Directory() {
1067         return new File(getDirectory().getParent(), "bcp47");
1068     }
1069 
1070     private SupplementalDataInfo(File directory) {
1071         this.directory = directory;
1072     }; // hide
1073 
1074     private void makeStuffSafe() {
1075         // now make stuff safe
1076         allLanguages.addAll(languageToPopulation.keySet());
1077         allLanguages.addAll(baseLanguageToPopulation.keySet());
1078         allLanguages = Collections.unmodifiableSet(allLanguages);
1079         skippedElements = Collections.unmodifiableSet(skippedElements);
1080         multizone = Collections.unmodifiableSet(multizone);
1081         zone_territory = Collections.unmodifiableMap(zone_territory);
1082         alias_zone = Collections.unmodifiableMap(alias_zone);
1083         references = Collections.unmodifiableMap(references);
1084         likelySubtags = Collections.unmodifiableMap(likelySubtags);
1085         currencyToCurrencyNumberInfo = Collections.unmodifiableMap(currencyToCurrencyNumberInfo);
1086         territoryToCurrencyDateInfo.freeze();
1087         // territoryToTelephoneCodeInfo.freeze();
1088         territoryToTelephoneCodeInfo = Collections.unmodifiableMap(territoryToTelephoneCodeInfo);
1089 
1090         typeToZoneToRegionToZone = CldrUtility.protectCollection(typeToZoneToRegionToZone);
1091         typeToTagToReplacement = CldrUtility.protectCollection(typeToTagToReplacement);
1092 
1093         zoneToMetaZoneRanges.freeze();
1094 
1095         containment.freeze();
1096         containmentCore.freeze();
1097         //        containmentNonDeprecated.freeze();
1098         containmentGrouping.freeze();
1099         containmentDeprecated.freeze();
1100 
1101         containerToSubdivision.freeze();
1102 
1103         CldrUtility.protectCollection(languageToBasicLanguageData);
1104         for (String language : languageToTerritories2.keySet()) {
1105             for (Pair<Boolean, Pair<Double, String>> pair : languageToTerritories2.getAll(language)) {
1106                 languageToTerritories.put(language, pair.getSecond().getSecond());
1107             }
1108         }
1109         languageToTerritories2 = null; // free up the memory.
1110         languageToTerritories.freeze();
1111         zone_aliases.freeze();
1112         languageToScriptVariants.freeze();
1113 
1114         numericTerritoryMapping.freeze();
1115         alpha3TerritoryMapping.freeze();
1116         numericCurrencyCodeMapping.freeze();
1117 
1118         // freeze contents
1119         for (String language : languageToPopulation.keySet()) {
1120             languageToPopulation.get(language).freeze();
1121         }
1122         for (String language : baseLanguageToPopulation.keySet()) {
1123             baseLanguageToPopulation.get(language).freeze();
1124         }
1125         for (String territory : territoryToPopulationData.keySet()) {
1126             territoryToPopulationData.get(territory).freeze();
1127         }
1128         for (String territory : territoryToLanguageToPopulationData.keySet()) {
1129             Map<String, PopulationData> languageToPopulationDataTemp = territoryToLanguageToPopulationData
1130                 .get(territory);
1131             for (String language : languageToPopulationDataTemp.keySet()) {
1132                 languageToPopulationDataTemp.get(language).freeze();
1133             }
1134         }
1135         localeToPluralInfo2.put(PluralType.cardinal, Collections.unmodifiableMap(localeToPluralInfo2.get(PluralType.cardinal)));
1136         localeToPluralInfo2.put(PluralType.ordinal, Collections.unmodifiableMap(localeToPluralInfo2.get(PluralType.ordinal)));
1137 
1138         localeToPluralRanges = Collections.unmodifiableMap(localeToPluralRanges);
1139         for (PluralRanges pluralRanges : localeToPluralRanges.values()) {
1140             pluralRanges.freeze();
1141         }
1142 
1143         if (lastDayPeriodLocales != null) {
1144             addDayPeriodInfo();
1145         }
1146         typeToLocaleToDayPeriodInfo = CldrUtility.protectCollection(typeToLocaleToDayPeriodInfo);
1147         languageMatch = CldrUtility.protectCollection(languageMatch);
1148         bcp47Key2Subtypes.freeze();
1149         bcp47Extension2Keys.freeze();
1150         bcp47Aliases.freeze();
1151         if (bcp47Key2Subtypes.isEmpty()) {
1152             throw new InternalError("No BCP47 key 2 subtype data was loaded from bcp47 dir " + getBcp47Directory().getAbsolutePath());
1153         }
1154         CldrUtility.protectCollection(bcp47Descriptions);
1155 
1156         CoverageLevelInfo.fixEU(coverageLevels, this);
1157         coverageLevels = Collections.unmodifiableSortedSet(coverageLevels);
1158 
1159         measurementData = CldrUtility.protectCollection(measurementData);
1160         timeData = CldrUtility.protectCollection(timeData);
1161 
1162         validityInfo = CldrUtility.protectCollection(validityInfo);
1163         attributeValidityInfo = CldrUtility.protectCollection(attributeValidityInfo);
1164         parentLocales = Collections.unmodifiableMap(parentLocales);
1165         languageGroups = ImmutableSetMultimap.copyOf(languageGroups);
1166 
1167         ImmutableSet.Builder<String> newScripts = ImmutableSet.<String> builder();
1168         Map<Validity.Status, Set<String>> scripts = Validity.getInstance().getStatusToCodes(LstrType.script);
1169         for (Entry<Status, Set<String>> e : scripts.entrySet()) {
1170             switch (e.getKey()) {
1171             case regular:
1172             case special:
1173             case unknown:
1174                 newScripts.addAll(e.getValue());
1175                 break;
1176             default:
1177                 break; // do nothing
1178             }
1179         }
1180         CLDRScriptCodes = newScripts.build();
1181     }
1182 
1183     // private Map<String, Map<String, String>> makeUnmodifiable(Map<String, Map<String, String>>
1184     // metazoneToRegionToZone) {
1185     // Map<String, Map<String, String>> temp = metazoneToRegionToZone;
1186     // for (String mzone : metazoneToRegionToZone.keySet()) {
1187     // temp.put(mzone, Collections.unmodifiableMap(metazoneToRegionToZone.get(mzone)));
1188     // }
1189     // return Collections.unmodifiableMap(temp);
1190     // }
1191 
1192     /**
1193      * Core function used to process each of the paths, and add the data to the appropriate data member.
1194      */
1195     class MyHandler extends XMLFileReader.SimpleHandler {
1196         private static final double MAX_POPULATION = 3000000000.0;
1197 
1198         XPathParts parts = new XPathParts();
1199 
1200         LanguageTagParser languageTagParser = new LanguageTagParser();
1201 
1202         /**
1203          * Finish processing anything left hanging in the file.
1204          */
1205         public void cleanup() {
1206             if (lastPluralMap.size() > 0) {
1207                 addPluralInfo(lastPluralWasOrdinal);
1208             }
1209             lastPluralLocales = "";
1210         }
1211 
1212         public void handlePathValue(String path, String value) {
1213             try {
1214                 parts.set(path);
1215                 String level0 = parts.getElement(0);
1216                 String level1 = parts.size() < 2 ? null : parts.getElement(1);
1217                 String level2 = parts.size() < 3 ? null : parts.getElement(2);
1218                 String level3 = parts.size() < 4 ? null : parts.getElement(3);
1219                 // String level4 = parts.size() < 5 ? null : parts.getElement(4);
1220                 if (level1.equals("generation")) {
1221                     // skip
1222                     return;
1223                 }
1224                 if (level1.equals("version")) {
1225                     if (cldrVersion == null) {
1226                         String version = parts.getAttributeValue(1, "cldrVersion");
1227                         if (version == null) {
1228                             // old format
1229                             version = parts.getAttributeValue(0, "version");
1230                         }
1231                         cldrVersion = VersionInfo.getInstance(version);
1232                     }
1233                     return;
1234                 }
1235 
1236                 // copy the rest from ShowLanguages later
1237                 if (level0.equals("ldmlBCP47")) {
1238                     if (handleBcp47(level1, level2)) {
1239                         return;
1240                     }
1241                 } else if (level1.equals("territoryInfo")) {
1242                     if (handleTerritoryInfo()) {
1243                         return;
1244                     }
1245                 } else if (level1.equals("calendarPreferenceData")) {
1246                     handleCalendarPreferenceData();
1247                     return;
1248                 } else if (level1.equals("languageData")) {
1249                     handleLanguageData();
1250                     return;
1251                 } else if (level1.equals("territoryContainment")) {
1252                     handleTerritoryContainment();
1253                     return;
1254                 } else if (level1.equals("subdivisionContainment")) {
1255                     handleSubdivisionContainment();
1256                     return;
1257                 } else if (level1.equals("currencyData")) {
1258                     if (handleCurrencyData(level2)) {
1259                         return;
1260                     }
1261                     // } else if (level1.equals("timezoneData")) {
1262                     // if (handleTimezoneData(level2)) {
1263                     // return;
1264                     // }
1265                 } else if ("metazoneInfo".equals(level2)) {
1266                     if (handleMetazoneInfo(level2, level3)) {
1267                         return;
1268                     }
1269                 } else if ("mapTimezones".equals(level2)) {
1270                     if (handleMetazoneData(level2, level3)) {
1271                         return;
1272                     }
1273                 } else if (level1.equals("plurals")) {
1274                     if (addPluralPath(parts, value)) {
1275                         return;
1276                     }
1277                 } else if (level1.equals("dayPeriodRuleSet")) {
1278                     addDayPeriodPath(parts, value);
1279                     return;
1280                 } else if (level1.equals("telephoneCodeData")) {
1281                     handleTelephoneCodeData(parts);
1282                     return;
1283                 } else if (level1.equals("references")) {
1284                     String type = parts.getAttributeValue(-1, "type");
1285                     String uri = parts.getAttributeValue(-1, "uri");
1286                     references.put(type, new Pair<String, String>(uri, value).freeze());
1287                     return;
1288                 } else if (level1.equals("likelySubtags")) {
1289                     handleLikelySubtags();
1290                     return;
1291                 } else if (level1.equals("numberingSystems")) {
1292                     handleNumberingSystems();
1293                     return;
1294                 } else if (level1.equals("coverageLevels")) {
1295                     handleCoverageLevels();
1296                     return;
1297                 } else if (level1.equals("parentLocales")) {
1298                     handleParentLocales();
1299                     return;
1300                 } else if (level1.equals("metadata")) {
1301                     if (handleMetadata(level2, value)) {
1302                         return;
1303                     }
1304                 } else if (level1.equals("codeMappings")) {
1305                     if (handleCodeMappings(level2)) {
1306                         return;
1307                     }
1308                 } else if (level1.equals("languageMatching")) {
1309                     if (handleLanguageMatcher(level2)) {
1310                         return;
1311                     }
1312                 } else if (level1.equals("measurementData")) {
1313                     if (handleMeasurementData(level2)) {
1314                         return;
1315                     }
1316                 } else if (level1.equals("timeData")) {
1317                     if (handleTimeData(level2)) {
1318                         return;
1319                     }
1320                 } else if (level1.equals("languageGroups")) {
1321                     if (handleLanguageGroups(level2, value)) {
1322                         return;
1323                     }
1324                 }
1325 
1326                 // capture elements we didn't look at, since we should cover everything.
1327                 // this helps for updates
1328 
1329                 final String skipKey = level1 + (level2 == null ? "" : "/" + level2);
1330                 if (!skippedElements.contains(skipKey)) {
1331                     skippedElements.add(skipKey);
1332                 }
1333                 // System.out.println("Skipped Element: " + path);
1334             } catch (Exception e) {
1335                 throw (IllegalArgumentException) new IllegalArgumentException("Exception while processing path: "
1336                     + path + ",\tvalue: " + value).initCause(e);
1337             }
1338         }
1339 
1340         private boolean handleLanguageGroups(String level2, String value) {
1341             String parent = parts.getAttributeValue(-1, "parent");
1342             List<String> children = WHITESPACE_SPLTTER.splitToList(value);
1343             languageGroups.putAll(parent, children);
1344             return true;
1345         }
1346 
1347         private boolean handleMeasurementData(String level2) {
1348             /**
1349              * <measurementSystem type="US" territories="LR MM US"/>
1350              * <paperSize type="A4" territories="001"/>
1351              */
1352             MeasurementType measurementType = MeasurementType.valueOf(level2);
1353             String type = parts.getAttributeValue(-1, "type");
1354             String territories = parts.getAttributeValue(-1, "territories");
1355             Map<String, String> data = measurementData.get(measurementType);
1356             if (data == null) {
1357                 measurementData.put(measurementType, data = new HashMap<String, String>());
1358             }
1359             for (String territory : territories.trim().split("\\s+")) {
1360                 data.put(territory, type);
1361             }
1362             return true;
1363         }
1364 
1365         private boolean handleTimeData(String level2) {
1366             /**
1367              * <hours preferred="H" allowed="H" regions="IL RU"/>
1368              */
1369             String preferred = parts.getAttributeValue(-1, "preferred");
1370             // String[] allowed = parts.getAttributeValue(-1, "allowed").trim().split("\\s+");
1371             PreferredAndAllowedHour preferredAndAllowedHour = new PreferredAndAllowedHour(preferred,
1372                 parts.getAttributeValue(-1, "allowed"));
1373             for (String region : parts.getAttributeValue(-1, "regions").trim().split("\\s+")) {
1374                 PreferredAndAllowedHour oldValue = timeData.put(region, preferredAndAllowedHour);
1375                 if (oldValue != null) {
1376                     throw new IllegalArgumentException("timeData/hours must not have duplicate regions: " + region);
1377                 }
1378             }
1379             return true;
1380         }
1381 
1382         private boolean handleBcp47(String level1, String level2) {
1383             if (level1.equals("version") || level1.equals("generation") || level1.equals("cldrVersion")) {
1384                 return true; // skip
1385             }
1386             if (!level1.equals("keyword")) {
1387                 throw new IllegalArgumentException("Unexpected level1 element: " + level1);
1388             }
1389 
1390             String finalElement = parts.getElement(-1);
1391             String key = parts.getAttributeValue(2, "name");
1392             String extension = parts.getAttributeValue(2, "extension");
1393             if (extension == null) {
1394                 extension = "u";
1395             }
1396             bcp47Extension2Keys.put(extension, key);
1397 
1398             String keyAlias = parts.getAttributeValue(2, "alias");
1399             String keyDescription = parts.getAttributeValue(2, "description");
1400             String deprecated = parts.getAttributeValue(2, "deprecated");
1401             // TODO add preferred, valueType, since
1402 
1403             final R2<String, String> key_empty = (R2<String, String>) Row.of(key, "").freeze();
1404 
1405             if (keyAlias != null) {
1406                 bcp47Aliases.putAll(key_empty, Arrays.asList(keyAlias.trim().split("\\s+")));
1407             }
1408 
1409             if (keyDescription != null) {
1410                 bcp47Descriptions.put(key_empty, keyDescription);
1411             }
1412             if (deprecated != null && deprecated.equals("true")) {
1413                 bcp47Deprecated.put(key_empty, deprecated);
1414             }
1415 
1416             switch (finalElement) {
1417             case "key":
1418                 break; // all actions taken above
1419 
1420             case "type":
1421                 String subtype = parts.getAttributeValue(3, "name");
1422                 String subtypeAlias = parts.getAttributeValue(3, "alias");
1423                 String desc = parts.getAttributeValue(3, "description");
1424                 String subtypeDescription = desc == null ? null : desc.replaceAll("\\s+", " ");
1425                 String subtypeSince = parts.getAttributeValue(3, "since");
1426                 String subtypePreferred = parts.getAttributeValue(3, "preferred");
1427                 String subtypeDeprecated = parts.getAttributeValue(3, "deprecated");
1428 
1429                 Set<String> set = bcp47Key2Subtypes.get(key);
1430                 if (set != null && set.contains(key)) {
1431                     throw new IllegalArgumentException("Collision with bcp47 key-value: " + key + "," + subtype);
1432                 }
1433                 bcp47Key2Subtypes.put(key, subtype);
1434 
1435                 final R2<String, String> key_subtype = (R2<String, String>) Row.of(key, subtype).freeze();
1436 
1437                 if (subtypeAlias != null) {
1438                     bcp47Aliases.putAll(key_subtype, Arrays.asList(subtypeAlias.trim().split("\\s+")));
1439                 }
1440                 if (subtypeDescription != null) {
1441                     bcp47Descriptions.put(key_subtype, subtypeDescription.replaceAll("\\s+", " "));
1442                 }
1443                 if (subtypeDescription != null) {
1444                     bcp47Since.put(key_subtype, subtypeSince);
1445                 }
1446                 if (subtypePreferred != null) {
1447                     bcp47Preferred.put(key_subtype, subtypePreferred);
1448                 }
1449                 if (subtypeDeprecated != null) {
1450                     bcp47Deprecated.put(key_subtype, subtypeDeprecated);
1451                 }
1452                 break;
1453             default:
1454                 throw new IllegalArgumentException("Unexpected element: " + finalElement);
1455             }
1456 
1457             return true;
1458         }
1459 
1460         private boolean handleLanguageMatcher(String level2) {
1461             String type = parts.getAttributeValue(2, "type");
1462             String alt = parts.getAttributeValue(2, "alt");
1463             if (alt != null) {
1464                 type += "_" + alt;
1465             }
1466             switch (parts.getElement(3)) {
1467             case "paradigmLocales":
1468                 List<String> locales = WHITESPACE_SPLTTER.splitToList(parts.getAttributeValue(3, "locales"));
1469                 // TODO
1470 //                LanguageMatchData languageMatchData = languageMatchData.get(type);
1471 //                if (languageMatchData == null) {
1472 //                    languageMatch.put(type, languageMatchData = new LanguageMatchData());
1473 //                }
1474                 break;
1475             case "matchVariable":
1476                 String id = parts.getAttributeValue(3, "id");
1477                 String value = parts.getAttributeValue(3, "value");
1478                 // TODO
1479                 break;
1480             case "languageMatch":
1481                 List<R4<String, String, Integer, Boolean>> matches = languageMatch.get(type);
1482                 if (matches == null) {
1483                     languageMatch.put(type, matches = new ArrayList<R4<String, String, Integer, Boolean>>());
1484                 }
1485                 String percent = parts.getAttributeValue(3, "percent");
1486                 String distance = parts.getAttributeValue(3, "distance");
1487                 matches.add(Row.of(
1488                     parts.getAttributeValue(3, "desired"),
1489                     parts.getAttributeValue(3, "supported"),
1490                     percent != null ? Integer.parseInt(percent)
1491                         : 100 - Integer.parseInt(distance),
1492                     "true".equals(parts.getAttributeValue(3, "oneway"))));
1493                 break;
1494             default:
1495                 throw new IllegalArgumentException("Unknown element");
1496             }
1497             return true;
1498         }
1499 
1500         private boolean handleCodeMappings(String level2) {
1501             if (level2.equals("territoryCodes")) {
1502                 // <territoryCodes type="VU" numeric="548" alpha3="VUT"/>
1503                 String type = parts.getAttributeValue(-1, "type");
1504                 final String numeric = parts.getAttributeValue(-1, "numeric");
1505                 if (numeric != null) {
1506                     numericTerritoryMapping.put(type, Integer.parseInt(numeric));
1507                 }
1508                 final String alpha3 = parts.getAttributeValue(-1, "alpha3");
1509                 if (alpha3 != null) {
1510                     alpha3TerritoryMapping.put(type, alpha3);
1511                 }
1512                 return true;
1513             } else if (level2.equals("currencyCodes")) {
1514                 // <currencyCodes type="BBD" numeric="52"/>
1515                 String type = parts.getAttributeValue(-1, "type");
1516                 final String numeric = parts.getAttributeValue(-1, "numeric");
1517                 if (numeric != null) {
1518                     numericCurrencyCodeMapping.put(type, Integer.parseInt(numeric));
1519                 }
1520                 return true;
1521             }
1522             return false;
1523         }
1524 
1525         private void handleNumberingSystems() {
1526             NumberingSystemInfo ns = new NumberingSystemInfo(parts);
1527             numberingSystems.put(ns.name, ns);
1528             if (ns.type == NumberingSystemType.numeric) {
1529                 numericSystems.add(ns.name);
1530             }
1531         }
1532 
1533         private void handleCoverageLevels() {
1534             if (parts.containsElement("approvalRequirement")) {
1535                 approvalRequirements.add(parts.toString());
1536             } else if (parts.containsElement("coverageLevel")) {
1537                 String match = parts.containsAttribute("match") ? coverageVariables.replace(parts.getAttributeValue(-1,
1538                     "match")) : null;
1539                 String valueStr = parts.getAttributeValue(-1, "value");
1540                 // Ticket 7125: map the number to English. So switch from English to number for construction
1541                 valueStr = Integer.toString(Level.get(valueStr).getLevel());
1542 
1543                 String inLanguage = parts.containsAttribute("inLanguage") ? coverageVariables.replace(parts
1544                     .getAttributeValue(-1, "inLanguage")) : null;
1545                 String inScript = parts.containsAttribute("inScript") ? coverageVariables.replace(parts
1546                     .getAttributeValue(-1, "inScript")) : null;
1547                 String inTerritory = parts.containsAttribute("inTerritory") ? coverageVariables.replace(parts
1548                     .getAttributeValue(-1, "inTerritory")) : null;
1549                 Integer value = (valueStr != null) ? Integer.valueOf(valueStr) : Integer.valueOf("101");
1550                 if (cldrVersion.getMajor() < 2) {
1551                     value = 40;
1552                 }
1553                 CoverageLevelInfo ci = new CoverageLevelInfo(match, value, inLanguage, inScript, inTerritory);
1554                 coverageLevels.add(ci);
1555             } else if (parts.containsElement("coverageVariable")) {
1556                 String key = parts.getAttributeValue(-1, "key");
1557                 String value = parts.getAttributeValue(-1, "value");
1558                 coverageVariables.add(key, value);
1559             }
1560         }
1561 
1562         private void handleParentLocales() {
1563             String parent = parts.getAttributeValue(-1, "parent");
1564             String locales = parts.getAttributeValue(-1, "locales");
1565             String[] pl = locales.split(" ");
1566             for (int i = 0; i < pl.length; i++) {
1567                 parentLocales.put(pl[i], parent);
1568             }
1569         }
1570 
1571         private void handleCalendarPreferenceData() {
1572             String territoryString = parts.getAttributeValue(-1, "territories");
1573             String orderingString = parts.getAttributeValue(-1, "ordering");
1574             String[] calendars = orderingString.split(" ");
1575             String[] territories = territoryString.split(" ");
1576             List<String> calendarList = Arrays.asList(calendars);
1577             for (int i = 0; i < territories.length; i++) {
1578                 calendarPreferences.put(territories[i], calendarList);
1579             }
1580         }
1581 
1582         private void handleLikelySubtags() {
1583             String from = parts.getAttributeValue(-1, "from");
1584             String to = parts.getAttributeValue(-1, "to");
1585             likelySubtags.put(from, to);
1586         }
1587 
1588         /**
1589          * Only called if level2 = mapTimezones. Level 1 might be metaZones or might be windowsZones
1590          */
1591         private boolean handleMetazoneData(String level2, String level3) {
1592             if (level3.equals("mapZone")) {
1593                 String maintype = parts.getAttributeValue(2, "type");
1594                 if (maintype == null) {
1595                     maintype = "windows";
1596                 }
1597                 String mzone = parts.getAttributeValue(3, "other");
1598                 String region = parts.getAttributeValue(3, "territory");
1599                 String zone = parts.getAttributeValue(3, "type");
1600 
1601                 Map<String, Map<String, String>> zoneToRegionToZone = typeToZoneToRegionToZone.get(maintype);
1602                 if (zoneToRegionToZone == null) {
1603                     typeToZoneToRegionToZone.put(maintype,
1604                         zoneToRegionToZone = new TreeMap<String, Map<String, String>>());
1605                 }
1606                 Map<String, String> regionToZone = zoneToRegionToZone.get(mzone);
1607                 if (regionToZone == null) {
1608                     zoneToRegionToZone.put(mzone, regionToZone = new TreeMap<String, String>());
1609                 }
1610                 if (region != null) {
1611                     regionToZone.put(region, zone);
1612                 }
1613                 if (maintype.equals("metazones")) {
1614                     if (mzone != null && region.equals("001")) {
1615                         metazoneContinentMap.put(mzone, zone.substring(0, zone.indexOf("/")));
1616                     }
1617                     allMetazones.add(mzone);
1618                 }
1619                 return true;
1620             }
1621             return false;
1622         }
1623 
1624         private Collection<String> getSpaceDelimited(int index, String attribute, Collection<String> defaultValue) {
1625             String temp = parts.getAttributeValue(index, attribute);
1626             Collection<String> elements = temp == null ? defaultValue : Arrays.asList(temp.split("\\s+"));
1627             return elements;
1628         }
1629 
1630         /*
1631          *
1632          * <supplementalData>
1633          * <metaZones>
1634          * <metazoneInfo>
1635          * <timezone type="Asia/Yerevan">
1636          * <usesMetazone to="1991-09-22 20:00" mzone="Yerevan"/>
1637          * <usesMetazone from="1991-09-22 20:00" mzone="Armenia"/>
1638          */
1639 
1640         private boolean handleMetazoneInfo(String level2, String level3) {
1641             if (level3.equals("timezone")) {
1642                 String zone = parts.getAttributeValue(3, "type");
1643                 String mzone = parts.getAttributeValue(4, "mzone");
1644                 String from = parts.getAttributeValue(4, "from");
1645                 String to = parts.getAttributeValue(4, "to");
1646                 MetaZoneRange mzoneRange = new MetaZoneRange(mzone, from, to);
1647                 zoneToMetaZoneRanges.put(zone, mzoneRange);
1648                 return true;
1649             }
1650             return false;
1651         }
1652 
1653         private boolean handleMetadata(String level2, String value) {
1654             if (parts.contains("defaultContent")) {
1655                 String defContent = parts.getAttributeValue(-1, "locales").trim();
1656                 String[] defLocales = defContent.split("\\s+");
1657                 defaultContentLocales = Collections.unmodifiableSet(new TreeSet<String>(Arrays.asList(defLocales)));
1658                 return true;
1659             }
1660             if (level2.equals("alias")) {
1661                 // <alias>
1662                 // <!-- grandfathered 3066 codes -->
1663                 // <languageAlias type="art-lojban" replacement="jbo"/> <!-- Lojban -->
1664                 String level3 = parts.getElement(3);
1665                 if (!level3.endsWith("Alias")) {
1666                     throw new IllegalArgumentException();
1667                 }
1668                 level3 = level3.substring(0, level3.length() - "Alias".length());
1669                 boolean isSubdivision = level3.equals("subdivision");
1670                 Map<String, R2<List<String>, String>> tagToReplacement = typeToTagToReplacement.get(level3);
1671                 if (tagToReplacement == null) {
1672                     typeToTagToReplacement.put(level3,
1673                         tagToReplacement = new TreeMap<String, R2<List<String>, String>>());
1674                 }
1675                 final String replacement = parts.getAttributeValue(3, "replacement");
1676                 List<String> replacementList = null;
1677                 if (replacement != null) {
1678                     Set<String> builder = new LinkedHashSet<>();
1679                     for (String item : replacement.split("\\s+")) {
1680                         String cleaned = SubdivisionNames.isRegionCode(item) ? item : replacement.replace("-", "").toLowerCase(Locale.ROOT);
1681 //                            : replacement.replace("-", "_");
1682                         builder.add(cleaned);
1683                     }
1684                     replacementList = ImmutableList.copyOf(builder);
1685                 }
1686                 final String reason = parts.getAttributeValue(3, "reason");
1687                 String cleanTag = parts.getAttributeValue(3, "type");
1688                 tagToReplacement.put(cleanTag, (R2<List<String>, String>) Row.of(replacementList, reason).freeze());
1689                 return true;
1690             } else if (level2.equals("validity")) {
1691                 // <variable id="$grandfathered" type="choice">
1692                 String level3 = parts.getElement(3);
1693                 if (level3.equals("variable")) {
1694                     Map<String, String> attributes = parts.getAttributes(-1);
1695                     validityInfo.put(attributes.get("id"), Row.of(attributes.get("type"), value));
1696                     String idString = attributes.get("id");
1697                     if (("$language".equals(idString) || "$languageExceptions".equals(attributes.get("id")))
1698                         && "choice".equals(attributes.get("type"))) {
1699                         String[] validCodeArray = value.trim().split("\\s+");
1700                         CLDRLanguageCodes.addAll(Arrays.asList(validCodeArray));
1701                     }
1702 //                    if ("$script".equals(attributes.get("id")) && "choice".equals(attributes.get("type"))) {
1703 //                        String[] validCodeArray = value.trim().split("\\s+");
1704 //                        CLDRScriptCodes = Collections
1705 //                            .unmodifiableSet(new TreeSet<String>(Arrays.asList(validCodeArray)));
1706 //                    }
1707                     return true;
1708                 } else if (level3.equals("attributeValues")) {
1709                     AttributeValidityInfo.add(parts.getAttributes(-1), value, attributeValidityInfo);
1710                     return true;
1711                 }
1712             } else if (level2.equals("serialElements")) {
1713                 serialElements = Arrays.asList(value.trim().split("\\s+"));
1714                 return true;
1715             } else if (level2.equals("distinguishing")) {
1716                 String level3 = parts.getElement(3);
1717                 if (level3.equals("distinguishingItems")) {
1718                     Map<String, String> attributes = parts.getAttributes(-1);
1719                     // <distinguishingItems
1720                     // attributes="key request id _q registry alt iso4217 iso3166 mzone from to type numberSystem"/>
1721                     // <distinguishingItems exclude="true"
1722                     // elements="default measurementSystem mapping abbreviationFallback preferenceOrdering"
1723                     // attributes="type"/>
1724 
1725                     if (attributes.containsKey("exclude") && "true".equals(attributes.get("exclude"))) {
1726                         return false; // don't handle the excludes -yet.
1727                     } else {
1728                         distinguishingAttributes = Collections.unmodifiableCollection(getSpaceDelimited(-1,
1729                             "attributes", STAR_SET));
1730                         return true;
1731                     }
1732                 }
1733             }
1734             return false;
1735         }
1736 
1737         private boolean handleTerritoryInfo() {
1738 
1739             // <territoryInfo>
1740             // <territory type="AD" gdp="1840000000" literacyPercent="100"
1741             // population="66000"> <!--Andorra-->
1742             // <languagePopulation type="ca" populationPercent="50"/>
1743             // <!--Catalan-->
1744 
1745             Map<String, String> territoryAttributes = parts.getAttributes(2);
1746             String territory = territoryAttributes.get("type");
1747             double territoryPopulation = parseDouble(territoryAttributes.get("population"));
1748             if (failsRangeCheck("population", territoryPopulation, 0, MAX_POPULATION)) {
1749                 return true;
1750             }
1751 
1752             double territoryLiteracyPercent = parseDouble(territoryAttributes.get("literacyPercent"));
1753             double territoryGdp = parseDouble(territoryAttributes.get("gdp"));
1754             if (territoryToPopulationData.get(territory) == null) {
1755                 territoryToPopulationData.put(territory, new PopulationData()
1756                     .setPopulation(territoryPopulation)
1757                     .setLiteratePopulation(territoryLiteracyPercent * territoryPopulation / 100)
1758                     .setGdp(territoryGdp));
1759             }
1760             if (parts.size() > 3) {
1761 
1762                 Map<String, String> languageInTerritoryAttributes = parts
1763                     .getAttributes(3);
1764                 String language = languageInTerritoryAttributes.get("type");
1765                 double languageLiteracyPercent = parseDouble(languageInTerritoryAttributes.get("literacyPercent"));
1766                 if (Double.isNaN(languageLiteracyPercent)) {
1767                     languageLiteracyPercent = territoryLiteracyPercent;
1768                 }
1769                 double writingPercent = parseDouble(languageInTerritoryAttributes.get("writingPercent"));
1770                 if (Double.isNaN(writingPercent)) {
1771                     writingPercent = languageLiteracyPercent;
1772                 }
1773                 // else {
1774                 // System.out.println("writingPercent\t" + languageLiteracyPercent
1775                 // + "\tterritory\t" + territory
1776                 // + "\tlanguage\t" + language);
1777                 // }
1778                 double languagePopulationPercent = parseDouble(languageInTerritoryAttributes.get("populationPercent"));
1779                 double languagePopulation = languagePopulationPercent * territoryPopulation / 100;
1780                 // double languageGdp = languagePopulationPercent * territoryGdp;
1781 
1782                 // store
1783                 Map<String, PopulationData> territoryLanguageToPopulation = territoryToLanguageToPopulationData
1784                     .get(territory);
1785                 if (territoryLanguageToPopulation == null) {
1786                     territoryToLanguageToPopulationData.put(territory,
1787                         territoryLanguageToPopulation = new TreeMap<String, PopulationData>());
1788                 }
1789                 OfficialStatus officialStatus = OfficialStatus.unknown;
1790                 String officialStatusString = languageInTerritoryAttributes.get("officialStatus");
1791                 if (officialStatusString != null) officialStatus = OfficialStatus.valueOf(officialStatusString);
1792 
1793                 PopulationData newData = new PopulationData()
1794                     .setPopulation(languagePopulation)
1795                     .setLiteratePopulation(languageLiteracyPercent * languagePopulation / 100)
1796                     .setWritingPopulation(writingPercent * languagePopulation / 100)
1797                     .setOfficialStatus(officialStatus)
1798                 // .setGdp(languageGdp)
1799                 ;
1800                 newData.freeze();
1801                 if (territoryLanguageToPopulation.get(language) != null) {
1802                     System.out
1803                         .println("Internal Problem in supplementalData: multiple data items for "
1804                             + language + ", " + territory + "\tSkipping " + newData);
1805                     return true;
1806                 }
1807 
1808                 territoryLanguageToPopulation.put(language, newData);
1809                 // add the language, using the Pair fields to get the ordering right
1810                 languageToTerritories2.put(language,
1811                     Pair.of(newData.getOfficialStatus().isMajor() ? false : true,
1812                         Pair.of(-newData.getLiteratePopulation(), territory)));
1813 
1814                 // now collect data for languages globally
1815                 PopulationData data = languageToPopulation.get(language);
1816                 if (data == null) {
1817                     languageToPopulation.put(language, data = new PopulationData().set(newData));
1818                 } else {
1819                     data.add(newData);
1820                 }
1821                 // if (language.equals("en")) {
1822                 // System.out.println(territory + "\tnewData:\t" + newData + "\tdata:\t" + data);
1823                 // }
1824                 String baseLanguage = languageTagParser.set(language).getLanguage();
1825                 data = baseLanguageToPopulation.get(baseLanguage);
1826                 if (data == null) {
1827                     baseLanguageToPopulation.put(baseLanguage, data = new PopulationData().set(newData));
1828                 } else {
1829                     data.add(newData);
1830                 }
1831                 if (!baseLanguage.equals(language)) {
1832                     languageToScriptVariants.put(baseLanguage, language);
1833                 }
1834             }
1835             return true;
1836         }
1837 
1838         private boolean handleCurrencyData(String level2) {
1839             if (level2.equals("fractions")) {
1840                 // <info iso4217="ADP" digits="0" rounding="0" cashRounding="5"/>
1841                 currencyToCurrencyNumberInfo.put(parts.getAttributeValue(3, "iso4217"),
1842                     new CurrencyNumberInfo(
1843                         parseIntegerOrNull(parts.getAttributeValue(3, "digits")),
1844                         parseIntegerOrNull(parts.getAttributeValue(3, "rounding")),
1845                         parseIntegerOrNull(parts.getAttributeValue(3, "cashDigits")),
1846                         parseIntegerOrNull(parts.getAttributeValue(3, "cashRounding"))));
1847                 return true;
1848             }
1849             /*
1850              * <region iso3166="AD">
1851              * <currency iso4217="EUR" from="1999-01-01"/>
1852              * <currency iso4217="ESP" from="1873" to="2002-02-28"/>
1853              */
1854             if (level2.equals("region")) {
1855                 territoryToCurrencyDateInfo.put(parts.getAttributeValue(2, "iso3166"),
1856                     new CurrencyDateInfo(parts.getAttributeValue(3, "iso4217"),
1857                         parts.getAttributeValue(3, "from"),
1858                         parts.getAttributeValue(3, "to"),
1859                         parts.getAttributeValue(3, "tender")));
1860                 return true;
1861             }
1862 
1863             return false;
1864         }
1865 
1866         private void handleTelephoneCodeData(XPathParts parts) {
1867             // element 2: codesByTerritory territory [draft] [references]
1868             String terr = parts.getAttributeValue(2, "territory");
1869             // element 3: telephoneCountryCode code [from] [to] [draft] [references] [alt]
1870             TelephoneCodeInfo tcInfo = new TelephoneCodeInfo(parts.getAttributeValue(3, "code"),
1871                 parts.getAttributeValue(3, "from"),
1872                 parts.getAttributeValue(3, "to"),
1873                 parts.getAttributeValue(3, "alt"));
1874 
1875             Set<TelephoneCodeInfo> tcSet = territoryToTelephoneCodeInfo.get(terr);
1876             if (tcSet == null) {
1877                 tcSet = new LinkedHashSet<TelephoneCodeInfo>();
1878                 territoryToTelephoneCodeInfo.put(terr, tcSet);
1879             }
1880             tcSet.add(tcInfo);
1881         }
1882 
1883         private void handleTerritoryContainment() {
1884             // <group type="001" contains="002 009 019 142 150"/>
1885             final String container = parts.getAttributeValue(-1, "type");
1886             final List<String> contained = Arrays
1887                 .asList(parts.getAttributeValue(-1, "contains").split("\\s+"));
1888             // everything!
1889             containment.putAll(container, contained);
1890 
1891             String status = parts.getAttributeValue(-1, "status");
1892             String grouping = parts.getAttributeValue(-1, "grouping");
1893             if (status == null && grouping == null) {
1894                 containmentCore.putAll(container, contained);
1895             }
1896             if (status != null && status.equals("deprecated")) {
1897                 containmentDeprecated.putAll(container, contained);
1898             }
1899             if (grouping != null) {
1900                 containmentGrouping.putAll(container, contained);
1901             }
1902         }
1903 
1904         private void handleSubdivisionContainment() {
1905             //      <subgroup type="AL" subtype="04" contains="FR MK LU"/>
1906             final String country = parts.getAttributeValue(-1, "type");
1907             final String subtype = parts.getAttributeValue(-1, "subtype");
1908             final String container = subtype == null ? country : (country + subtype).toLowerCase(Locale.ROOT);
1909             for (String contained : parts.getAttributeValue(-1, "contains").split("\\s+")) {
1910                 String newContained = contained.charAt(0) >= 'a' ? contained : (country + contained).toLowerCase(Locale.ROOT);
1911                 containerToSubdivision.put(container, newContained);
1912             }
1913         }
1914 
1915         private void handleLanguageData() {
1916             // <languageData>
1917             // <language type="aa" scripts="Latn" territories="DJ ER ET"/> <!--
1918             // Reflecting submitted data, cldrbug #1013 -->
1919             // <language type="ab" scripts="Cyrl" territories="GE"
1920             // alt="secondary"/>
1921             String language = (String) parts.getAttributeValue(2, "type");
1922             BasicLanguageData languageData = new BasicLanguageData();
1923             languageData
1924                 .setType(parts.getAttributeValue(2, "alt") == null ? BasicLanguageData.Type.primary
1925                     : BasicLanguageData.Type.secondary);
1926             languageData.setScripts(parts.getAttributeValue(2, "scripts"))
1927                 .setTerritories(parts.getAttributeValue(2, "territories"));
1928             Map<Type, BasicLanguageData> map = languageToBasicLanguageData.get(language);
1929             if (map == null) {
1930                 languageToBasicLanguageData.put(language, map = new EnumMap<Type, BasicLanguageData>(
1931                     BasicLanguageData.Type.class));
1932             }
1933             if (map.containsKey(languageData.type)) {
1934                 throw new IllegalArgumentException("Duplicate value:\t" + parts);
1935             }
1936             map.put(languageData.type, languageData);
1937         }
1938 
1939         private boolean failsRangeCheck(String path, double input, double min, double max) {
1940             if (input >= min && input <= max) {
1941                 return false;
1942             }
1943             System.out
1944                 .println("Internal Problem in supplementalData: range check fails for "
1945                     + input + ", min: " + min + ", max:" + max + "\t" + path);
1946 
1947             return false;
1948         }
1949 
1950         private double parseDouble(String literacyString) {
1951             return literacyString == null ? Double.NaN : Double
1952                 .parseDouble(literacyString);
1953         }
1954     }
1955 
1956     public class CoverageVariableInfo {
1957         public Set<String> targetScripts;
1958         public Set<String> targetTerritories;
1959         public Set<String> calendars;
1960         public Set<String> targetCurrencies;
1961         public Set<String> targetTimeZones;
1962         public Set<String> targetPlurals;
1963     }
1964 
1965     public static String toRegexString(Set<String> s) {
1966         Iterator<String> it = s.iterator();
1967         StringBuilder sb = new StringBuilder("(");
1968         int count = 0;
1969         while (it.hasNext()) {
1970             if (count > 0) {
1971                 sb.append("|");
1972             }
1973             sb.append(it.next());
1974             count++;
1975         }
1976         sb.append(")");
1977         return sb.toString();
1978 
1979     }
1980 
1981     public int parseIntegerOrNull(String attributeValue) {
1982         return attributeValue == null ? -1 : Integer.parseInt(attributeValue);
1983     }
1984 
1985     Set<String> skippedElements = new TreeSet<String>();
1986 
1987     private Map<String, Pair<String, String>> references = new TreeMap<String, Pair<String, String>>();
1988     private Map<String, String> likelySubtags = new TreeMap<String, String>();
1989     // make public temporarily until we resolve.
1990     private SortedSet<CoverageLevelInfo> coverageLevels = new TreeSet<CoverageLevelInfo>();
1991     private Map<String, String> parentLocales = new HashMap<String, String>();
1992     private Map<String, List<String>> calendarPreferences = new HashMap<String, List<String>>();
1993     private Map<String, CoverageVariableInfo> localeSpecificVariables = new TreeMap<String, CoverageVariableInfo>();
1994     private VariableReplacer coverageVariables = new VariableReplacer();
1995     private Map<String, NumberingSystemInfo> numberingSystems = new HashMap<String, NumberingSystemInfo>();
1996     private Set<String> numericSystems = new TreeSet<String>();
1997     private Set<String> defaultContentLocales;
1998     public Map<CLDRLocale, CLDRLocale> baseToDefaultContent; // wo -> wo_Arab_SN
1999     public Map<CLDRLocale, CLDRLocale> defaultContentToBase; // wo_Arab_SN -> wo
2000     private Set<String> CLDRLanguageCodes = new TreeSet<String>();;
2001     private Set<String> CLDRScriptCodes;
2002 
2003     /**
2004      * Get the population data for a language. Warning: if the language has script variants, cycle on those variants.
2005      *
2006      * @param language
2007      * @param output
2008      * @return
2009      */
2010     public PopulationData getLanguagePopulationData(String language) {
2011         return languageToPopulation.get(language);
2012     }
2013 
2014     public PopulationData getBaseLanguagePopulationData(String language) {
2015         return baseLanguageToPopulation.get(language);
2016     }
2017 
2018     public Set<String> getLanguages() {
2019         return allLanguages;
2020     }
2021 
2022     public Set<String> getTerritoryToLanguages(String territory) {
2023         Map<String, PopulationData> result = territoryToLanguageToPopulationData
2024             .get(territory);
2025         if (result == null) {
2026             return Collections.emptySet();
2027         }
2028         return result.keySet();
2029     }
2030 
2031     public PopulationData getLanguageAndTerritoryPopulationData(String language,
2032         String territory) {
2033         Map<String, PopulationData> result = territoryToLanguageToPopulationData
2034             .get(territory);
2035         if (result == null) {
2036             return null;
2037         }
2038         return result.get(language);
2039     }
2040 
2041     public Set<String> getTerritoriesWithPopulationData() {
2042         return territoryToLanguageToPopulationData.keySet();
2043     }
2044 
2045     public Set<String> getLanguagesForTerritoryWithPopulationData(String territory) {
2046         return territoryToLanguageToPopulationData.get(territory).keySet();
2047     }
2048 
2049     public Set<BasicLanguageData> getBasicLanguageData(String language) {
2050         Map<Type, BasicLanguageData> map = languageToBasicLanguageData.get(language);
2051         if (map == null) {
2052             throw new IllegalArgumentException("Bad language code: " + language);
2053         }
2054         return new LinkedHashSet<BasicLanguageData>(map.values());
2055     }
2056 
2057     public Map<Type, BasicLanguageData> getBasicLanguageDataMap(String language) {
2058         return languageToBasicLanguageData.get(language);
2059     }
2060 
2061     public Set<String> getBasicLanguageDataLanguages() {
2062         return languageToBasicLanguageData.keySet();
2063     }
2064 
2065     public Relation<String, String> getContainmentCore() {
2066         return containmentCore;
2067     }
2068 
2069     public Set<String> getContained(String territoryCode) {
2070         return containment.getAll(territoryCode);
2071     }
2072 
2073     public Set<String> getContainers() {
2074         return containment.keySet();
2075     }
2076 
2077     public Set<String> getContainedSubdivisions(String territoryOrSubdivisionCode) {
2078         return containerToSubdivision.getAll(territoryOrSubdivisionCode);
2079     }
2080 
2081     public Set<String> getContainersForSubdivisions() {
2082         return containerToSubdivision.keySet();
2083     }
2084 
2085     public Relation<String, String> getTerritoryToContained() {
2086         return getTerritoryToContained(ContainmentStyle.all); // true
2087     }
2088 
2089     //    public Relation<String, String> getTerritoryToContained(boolean allowDeprecated) {
2090     //        return allowDeprecated ? containment : containmentNonDeprecated;
2091     //    }
2092     //
2093     public enum ContainmentStyle {
2094         all, core, grouping, deprecated
2095     }
2096 
2097     public Relation<String, String> getTerritoryToContained(ContainmentStyle containmentStyle) {
2098         switch (containmentStyle) {
2099         case all:
2100             return containment;
2101         case core:
2102             return containmentCore;
2103         case grouping:
2104             return containmentGrouping;
2105         case deprecated:
2106             return containmentDeprecated;
2107         }
2108         throw new IllegalArgumentException("internal error");
2109     }
2110 
2111     public Set<String> getSkippedElements() {
2112         return skippedElements;
2113     }
2114 
2115     public Set<String> getZone_aliases(String zone) {
2116         Set<String> result = zone_aliases.getAll(zone);
2117         if (result == null) {
2118             return Collections.emptySet();
2119         }
2120         return result;
2121     }
2122 
2123     public String getZone_territory(String zone) {
2124         return zone_territory.get(zone);
2125     }
2126 
2127     public Set<String> getCanonicalZones() {
2128         return zone_territory.keySet();
2129     }
2130 
2131     /**
2132      * Return the multizone countries (should change name).
2133      *
2134      * @return
2135      */
2136     public Set<String> getMultizones() {
2137         // TODO Auto-generated method stub
2138         return multizone;
2139     }
2140 
2141     private Set<String> singleRegionZones;
2142 
2143     public Set<String> getSingleRegionZones() {
2144         synchronized (this) {
2145             if (singleRegionZones == null) {
2146                 singleRegionZones = new HashSet<String>();
2147                 SupplementalDataInfo supplementalData = this; // TODO: this?
2148                 Set<String> multizoneCountries = supplementalData.getMultizones();
2149                 for (String zone : supplementalData.getCanonicalZones()) {
2150                     String region = supplementalData.getZone_territory(zone);
2151                     if (!multizoneCountries.contains(region) || zone.startsWith("Etc/")) {
2152                         singleRegionZones.add(zone);
2153                     }
2154                 }
2155                 singleRegionZones.remove("Etc/Unknown"); // remove special case
2156                 singleRegionZones = Collections.unmodifiableSet(singleRegionZones);
2157             }
2158         }
2159         return singleRegionZones;
2160     }
2161 
2162     public Set<String> getTerritoriesForPopulationData(String language) {
2163         return languageToTerritories.getAll(language);
2164     }
2165 
2166     public Set<String> getLanguagesForTerritoriesPopulationData() {
2167         return languageToTerritories.keySet();
2168     }
2169 
2170     /**
2171      * Return the list of default content locales.
2172      *
2173      * @return
2174      */
2175     public Set<String> getDefaultContentLocales() {
2176         return defaultContentLocales;
2177     }
2178 
2179     public static Map<String, String> makeLocaleToDefaultContents(Set<String> defaultContents,
2180         Map<String, String> result, Set<String> errors) {
2181         for (String s : defaultContents) {
2182             String simpleParent = LanguageTagParser.getSimpleParent(s);
2183             String oldValue = result.get(simpleParent);
2184             if (oldValue != null) {
2185                 errors.add("*** Error: Default contents cannot contain two children for the same parent:\t"
2186                     + oldValue + ", " + s + "; keeping " + oldValue);
2187                 continue;
2188             }
2189             result.put(simpleParent, s);
2190         }
2191         return result;
2192     }
2193 
2194     /**
2195      * Return the list of default content locales.
2196      *
2197      * @return
2198      */
2199     public Set<CLDRLocale> getDefaultContentCLDRLocales() {
2200         initCLDRLocaleBasedData();
2201         return defaultContentToBase.keySet();
2202     }
2203 
2204     /**
2205      * Get the default content locale for a specified language
2206      *
2207      * @param language
2208      *            language to search
2209      * @return default content, or null if none
2210      */
2211     public String getDefaultContentLocale(String language) {
2212         for (String dc : defaultContentLocales) {
2213             if (dc.startsWith(language + "_")) {
2214                 return dc;
2215             }
2216         }
2217         return null;
2218     }
2219 
2220     /**
2221      * Get the default content locale for a specified language and script.
2222      * If script is null, delegates to {@link #getDefaultContentLocale(String)}
2223      *
2224      * @param language
2225      * @param script
2226      *            if null, delegates to {@link #getDefaultContentLocale(String)}
2227      * @return default content, or null if none
2228      */
2229     public String getDefaultContentLocale(String language, String script) {
2230         if (script == null) return getDefaultContentLocale(language);
2231         for (String dc : defaultContentLocales) {
2232             if (dc.startsWith(language + "_" + script + "_")) {
2233                 return dc;
2234             }
2235         }
2236         return null;
2237     }
2238 
2239     /**
2240      * Given a default locale (such as 'wo_Arab_SN') return the base locale (such as 'wo'), or null if the input wasn't
2241      * a default conetnt locale.
2242      *
2243      * @param baseLocale
2244      * @return
2245      */
2246     public CLDRLocale getBaseFromDefaultContent(CLDRLocale dcLocale) {
2247         initCLDRLocaleBasedData();
2248         return defaultContentToBase.get(dcLocale);
2249     }
2250 
2251     /**
2252      * Given a base locale (such as 'wo') return the default content locale (such as 'wo_Arab_SN'), or null.
2253      *
2254      * @param baseLocale
2255      * @return
2256      */
2257     public CLDRLocale getDefaultContentFromBase(CLDRLocale baseLocale) {
2258         initCLDRLocaleBasedData();
2259         return baseToDefaultContent.get(baseLocale);
2260     }
2261 
2262     /**
2263      * Is this a default content locale?
2264      *
2265      * @param dcLocale
2266      * @return
2267      */
2268     public boolean isDefaultContent(CLDRLocale dcLocale) {
2269         initCLDRLocaleBasedData();
2270         if (dcLocale == null) throw new NullPointerException("null locale");
2271         return (defaultContentToBase.get(dcLocale) != null);
2272     }
2273 
2274     public Set<String> getNumberingSystems() {
2275         return numberingSystems.keySet();
2276     }
2277 
2278     public Set<String> getNumericNumberingSystems() {
2279         return Collections.unmodifiableSet(numericSystems);
2280     }
2281 
2282     public String getDigits(String numberingSystem) {
2283         try {
2284             return numberingSystems.get(numberingSystem).digits;
2285         } catch (Exception e) {
2286             throw new IllegalArgumentException("Can't get digits for:" + numberingSystem);
2287         }
2288     }
2289 
2290     public NumberingSystemType getNumberingSystemType(String numberingSystem) {
2291         return numberingSystems.get(numberingSystem).type;
2292     }
2293 
2294     public SortedSet<CoverageLevelInfo> getCoverageLevelInfo() {
2295         return coverageLevels;
2296     }
2297 
2298     /**
2299      * Used to get the coverage value for a path. This is generally the most
2300      * efficient way for tools to get coverage.
2301      *
2302      * @param xpath
2303      * @param loc
2304      * @return
2305      */
2306     public Level getCoverageLevel(String xpath, String loc) {
2307         Level result = null;
2308         result = coverageCache.get(xpath, loc);
2309         if (result == null) {
2310             CoverageLevel2 cov = localeToCoverageLevelInfo.get(loc);
2311             if (cov == null) {
2312                 cov = CoverageLevel2.getInstance(this, loc);
2313                 localeToCoverageLevelInfo.put(loc, cov);
2314             }
2315 
2316             result = cov.getLevel(xpath);
2317             coverageCache.put(xpath, loc, result);
2318         }
2319         return result;
2320     }
2321 
2322     /**
2323      * Cache Data structure with object expiry,
2324      * List that can hold up to MAX_LOCALES caches of locales, when one locale hasn't been used for a while it will removed and GC'd
2325      */
2326     private class CoverageCache {
2327         private final Deque<Node> localeList = new LinkedList<>();
2328         private final int MAX_LOCALES = 10;
2329 
2330         /**
2331          * Object to sync on for modifying the locale list
2332          */
2333         private final Object LOCALE_LIST_ITER_SYNC = new Object();
2334 
2335         /*
2336          * constructor
2337          */
2338         public CoverageCache() {
2339 //            localeList = new LinkedList<Node>();
2340         }
2341 
2342         /*
2343          * retrieves coverage level associated with two keys if it exists in the cache, otherwise returns null
2344          * @param xpath
2345          * @param loc
2346          * @return the coverage level of the above two keys
2347          */
2348         public Level get(String xpath, String loc) {
2349             synchronized (LOCALE_LIST_ITER_SYNC) {
2350                 Iterator<Node> it = localeList.iterator();
2351                 Node reAddNode = null;
2352                 while (it.hasNext()) {
2353 //            for (Iterator<Node> it = localeList.iterator(); it.hasNext();) {
2354                     Node node = it.next();
2355                     if (node.loc.equals(loc)) {
2356                         reAddNode = node;
2357                         it.remove();
2358                         break;
2359 
2360                     }
2361                 }
2362                 if (reAddNode != null) {
2363                     localeList.addFirst(reAddNode);
2364                     return reAddNode.map.get(xpath);
2365                 }
2366                 return null;
2367             }
2368         }
2369 
2370         /*
2371          * places a coverage level into the cache, with two keys
2372          * @param xpath
2373          * @param loc
2374          * @param covLevel    the coverage level of the above two keys
2375          */
2376         public void put(String xpath, String loc, Level covLevel) {
2377             synchronized (LOCALE_LIST_ITER_SYNC) {
2378                 //if locale's map is already in the cache add to it
2379 //            for (Iterator<Node> it = localeList.iterator(); it.hasNext();) {
2380                 for (Node node : localeList) {
2381 //                Node node = it.next();
2382                     if (node.loc.equals(loc)) {
2383                         node.map.put(xpath, covLevel);
2384                         return;
2385                     }
2386                 }
2387 
2388                 //if it is not, add a new map with the coverage level, and remove the last map in the list (used most seldom) if the list is too large
2389                 Map<String, Level> newMap = new ConcurrentHashMap<String, Level>();
2390                 newMap.put(xpath, covLevel);
2391                 localeList.addFirst(new Node(loc, newMap));
2392 
2393                 if (localeList.size() > MAX_LOCALES) {
2394                     localeList.removeLast();
2395                 }
2396             }
2397         }
2398 
2399         /*
2400          * node to hold a location and a Map
2401          */
2402         private class Node {
2403             //public fields to emulate a C/C++ struct
2404             public String loc;
2405             public Map<String, Level> map;
2406 
2407             public Node(String _loc, Map<String, Level> _map) {
2408                 loc = _loc;
2409                 map = _map;
2410             }
2411         }
2412     }
2413 
2414     /**
2415      * Used to get the coverage value for a path. Note, it is more efficient to create
2416      * a CoverageLevel2 for a language, and keep it around.
2417      *
2418      * @param xpath
2419      * @param loc
2420      * @return
2421      */
2422     public int getCoverageValue(String xpath, String loc) {
2423         return getCoverageLevel(xpath, loc).getLevel();
2424     }
2425 
2426     private RegexLookup<Level> coverageLookup = null;
2427 
2428     public synchronized RegexLookup<Level> getCoverageLookup() {
2429         if (coverageLookup == null) {
2430             RegexLookup<Level> lookup = new RegexLookup<Level>(RegexLookup.LookupType.STAR_PATTERN_LOOKUP);
2431 
2432             Matcher variable = PatternCache.get("\\$\\{[A-Za-z][\\-A-Za-z]*\\}").matcher("");
2433 
2434             for (CoverageLevelInfo ci : getCoverageLevelInfo()) {
2435                 String pattern = ci.match.replace('\'', '"')
2436                     .replace("[@", "\\[@") // make sure that attributes are quoted
2437                     .replace("(", "(?:") // make sure that there are no capturing groups (beyond what we generate
2438                     .replace("(?:?!", "(?!"); // Allow negative lookahead
2439                 pattern = "^//ldml/" + pattern + "$"; // for now, force a complete match
2440                 String variableType = null;
2441                 variable.reset(pattern);
2442                 if (variable.find()) {
2443                     pattern = pattern.substring(0, variable.start()) + "([^\"]*)" + pattern.substring(variable.end());
2444                     variableType = variable.group();
2445                     if (variable.find()) {
2446                         throw new IllegalArgumentException("We can only handle a single variable on a line");
2447                     }
2448                 }
2449 
2450                 // .replaceAll("\\]","\\\\]");
2451                 lookup.add(new CoverageLevel2.MyRegexFinder(pattern, variableType, ci), ci.value);
2452             }
2453             coverageLookup = lookup;
2454         }
2455         return coverageLookup;
2456     }
2457 
2458     /**
2459      * This appears to be unused, so didn't provide new version.
2460      *
2461      * @param xpath
2462      * @return
2463      */
2464     public int getCoverageValueOld(String xpath) {
2465         ULocale loc = new ULocale("und");
2466         return getCoverageValueOld(xpath, loc);
2467     }
2468 
2469     /**
2470      * Older version of code.
2471      *
2472      * @param xpath
2473      * @param loc
2474      * @return
2475      */
2476     public int getCoverageValueOld(String xpath, ULocale loc) {
2477         String targetLanguage = loc.getLanguage();
2478 
2479         CoverageVariableInfo cvi = getCoverageVariableInfo(targetLanguage);
2480         String targetScriptString = toRegexString(cvi.targetScripts);
2481         String targetTerritoryString = toRegexString(cvi.targetTerritories);
2482         String calendarListString = toRegexString(cvi.calendars);
2483         String targetCurrencyString = toRegexString(cvi.targetCurrencies);
2484         String targetTimeZoneString = toRegexString(cvi.targetTimeZones);
2485         String targetPluralsString = toRegexString(cvi.targetPlurals);
2486         Iterator<CoverageLevelInfo> i = coverageLevels.iterator();
2487         while (i.hasNext()) {
2488             CoverageLevelInfo ci = i.next();
2489             String regex = "//ldml/" + ci.match.replace('\'', '"')
2490                 .replaceAll("\\[", "\\\\[")
2491                 .replaceAll("\\]", "\\\\]")
2492                 .replace("${Target-Language}", targetLanguage)
2493                 .replace("${Target-Scripts}", targetScriptString)
2494                 .replace("${Target-Territories}", targetTerritoryString)
2495                 .replace("${Target-TimeZones}", targetTimeZoneString)
2496                 .replace("${Target-Currencies}", targetCurrencyString)
2497                 .replace("${Target-Plurals}", targetPluralsString)
2498                 .replace("${Calendar-List}", calendarListString);
2499 
2500             // Special logic added for coverage fields that are only to be applicable
2501             // to certain territories
2502             if (ci.inTerritory != null) {
2503                 if (ci.inTerritory.equals("EU")) {
2504                     Set<String> containedTerritories = new HashSet<String>();
2505                     containedTerritories.addAll(getContained(ci.inTerritory));
2506                     containedTerritories.retainAll(cvi.targetTerritories);
2507                     if (containedTerritories.isEmpty()) {
2508                         continue;
2509                     }
2510                 } else {
2511                     if (!cvi.targetTerritories.contains(ci.inTerritory)) {
2512                         continue;
2513                     }
2514                 }
2515             }
2516             // Special logic added for coverage fields that are only to be applicable
2517             // to certain languages
2518             if (ci.inLanguage != null && !ci.inLanguage.matcher(targetLanguage).matches()) {
2519                 continue;
2520             }
2521 
2522             // Special logic added for coverage fields that are only to be applicable
2523             // to certain scripts
2524             if (ci.inScript != null && !cvi.targetScripts.contains(ci.inScript)) {
2525                 continue;
2526             }
2527 
2528             if (xpath.matches(regex)) {
2529                 return ci.value.getLevel();
2530             }
2531 
2532             if (xpath.matches(regex)) {
2533                 return ci.value.getLevel();
2534             }
2535         }
2536         return Level.OPTIONAL.getLevel(); // If no match then return highest possible value
2537     }
2538 
2539     public CoverageVariableInfo getCoverageVariableInfo(String targetLanguage) {
2540         CoverageVariableInfo cvi;
2541         if (localeSpecificVariables.containsKey(targetLanguage)) {
2542             cvi = localeSpecificVariables.get(targetLanguage);
2543         } else {
2544             cvi = new CoverageVariableInfo();
2545             cvi.targetScripts = getTargetScripts(targetLanguage);
2546             cvi.targetTerritories = getTargetTerritories(targetLanguage);
2547             cvi.calendars = getCalendars(cvi.targetTerritories);
2548             cvi.targetCurrencies = getCurrentCurrencies(cvi.targetTerritories);
2549             cvi.targetTimeZones = getCurrentTimeZones(cvi.targetTerritories);
2550             cvi.targetPlurals = getTargetPlurals(targetLanguage);
2551             localeSpecificVariables.put(targetLanguage, cvi);
2552         }
2553         return cvi;
2554     }
2555 
2556     private Set<String> getTargetScripts(String language) {
2557         Set<String> targetScripts = new HashSet<String>();
2558         try {
2559             Set<BasicLanguageData> langData = getBasicLanguageData(language);
2560             Iterator<BasicLanguageData> ldi = langData.iterator();
2561             while (ldi.hasNext()) {
2562                 BasicLanguageData bl = ldi.next();
2563                 Set<String> addScripts = bl.scripts;
2564                 if (addScripts != null && bl.getType() != BasicLanguageData.Type.secondary) {
2565                     targetScripts.addAll(addScripts);
2566                 }
2567             }
2568         } catch (Exception e) {
2569             // fall through
2570         }
2571 
2572         if (targetScripts.size() == 0) {
2573             targetScripts.add("Zzzz"); // Unknown Script
2574         }
2575         return targetScripts;
2576     }
2577 
2578     private Set<String> getTargetTerritories(String language) {
2579         Set<String> targetTerritories = new HashSet<String>();
2580         try {
2581             Set<BasicLanguageData> langData = getBasicLanguageData(language);
2582             Iterator<BasicLanguageData> ldi = langData.iterator();
2583             while (ldi.hasNext()) {
2584                 BasicLanguageData bl = ldi.next();
2585                 Set<String> addTerritories = bl.territories;
2586                 if (addTerritories != null && bl.getType() != BasicLanguageData.Type.secondary) {
2587                     targetTerritories.addAll(addTerritories);
2588                 }
2589             }
2590         } catch (Exception e) {
2591             // fall through
2592         }
2593         if (targetTerritories.size() == 0) {
2594             targetTerritories.add("ZZ");
2595         }
2596         return targetTerritories;
2597     }
2598 
2599     private Set<String> getCalendars(Set<String> territories) {
2600         Set<String> targetCalendars = new HashSet<String>();
2601         Iterator<String> it = territories.iterator();
2602         while (it.hasNext()) {
2603             List<String> addCalendars = getCalendars(it.next());
2604             if (addCalendars == null) {
2605                 continue;
2606             }
2607             targetCalendars.addAll(addCalendars);
2608         }
2609         return targetCalendars;
2610     }
2611 
2612     /**
2613      * @param territory
2614      * @return a list the calendars used in the specified territorys
2615      */
2616     public List<String> getCalendars(String territory) {
2617         return calendarPreferences.get(territory);
2618     }
2619 
2620     private Set<String> getCurrentCurrencies(Set<String> territories) {
2621         Date now = new Date();
2622         return getCurrentCurrencies(territories, now, now);
2623     }
2624 
2625     public Set<String> getCurrentCurrencies(Set<String> territories, Date startsBefore, Date endsAfter) {
2626         Set<String> targetCurrencies = new HashSet<String>();
2627         Iterator<String> it = territories.iterator();
2628         while (it.hasNext()) {
2629             Set<CurrencyDateInfo> targetCurrencyInfo = getCurrencyDateInfo(it.next());
2630             if (targetCurrencyInfo == null) {
2631                 continue;
2632             }
2633             Iterator<CurrencyDateInfo> it2 = targetCurrencyInfo.iterator();
2634             while (it2.hasNext()) {
2635                 CurrencyDateInfo cdi = it2.next();
2636                 if (cdi.getStart().before(startsBefore) && cdi.getEnd().after(endsAfter) && cdi.isLegalTender()) {
2637                     targetCurrencies.add(cdi.getCurrency());
2638                 }
2639             }
2640         }
2641         return targetCurrencies;
2642     }
2643 
2644     private Set<String> getCurrentTimeZones(Set<String> territories) {
2645         Set<String> targetTimeZones = new HashSet<String>();
2646         Iterator<String> it = territories.iterator();
2647         while (it.hasNext()) {
2648             String[] countryIDs = TimeZone.getAvailableIDs(it.next());
2649             for (int i = 0; i < countryIDs.length; i++) {
2650                 targetTimeZones.add(countryIDs[i]);
2651             }
2652         }
2653         return targetTimeZones;
2654     }
2655 
2656     private Set<String> getTargetPlurals(String language) {
2657         Set<String> targetPlurals = new HashSet<String>();
2658         targetPlurals.addAll(getPlurals(PluralType.cardinal, language).getCanonicalKeywords());
2659         // TODO: Kept 0 and 1 specifically until Mark figures out what to do with them.
2660         // They should be removed once this is done.
2661         targetPlurals.add("0");
2662         targetPlurals.add("1");
2663         return targetPlurals;
2664     }
2665 
2666     public String getExplicitParentLocale(String loc) {
2667         return parentLocales.get(loc);
2668     }
2669 
2670     public Set<String> getExplicitChildren() {
2671         return parentLocales.keySet();
2672     }
2673 
2674     public Collection<String> getExplicitParents() {
2675         return parentLocales.values();
2676     }
2677 
2678     private final static class ApprovalRequirementMatcher {
2679         @Override
2680         public String toString() {
2681             return locales + " / " + xpathMatcher + " = " + requiredVotes;
2682         }
2683 
2684         ApprovalRequirementMatcher(String xpath) {
2685             XPathParts parts = new XPathParts();
2686             parts.set(xpath);
2687             if (parts.containsElement("approvalRequirement")) {
2688                 requiredVotes = Integer.parseInt(parts.getAttributeValue(-1, "votes"));
2689                 String localeAttrib = parts.getAttributeValue(-1, "locales");
2690                 if (localeAttrib == null || localeAttrib.equals(STAR) || localeAttrib.isEmpty()) {
2691                     locales = null; // no locale listed == '*'
2692                 } else {
2693                     Set<CLDRLocale> localeList = new HashSet<CLDRLocale>();
2694                     String[] el = localeAttrib.split(" ");
2695                     for (int i = 0; i < el.length; i++) {
2696                         if (el[i].indexOf(":") == -1) { // Just a simple locale designation
2697                             localeList.add(CLDRLocale.getInstance(el[i]));
2698                         } else { // Org:CoverageLevel
2699                             String[] coverageLocaleParts = el[i].split(":", 2);
2700                             String org = coverageLocaleParts[0];
2701                             String level = coverageLocaleParts[1].toUpperCase();
2702                             Set<String> coverageLocales = sc.getLocaleCoverageLocales(Organization.fromString(org), EnumSet.of(Level.fromString(level)));
2703                             for (String cl : coverageLocales) {
2704                                 localeList.add(CLDRLocale.getInstance(cl));
2705                             }
2706                         }
2707                     }
2708                     locales = Collections.unmodifiableSet(localeList);
2709                 }
2710                 String xpathMatch = parts.getAttributeValue(-1, "paths");
2711                 if (xpathMatch == null || xpathMatch.isEmpty() || xpathMatch.equals(STAR)) {
2712                     xpathMatcher = null;
2713                 } else {
2714                     xpathMatcher = PatternCache.get(xpathMatch);
2715                 }
2716             } else {
2717                 throw new RuntimeException("Unknown approval requirement: " + xpath);
2718             }
2719         }
2720 
2721         final private Set<CLDRLocale> locales;
2722         final private Pattern xpathMatcher;
2723         final int requiredVotes;
2724 
2725         public static List<ApprovalRequirementMatcher> buildAll(List<String> approvalRequirements) {
2726             List<ApprovalRequirementMatcher> newList = new LinkedList<ApprovalRequirementMatcher>();
2727 
2728             for (String xpath : approvalRequirements) {
2729                 newList.add(new ApprovalRequirementMatcher(xpath));
2730             }
2731 
2732             return Collections.unmodifiableList(newList);
2733         }
2734 
2735         public boolean matches(CLDRLocale loc, PathHeader ph) {
2736             if (DEBUG) System.err.println(">> testing " + loc + " / " + ph + " vs " + toString());
2737             if (locales != null) {
2738                 if (!locales.contains(loc)) {
2739                     return false;
2740                 }
2741             }
2742             if (xpathMatcher != null) {
2743                 if (ph != null) {
2744                     if (!xpathMatcher.matcher(ph.getOriginalPath()).matches()) {
2745                         return false;
2746                     } else {
2747                         return true;
2748                     }
2749                 } else {
2750                     return false;
2751                 }
2752             }
2753             return true;
2754         }
2755 
2756         public int getRequiredVotes() {
2757             return requiredVotes;
2758         }
2759     }
2760 
2761     // run these from first to last to get the approval info.
2762     volatile List<ApprovalRequirementMatcher> approvalMatchers = null;
2763 
2764     /**
2765      * Only called by VoteResolver.
2766      * @param loc
2767      * @param PathHeader - which path this is applied to, or null if unknown.
2768      * @return
2769      */
2770     public int getRequiredVotes(CLDRLocale loc, PathHeader ph) {
2771         if (approvalMatchers == null) {
2772             approvalMatchers = ApprovalRequirementMatcher.buildAll(approvalRequirements);
2773         }
2774 
2775         for (ApprovalRequirementMatcher m : approvalMatchers) {
2776             if (m.matches(loc, ph)) {
2777                 return m.getRequiredVotes();
2778             }
2779         }
2780         throw new RuntimeException("Error: " + loc + " " + ph + " ran off the end of the approvalMatchers.");
2781     }
2782 
2783     /**
2784      * Return the canonicalized zone, or null if there is none.
2785      *
2786      * @param alias
2787      * @return
2788      */
2789     public String getZoneFromAlias(String alias) {
2790         String zone = alias_zone.get(alias);
2791         if (zone != null)
2792             return zone;
2793         if (zone_territory.get(alias) != null)
2794             return alias;
2795         return null;
2796     }
2797 
2798     public boolean isCanonicalZone(String alias) {
2799         return zone_territory.get(alias) != null;
2800     }
2801 
2802     /**
2803      * Return the approximate economic weight of this language, computed by taking
2804      * all of the languages in each territory, looking at the literate population
2805      * and dividing up the GDP of the territory (in PPP) according to the
2806      * proportion that language has of the total. This is only an approximation,
2807      * since the language information is not complete, languages may overlap
2808      * (bilingual speakers), the literacy figures may be estimated, and literacy
2809      * is only a rough proxy for weight of each language in the economy of the
2810      * territory.
2811      *
2812      * @param languageId
2813      * @return
2814      */
2815     public double getApproximateEconomicWeight(String targetLanguage) {
2816         double weight = 0;
2817         Set<String> territories = getTerritoriesForPopulationData(targetLanguage);
2818         if (territories == null) return weight;
2819         for (String territory : territories) {
2820             Set<String> languagesInTerritory = getTerritoryToLanguages(territory);
2821             double totalLiteratePopulation = 0;
2822             double targetLiteratePopulation = 0;
2823             for (String language : languagesInTerritory) {
2824                 PopulationData populationData = getLanguageAndTerritoryPopulationData(
2825                     language, territory);
2826                 totalLiteratePopulation += populationData.getLiteratePopulation();
2827                 if (language.equals(targetLanguage)) {
2828                     targetLiteratePopulation = populationData.getLiteratePopulation();
2829                 }
2830             }
2831             PopulationData territoryPopulationData = getPopulationDataForTerritory(territory);
2832             final double gdp = territoryPopulationData.getGdp();
2833             final double scaledGdp = gdp * targetLiteratePopulation / totalLiteratePopulation;
2834             if (scaledGdp > 0) {
2835                 weight += scaledGdp;
2836             } else {
2837                 // System.out.println("?\t" + territory + "\t" + targetLanguage);
2838             }
2839         }
2840         return weight;
2841     }
2842 
2843     public PopulationData getPopulationDataForTerritory(String territory) {
2844         return territoryToPopulationData.get(territory);
2845     }
2846 
2847     public Set<String> getScriptVariantsForPopulationData(String language) {
2848         return languageToScriptVariants.getAll(language);
2849     }
2850 
2851     public Map<String, Pair<String, String>> getReferences() {
2852         return references;
2853     }
2854 
2855     public Map<String, Map<String, String>> getMetazoneToRegionToZone() {
2856         return typeToZoneToRegionToZone.get("metazones");
2857     }
2858 
2859     public String getZoneForMetazoneByRegion(String metazone, String region) {
2860         String result = null;
2861         if (getMetazoneToRegionToZone().containsKey(metazone)) {
2862             Map<String, String> myMap = getMetazoneToRegionToZone().get(metazone);
2863             if (myMap.containsKey(region)) {
2864                 result = myMap.get(region);
2865             } else {
2866                 result = myMap.get("001");
2867             }
2868         }
2869 
2870         if (result == null) {
2871             result = "Etc/GMT";
2872         }
2873 
2874         return result;
2875     }
2876 
2877     public Map<String, Map<String, Map<String, String>>> getTypeToZoneToRegionToZone() {
2878         return typeToZoneToRegionToZone;
2879     }
2880 
2881     /**
2882      * @deprecated, use PathHeader.getMetazonePageTerritory
2883      */
2884     public Map<String, String> getMetazoneToContinentMap() {
2885         return metazoneContinentMap;
2886     }
2887 
2888     public Set<String> getAllMetazones() {
2889         return allMetazones;
2890     }
2891 
2892     public Map<String, String> getLikelySubtags() {
2893         return likelySubtags;
2894     }
2895 
2896     public enum PluralType {
2897         cardinal(PluralRules.PluralType.CARDINAL), ordinal(PluralRules.PluralType.ORDINAL);
2898 
2899         // add some gorp to interwork until we clean things up
2900 
2901         public final PluralRules.PluralType standardType;
2902 
2903         PluralType(PluralRules.PluralType standardType) {
2904             this.standardType = standardType;
2905         }
2906 
2907         public static PluralType fromStandardType(PluralRules.PluralType standardType) {
2908             return standardType == null ? null
2909                 : standardType == PluralRules.PluralType.CARDINAL ? cardinal
2910                     : ordinal;
2911         }
2912     };
2913 
2914     private EnumMap<PluralType, Map<String, PluralInfo>> localeToPluralInfo2 = new EnumMap<>(PluralType.class);
2915     {
2916         localeToPluralInfo2.put(PluralType.cardinal, new LinkedHashMap<String, PluralInfo>());
2917         localeToPluralInfo2.put(PluralType.ordinal, new LinkedHashMap<String, PluralInfo>());
2918     }
2919     private Map<String, PluralRanges> localeToPluralRanges = new LinkedHashMap<String, PluralRanges>();
2920 
2921     private Map<DayPeriodInfo.Type, Map<String, DayPeriodInfo>> typeToLocaleToDayPeriodInfo = new EnumMap<DayPeriodInfo.Type, Map<String, DayPeriodInfo>>(
2922         DayPeriodInfo.Type.class);
2923     private Map<String, CoverageLevel2> localeToCoverageLevelInfo = new ConcurrentHashMap<String, CoverageLevel2>();
2924     private CoverageCache coverageCache = new CoverageCache();
2925     private transient String lastPluralLocales = "";
2926     private transient PluralType lastPluralWasOrdinal = null;
2927     private transient Map<Count, String> lastPluralMap = new EnumMap<Count, String>(Count.class);
2928     private transient String lastDayPeriodLocales = null;
2929     private transient DayPeriodInfo.Type lastDayPeriodType = null;
2930     private transient DayPeriodInfo.Builder dayPeriodBuilder = new DayPeriodInfo.Builder();
2931 
2932     private void addDayPeriodPath(XPathParts path, String value) {
2933         // ldml/dates/calendars/calendar[@type="gregorian"]/dayPeriods/dayPeriodContext[@type="format"]/dayPeriodWidth[@type="wide"]/dayPeriod[@type="am"]
2934         /*
2935          * <supplementalData>
2936          * <version number="$Revision: 13987 $"/>
2937          * <generation date="$D..e... $"/>
2938          * <dayPeriodRuleSet>
2939          * <dayPeriodRules locales = "en"> <!-- default for any locales not listed under other dayPeriods -->
2940          * <dayPeriodRule type = "am" from = "0:00" before="12:00"/>
2941          * <dayPeriodRule type = "pm" from = "12:00" to="24:00"/>
2942          */
2943         String typeString = path.getAttributeValue(1, "type");
2944         String locales = path.getAttributeValue(2, "locales").trim();
2945         DayPeriodInfo.Type type = typeString == null
2946             ? DayPeriodInfo.Type.format
2947             : DayPeriodInfo.Type.valueOf(typeString.trim());
2948         if (!locales.equals(lastDayPeriodLocales) || type != lastDayPeriodType) {
2949             if (lastDayPeriodLocales != null) {
2950                 addDayPeriodInfo();
2951             }
2952             lastDayPeriodLocales = locales;
2953             lastDayPeriodType = type;
2954             // System.out.println(type + ", " + locales + ", " + path);
2955         }
2956         if (path.size() != 4) {
2957             if (locales.equals("root")) return; // we allow root to be empty
2958             throw new IllegalArgumentException(locales + " must have dayPeriodRule elements");
2959         }
2960         DayPeriod dayPeriod;
2961         try {
2962             dayPeriod = DayPeriod.fromString(path.getAttributeValue(-1, "type"));
2963         } catch (Exception e) {
2964             System.err.println(e.getMessage());
2965             return;
2966         }
2967         String at = path.getAttributeValue(-1, "at");
2968         String from = path.getAttributeValue(-1, "from");
2969         String after = path.getAttributeValue(-1, "after");
2970         String to = path.getAttributeValue(-1, "to");
2971         String before = path.getAttributeValue(-1, "before");
2972         if (at != null) {
2973             if (from != null || after != null || to != null || before != null) {
2974                 throw new IllegalArgumentException();
2975             }
2976             from = at;
2977             to = at;
2978         } else if ((from == null) == (after == null) || (to == null) == (before == null)) {
2979             throw new IllegalArgumentException();
2980         }
2981 //        if (dayPeriodBuilder.contains(dayPeriod)) { // disallow multiple rules with same dayperiod
2982 //            throw new IllegalArgumentException("Multiple rules with same dayperiod are disallowed: "
2983 //                + lastDayPeriodLocales + ", " + lastDayPeriodType + ", " + dayPeriod);
2984 //        }
2985         boolean includesStart = from != null;
2986         boolean includesEnd = to != null;
2987         int start = parseTime(includesStart ? from : after);
2988         int end = parseTime(includesEnd ? to : before);
2989         // Check if any periods contain 0, e.g. 1700 - 300
2990         if (start > end) {
2991             // System.out.println("start " + start + " end " + end);
2992             dayPeriodBuilder.add(dayPeriod, start, includesStart, parseTime("24:00"), includesEnd);
2993             dayPeriodBuilder.add(dayPeriod, parseTime("0:00"), includesStart, end, includesEnd);
2994         } else {
2995             dayPeriodBuilder.add(dayPeriod, start, includesStart, end, includesEnd);
2996         }
2997     }
2998 
2999     static Pattern PARSE_TIME = PatternCache.get("(\\d\\d?):(\\d\\d)");
3000 
3001     private int parseTime(String string) {
3002         Matcher matcher = PARSE_TIME.matcher(string);
3003         if (!matcher.matches()) {
3004             throw new IllegalArgumentException();
3005         }
3006         return (Integer.parseInt(matcher.group(1)) * 60 + Integer.parseInt(matcher.group(2))) * 60 * 1000;
3007     }
3008 
3009     private void addDayPeriodInfo() {
3010         String[] locales = lastDayPeriodLocales.split("\\s+");
3011         DayPeriodInfo temp = dayPeriodBuilder.finish(locales);
3012         Map<String, DayPeriodInfo> locale2DPI = typeToLocaleToDayPeriodInfo.get(lastDayPeriodType);
3013         if (locale2DPI == null) {
3014             typeToLocaleToDayPeriodInfo.put(lastDayPeriodType, locale2DPI = new LinkedHashMap<String, DayPeriodInfo>());
3015             //System.out.println(lastDayPeriodType + ", " + locale2DPI);
3016         }
3017         for (String locale : locales) {
3018             locale2DPI.put(locale, temp);
3019         }
3020     }
3021 
3022     static String lastPluralRangesLocales = null;
3023     static PluralRanges lastPluralRanges = null;
3024 
3025     private boolean addPluralPath(XPathParts path, String value) {
3026         /*
3027          * Adding
3028          <pluralRanges locales="am">
3029           <pluralRange start="one" end="one" result="one" />
3030          </pluralRanges>
3031          */
3032         String locales = path.getAttributeValue(2, "locales").trim();
3033         String element = path.getElement(2);
3034         if ("pluralRanges".equals(element)) {
3035             if (!locales.equals(lastPluralRangesLocales)) {
3036                 addPluralRanges(locales);
3037             }
3038             if (path.size() == 3) {
3039                 // ok for ranges to be empty
3040                 return true;
3041             }
3042             String rangeStart = path.getAttributeValue(-1, "start");
3043             String rangeEnd = path.getAttributeValue(-1, "end");
3044             String result = path.getAttributeValue(-1, "result");
3045             lastPluralRanges.add(rangeStart == null ? null : Count.valueOf(rangeStart),
3046                 rangeEnd == null ? null : Count.valueOf(rangeEnd),
3047                 Count.valueOf(result));
3048             return true;
3049         } else if ("pluralRules".equals(element)) {
3050 
3051             String type = path.getAttributeValue(1, "type");
3052             PluralType pluralType = type == null ? PluralType.cardinal : PluralType.valueOf(type);
3053             if (!lastPluralLocales.equals(locales)) {
3054                 addPluralInfo(pluralType);
3055                 lastPluralLocales = locales;
3056             }
3057             final String countString = path.getAttributeValue(-1, "count");
3058             if (countString == null) {
3059                 return false;
3060             }
3061             Count count = Count.valueOf(countString);
3062             if (lastPluralMap.containsKey(count)) {
3063                 throw new IllegalArgumentException("Duplicate plural count: " + count + " in " + locales);
3064             }
3065             lastPluralMap.put(count, value);
3066             lastPluralWasOrdinal = pluralType;
3067             return true;
3068         } else {
3069             return false;
3070         }
3071     }
3072 
3073     private void addPluralRanges(String localesString) {
3074         final String[] locales = localesString.split("\\s+");
3075         lastPluralRanges = new PluralRanges();
3076         for (String locale : locales) {
3077             if (localeToPluralRanges.containsKey(locale)) {
3078                 throw new IllegalArgumentException("Duplicate plural locale: " + locale);
3079             }
3080             localeToPluralRanges.put(locale, lastPluralRanges);
3081         }
3082         lastPluralRangesLocales = localesString;
3083     }
3084 
3085     private void addPluralInfo(PluralType pluralType) {
3086         final String[] locales = lastPluralLocales.split("\\s+");
3087         PluralInfo info = new PluralInfo(lastPluralMap, pluralType);
3088         Map<String, PluralInfo> localeToInfo = localeToPluralInfo2.get(pluralType);
3089         for (String locale : locales) {
3090             if (localeToInfo.containsKey(locale)) {
3091                 throw new IllegalArgumentException("Duplicate plural locale: " + locale);
3092             } else if (!locale.isEmpty()) {
3093                 localeToInfo.put(locale, info);
3094             }
3095         }
3096         lastPluralMap.clear();
3097     }
3098 
3099     public static class SampleList {
3100         public static final SampleList EMPTY = new SampleList().freeze();
3101 
3102         private UnicodeSet uset = new UnicodeSet();
3103         private List<FixedDecimal> fractions = new ArrayList<FixedDecimal>(0);
3104 
3105         public String toString() {
3106             return toString(6, 3);
3107         }
3108 
3109         public String toString(int intLimit, int fractionLimit) {
3110             StringBuilder b = new StringBuilder();
3111             int intCount = 0;
3112             int fractionCount = 0;
3113             int limit = uset.getRangeCount();
3114             for (int i = 0; i < limit; ++i) {
3115                 if (intCount >= intLimit) {
3116                     b.append(", …");
3117                     break;
3118                 }
3119                 if (b.length() != 0) {
3120                     b.append(", ");
3121                 }
3122                 int start = uset.getRangeStart(i);
3123                 int end = uset.getRangeEnd(i);
3124                 if (start == end) {
3125                     b.append(start);
3126                     ++intCount;
3127                 } else if (start + 1 == end) {
3128                     b.append(start).append(", ").append(end);
3129                     intCount += 2;
3130                 } else {
3131                     b.append(start).append('-').append(end);
3132                     intCount += 2;
3133                 }
3134             }
3135             if (fractions.size() > 0) {
3136                 for (int i = 0; i < fractions.size(); ++i) {
3137                     if (fractionCount >= fractionLimit) {
3138                         break;
3139                     }
3140                     if (b.length() != 0) {
3141                         b.append(", ");
3142                     }
3143                     // Android patch (CLDR ticket #11014) begin.
3144                     FixedDecimal fraction = fractions.get(i);
3145                     String formatted = String.format(
3146                             Locale.ROOT,
3147                             "%." + fraction.getVisibleDecimalDigitCount() + "f",
3148                             fraction.getSource());
3149                     b.append(formatted);
3150                     // Android patch (CLDR ticket #11014) end.
3151                     ++fractionCount;
3152                 }
3153                 b.append(", …");
3154             }
3155             return b.toString();
3156         }
3157 
3158         public int getRangeCount() {
3159             return uset.getRangeCount();
3160         }
3161 
3162         public int getRangeStart(int index) {
3163             return uset.getRangeStart(index);
3164         }
3165 
3166         public int getRangeEnd(int index) {
3167             return uset.getRangeEnd(index);
3168         }
3169 
3170         public List<FixedDecimal> getFractions() {
3171             return fractions;
3172         }
3173 
3174         public int intSize() {
3175             return uset.size();
3176         }
3177 
3178         public SampleList remove(int i) {
3179             uset.remove(i);
3180             return this;
3181         }
3182 
3183         public SampleList add(int i) {
3184             uset.add(i);
3185             return this;
3186         }
3187 
3188         public SampleList freeze() {
3189             uset.freeze();
3190             if (fractions instanceof ArrayList) {
3191                 fractions = Collections.unmodifiableList(fractions);
3192             }
3193             return this;
3194         }
3195 
3196         public void add(FixedDecimal i) {
3197             fractions.add(i);
3198         }
3199 
3200         public int fractionSize() {
3201             return fractions.size();
3202         }
3203     }
3204 
3205     public static class CountSampleList {
3206         private final Map<Count, SampleList> countToIntegerSamples9999;
3207         private final Map<Count, SampleList[]> countToDigitToIntegerSamples9999;
3208 
3209         CountSampleList(PluralRules pluralRules, Set<Count> keywords, PluralType pluralType) {
3210             // Create the integer counts
3211             countToIntegerSamples9999 = new EnumMap<Count, SampleList>(Count.class);
3212             countToDigitToIntegerSamples9999 = new EnumMap<Count, SampleList[]>(Count.class);
3213             for (Count c : keywords) {
3214                 countToIntegerSamples9999.put(c, new SampleList());
3215                 SampleList[] row = new SampleList[5];
3216                 countToDigitToIntegerSamples9999.put(c, row);
3217                 for (int i = 1; i < 5; ++i) {
3218                     row[i] = new SampleList();
3219                 }
3220             }
3221             for (int ii = 0; ii < 10000; ++ii) {
3222                 int i = ii;
3223                 int digit;
3224                 if (i > 999) {
3225                     digit = 4;
3226                 } else if (i > 99) {
3227                     digit = 3;
3228                 } else if (i > 9) {
3229                     digit = 2;
3230                 } else {
3231                     digit = 1;
3232                 }
3233                 Count count = Count.valueOf(pluralRules.select(i));
3234                 addSimple(countToIntegerSamples9999, i, count);
3235                 addDigit(countToDigitToIntegerSamples9999, i, count, digit);
3236                 if (haveFractions(keywords, digit)) {
3237                     continue;
3238                 }
3239                 if (pluralType == PluralType.cardinal) {
3240                     for (int f = 0; f < 30; ++f) {
3241                         FixedDecimal ni = new FixedDecimal(i + f / 10.0d, f < 10 ? 1 : 2, f);
3242                         count = Count.valueOf(pluralRules.select(ni));
3243                         addSimple(countToIntegerSamples9999, ni, count);
3244                         addDigit(countToDigitToIntegerSamples9999, ni, count, digit);
3245                     }
3246                 }
3247             }
3248             // HACK for Breton
3249             addSimple(countToIntegerSamples9999, 1000000, Count.valueOf(pluralRules.select(1000000)));
3250 
3251             for (Count count : keywords) {
3252                 SampleList uset = countToIntegerSamples9999.get(count);
3253                 uset.freeze();
3254                 SampleList[] map = countToDigitToIntegerSamples9999.get(count);
3255                 for (int i = 1; i < map.length; ++i) {
3256                     map[i].freeze();
3257                 }
3258             }
3259         }
3260 
3261         private boolean haveFractions(Set<Count> keywords, int digit) {
3262             for (Count c : keywords) {
3263                 int size = countToDigitToIntegerSamples9999.get(c)[digit].fractionSize();
3264                 if (size < MAX_COLLECTED_FRACTION) {
3265                     return false;
3266                 }
3267             }
3268             return true;
3269         }
3270 
3271         static final int MAX_COLLECTED_FRACTION = 5;
3272 
3273         private boolean addDigit(Map<Count, SampleList[]> countToDigitToIntegerSamples9999, FixedDecimal i, Count count, int digit) {
3274             return addFraction(i, countToDigitToIntegerSamples9999.get(count)[digit]);
3275         }
3276 
3277         private boolean addFraction(FixedDecimal i, SampleList sampleList) {
3278             if (sampleList.fractionSize() < MAX_COLLECTED_FRACTION) {
3279                 sampleList.add(i);
3280                 return true;
3281             } else {
3282                 return false;
3283             }
3284         }
3285 
3286         private boolean addSimple(Map<Count, SampleList> countToIntegerSamples9999, FixedDecimal i, Count count) {
3287             return addFraction(i, countToIntegerSamples9999.get(count));
3288         }
3289 
3290         private void addDigit(Map<Count, SampleList[]> countToDigitToIntegerSamples9999, int i, Count count, int digit) {
3291             countToDigitToIntegerSamples9999.get(count)[digit].add(i);
3292         }
3293 
3294         private void addSimple(Map<Count, SampleList> countToIntegerSamples9999, int i, Count count) {
3295             countToIntegerSamples9999.get(count).add(i);
3296         }
3297 
3298         public SampleList get(Count type) {
3299             return countToIntegerSamples9999.get(type);
3300         }
3301 
3302         public SampleList get(Count c, int digit) {
3303             SampleList[] sampleLists = countToDigitToIntegerSamples9999.get(c);
3304             return sampleLists == null ? null : sampleLists[digit];
3305         }
3306     }
3307 
3308     /**
3309      * Immutable class with plural info for different locales
3310      *
3311      * @author markdavis
3312      */
3313     public static class PluralInfo implements Comparable<PluralInfo> {
3314         static final Set<Double> explicits = new HashSet<Double>();
3315         static {
3316             explicits.add(0.0d);
3317             explicits.add(1.0d);
3318         }
3319 
3320         public enum Count {
3321             zero, one, two, few, many, other;
3322             public static final int LENGTH = Count.values().length;
3323             public static final List<Count> VALUES = Collections.unmodifiableList(Arrays.asList(values()));
3324         }
3325 
3326         static final Pattern pluralPaths = PatternCache.get(".*pluralRule.*");
3327         static final int fractDecrement = 13;
3328         static final int fractStart = 20;
3329 
3330         private final Map<Count, Set<Double>> countToExampleSet;
3331         private final Map<Count, String> countToStringExample;
3332         private final Map<Integer, Count> exampleToCount;
3333         private final PluralRules pluralRules;
3334         private final String pluralRulesString;
3335         private final Set<String> canonicalKeywords;
3336         private final Set<Count> keywords;
3337         private final Set<Count> integerKeywords;
3338         private final Set<Count> decimalKeywords;
3339         private final CountSampleList countSampleList;
3340         private final Map<Count, String> countToRule;
3341 
3342         private PluralInfo(Map<Count, String> countToRule, PluralType pluralType) {
3343             EnumMap<Count, String> tempCountToRule = new EnumMap<Count, String>(Count.class);
3344             tempCountToRule.putAll(countToRule);
3345             this.countToRule = Collections.unmodifiableMap(tempCountToRule);
3346 
3347             // now build rules
3348             NumberFormat nf = NumberFormat.getNumberInstance(ULocale.ENGLISH);
3349             nf.setMaximumFractionDigits(2);
3350             StringBuilder pluralRuleBuilder = new StringBuilder();
3351             for (Count count : countToRule.keySet()) {
3352                 if (pluralRuleBuilder.length() != 0) {
3353                     pluralRuleBuilder.append(';');
3354                 }
3355                 pluralRuleBuilder.append(count).append(':').append(countToRule.get(count));
3356             }
3357             pluralRulesString = pluralRuleBuilder.toString();
3358             try {
3359                 pluralRules = PluralRules.parseDescription(pluralRulesString);
3360             } catch (ParseException e) {
3361                 throw new IllegalArgumentException("Can't create plurals from <" + pluralRulesString + ">", e);
3362             }
3363             EnumSet<Count> _keywords = EnumSet.noneOf(Count.class);
3364             EnumSet<Count> _integerKeywords = EnumSet.noneOf(Count.class);
3365             EnumSet<Count> _decimalKeywords = EnumSet.noneOf(Count.class);
3366             for (String s : pluralRules.getKeywords()) {
3367                 Count c = Count.valueOf(s);
3368                 _keywords.add(c);
3369                 if (pluralRules.getDecimalSamples(s, SampleType.DECIMAL) != null) {
3370                     _decimalKeywords.add(c);
3371                 } else {
3372                     int debug = 1;
3373                 }
3374                 if (pluralRules.getDecimalSamples(s, SampleType.INTEGER) != null) {
3375                     _integerKeywords.add(c);
3376                 } else {
3377                     int debug = 1;
3378                 }
3379             }
3380             keywords = Collections.unmodifiableSet(_keywords);
3381             decimalKeywords = Collections.unmodifiableSet(_decimalKeywords);
3382             integerKeywords = Collections.unmodifiableSet(_integerKeywords);
3383 
3384             countSampleList = new CountSampleList(pluralRules, keywords, pluralType);
3385 
3386             Map<Count, Set<Double>> countToExampleSetRaw = new TreeMap<Count, Set<Double>>();
3387             Map<Integer, Count> exampleToCountRaw = new TreeMap<Integer, Count>();
3388 
3389             Output<Map<Count, SampleList[]>> output = new Output();
3390 
3391             // double check
3392             // if (!targetKeywords.equals(typeToExamples2.keySet())) {
3393             // throw new IllegalArgumentException ("Problem in plurals " + targetKeywords + ", " + this);
3394             // }
3395             // now fix the longer examples
3396             String otherFractionalExamples = "";
3397             List<Double> otherFractions = new ArrayList<Double>(0);
3398 
3399             // add fractional samples
3400             Map<Count, String> countToStringExampleRaw = new TreeMap<Count, String>();
3401             for (Count type : keywords) {
3402                 SampleList uset = countSampleList.get(type);
3403                 countToStringExampleRaw.put(type, uset.toString(5, 5));
3404             }
3405             final String baseOtherExamples = countToStringExampleRaw.get(Count.other);
3406             String otherExamples = (baseOtherExamples == null ? "" : baseOtherExamples + "; ")
3407                 + otherFractionalExamples + "...";
3408             countToStringExampleRaw.put(Count.other, otherExamples);
3409 
3410             // Now do double examples (previously unused & not working).
3411             // Currently a bit of a hack, we should enhance SampleList to make this easier
3412             // and then use SampleList directly, see http://unicode.org/cldr/trac/ticket/9813
3413             for (Count type : countToStringExampleRaw.keySet()) {
3414                 Set<Double> doublesSet = new LinkedHashSet<Double>(0);
3415                 String examples = countToStringExampleRaw.get(type);
3416                 if (examples == null) {
3417                     examples = "";
3418                 }
3419                 String strippedExamples = examples.replaceAll("(, …)|(; ...)", "");
3420                 String[] exampleArray = strippedExamples.split("(, )|(-)");
3421                 for (String example : exampleArray) {
3422                     if (example == null || example.length() == 0) {
3423                         continue;
3424                     }
3425                     Double doubleValue = Double.valueOf(example);
3426                     doublesSet.add(doubleValue);
3427                 }
3428                 doublesSet = Collections.unmodifiableSet(doublesSet);
3429                 countToExampleSetRaw.put(type, doublesSet);
3430             }
3431 
3432             countToExampleSet = Collections.unmodifiableMap(countToExampleSetRaw);
3433             countToStringExample = Collections.unmodifiableMap(countToStringExampleRaw);
3434             exampleToCount = Collections.unmodifiableMap(exampleToCountRaw);
3435             Set<String> temp = new LinkedHashSet<String>();
3436             // String keyword = pluralRules.select(0.0d);
3437             // double value = pluralRules.getUniqueKeywordValue(keyword);
3438             // if (value == pluralRules.NO_UNIQUE_VALUE) {
3439             // temp.add("0");
3440             // }
3441             // keyword = pluralRules.select(1.0d);
3442             // value = pluralRules.getUniqueKeywordValue(keyword);
3443             // if (value == pluralRules.NO_UNIQUE_VALUE) {
3444             // temp.add("1");
3445             // }
3446             Set<String> keywords = pluralRules.getKeywords();
3447             for (Count count : Count.values()) {
3448                 String keyword = count.toString();
3449                 if (keywords.contains(keyword)) {
3450                     temp.add(keyword);
3451                 }
3452             }
3453             // if (false) {
3454             // change to this after rationalizing 0/1
3455             // temp.add("0");
3456             // temp.add("1");
3457             // for (Count count : Count.values()) {
3458             // temp.add(count.toString());
3459             // KeywordStatus status = org.unicode.cldr.util.PluralRulesUtil.getKeywordStatus(pluralRules,
3460             // count.toString(), 0, explicits, true);
3461             // if (status != KeywordStatus.SUPPRESSED && status != KeywordStatus.INVALID) {
3462             // temp.add(count.toString());
3463             // }
3464             // }
3465             // }
3466             canonicalKeywords = Collections.unmodifiableSet(temp);
3467         }
3468 
3469         public String toString() {
3470             return countToExampleSet + "; " + exampleToCount + "; " + pluralRules;
3471         }
3472 
3473         public Map<Count, Set<Double>> getCountToExamplesMap() {
3474             return countToExampleSet;
3475         }
3476 
3477         public Map<Count, String> getCountToStringExamplesMap() {
3478             return countToStringExample;
3479         }
3480 
3481         public Count getCount(double exampleCount) {
3482             return Count.valueOf(pluralRules.select(exampleCount));
3483         }
3484 
3485         public Count getCount(PluralRules.FixedDecimal exampleCount) {
3486             return Count.valueOf(pluralRules.select(exampleCount));
3487         }
3488 
3489         public PluralRules getPluralRules() {
3490             return pluralRules;
3491         }
3492 
3493         public String getRules() {
3494             return pluralRulesString;
3495         }
3496 
3497         public Count getDefault() {
3498             return null;
3499         }
3500 
3501         public Set<String> getCanonicalKeywords() {
3502             return canonicalKeywords;
3503         }
3504 
3505         public Set<Count> getCounts() {
3506             return keywords;
3507         }
3508 
3509         public Set<Count> getCounts(SampleType sampleType) {
3510             return sampleType == SampleType.DECIMAL ? decimalKeywords : integerKeywords;
3511         }
3512 
3513         /**
3514          * Return the integer samples from 0 to 9999. For simplicity and compactness, this is a UnicodeSet, but
3515          * the interpretation is simply as a list of integers. UnicodeSet.EMPTY is returned if there are none.
3516          * @param c
3517          * @return
3518          */
3519         public SampleList getSamples9999(Count c) {
3520             return countSampleList.get(c);
3521         }
3522 
3523         /**
3524          * Return the integer samples for the specified digit, eg 1 => 0..9. For simplicity and compactness, this is a UnicodeSet, but
3525          * the interpretation is simply as a list of integers.
3526          * @param c
3527          * @return
3528          */
3529         public SampleList getSamples9999(Count c, int digit) {
3530             return countSampleList.get(c, digit);
3531         }
3532 
3533         public boolean hasSamples(Count c, int digits) {
3534             SampleList samples = countSampleList.get(c, digits);
3535             return samples != null && (samples.fractionSize() > 0 || samples.intSize() > 0);
3536         }
3537 
3538         public String getRule(Count keyword) {
3539             return countToRule.get(keyword);
3540         }
3541 
3542         @Override
3543         public int compareTo(PluralInfo other) {
3544             int size1 = this.countToRule.size();
3545             int size2 = other.countToRule.size();
3546             int diff = size1 - size2;
3547             if (diff != 0) {
3548                 return diff;
3549             }
3550             Iterator<Count> it1 = countToRule.keySet().iterator();
3551             Iterator<Count> it2 = other.countToRule.keySet().iterator();
3552             while (it1.hasNext()) {
3553                 Count a1 = it1.next();
3554                 Count a2 = it2.next();
3555                 diff = a1.ordinal() - a2.ordinal();
3556                 if (diff != 0) {
3557                     return diff;
3558                 }
3559             }
3560             return pluralRules.compareTo(other.pluralRules);
3561         }
3562 
3563         enum MinMax {
3564             MIN, MAX
3565         }
3566 
3567         public static final FixedDecimal NEGATIVE_INFINITY = new FixedDecimal(Double.NEGATIVE_INFINITY, 0, 0);
3568         public static final FixedDecimal POSITIVE_INFINITY = new FixedDecimal(Double.POSITIVE_INFINITY, 0, 0);
3569 
3570         static double doubleValue(FixedDecimal a) {
3571             return a.doubleValue();
3572         }
3573 
3574         public boolean rangeExists(Count s, Count e, Output<FixedDecimal> minSample, Output<FixedDecimal> maxSample) {
3575             if (!getCounts().contains(s) || !getCounts().contains(e)) {
3576                 return false;
3577             }
3578             FixedDecimal temp;
3579             minSample.value = getLeastIn(s, SampleType.INTEGER, NEGATIVE_INFINITY, POSITIVE_INFINITY);
3580             temp = getLeastIn(s, SampleType.DECIMAL, NEGATIVE_INFINITY, POSITIVE_INFINITY);
3581             if (lessOrFewerDecimals(temp, minSample.value)) {
3582                 minSample.value = temp;
3583             }
3584             maxSample.value = getGreatestIn(e, SampleType.INTEGER, NEGATIVE_INFINITY, POSITIVE_INFINITY);
3585             temp = getGreatestIn(e, SampleType.DECIMAL, NEGATIVE_INFINITY, POSITIVE_INFINITY);
3586             if (greaterOrFewerDecimals(temp, maxSample.value)) {
3587                 maxSample.value = temp;
3588             }
3589             // if there is no range, just return
3590             if (doubleValue(minSample.value) >= doubleValue(maxSample.value)) {
3591                 return false;
3592             }
3593             // see if we can get a better range, with not such a large end range
3594 
3595             FixedDecimal lowestMax = new FixedDecimal(doubleValue(minSample.value) + 0.00001, 5);
3596             SampleType bestType = getCounts(SampleType.INTEGER).contains(e) ? SampleType.INTEGER : SampleType.DECIMAL;
3597             temp = getLeastIn(e, bestType, lowestMax, POSITIVE_INFINITY);
3598             if (lessOrFewerDecimals(temp, maxSample.value)) {
3599                 maxSample.value = temp;
3600             }
3601             if (maxSample.value.getSource() > 100000) {
3602                 temp = getLeastIn(e, bestType, lowestMax, POSITIVE_INFINITY);
3603                 if (lessOrFewerDecimals(temp, maxSample.value)) {
3604                     maxSample.value = temp;
3605                 }
3606             }
3607 
3608             return true;
3609         }
3610 
3611         public boolean greaterOrFewerDecimals(FixedDecimal a, FixedDecimal b) {
3612             return doubleValue(a) > doubleValue(b)
3613                 || doubleValue(b) == doubleValue(a) && b.getDecimalDigits() > a.getDecimalDigits();
3614         }
3615 
3616         public boolean lessOrFewerDecimals(FixedDecimal a, FixedDecimal b) {
3617             return doubleValue(a) < doubleValue(b)
3618                 || doubleValue(b) == doubleValue(a) && b.getDecimalDigits() > a.getDecimalDigits();
3619         }
3620 
3621         private FixedDecimal getLeastIn(Count s, SampleType sampleType, FixedDecimal min, FixedDecimal max) {
3622             FixedDecimal result = POSITIVE_INFINITY;
3623             FixedDecimalSamples sSamples1 = pluralRules.getDecimalSamples(s.toString(), sampleType);
3624             if (sSamples1 != null) {
3625                 for (FixedDecimalRange x : sSamples1.samples) {
3626                     // overlap in ranges??
3627                     if (doubleValue(x.start) > doubleValue(max)
3628                         || doubleValue(x.end) < doubleValue(min)) {
3629                         continue; // no, continue
3630                     }
3631                     // get restricted range
3632                     FixedDecimal minOverlap = greaterOrFewerDecimals(min, x.start) ? max : x.start;
3633                     //FixedDecimal maxOverlap = lessOrFewerDecimals(max, x.end) ? max : x.end;
3634 
3635                     // replace if better
3636                     if (lessOrFewerDecimals(minOverlap, result)) {
3637                         result = minOverlap;
3638                     }
3639                 }
3640             }
3641             return result;
3642         }
3643 
3644         private FixedDecimal getGreatestIn(Count s, SampleType sampleType, FixedDecimal min, FixedDecimal max) {
3645             FixedDecimal result = NEGATIVE_INFINITY;
3646             FixedDecimalSamples sSamples1 = pluralRules.getDecimalSamples(s.toString(), sampleType);
3647             if (sSamples1 != null) {
3648                 for (FixedDecimalRange x : sSamples1.samples) {
3649                     // overlap in ranges??
3650                     if (doubleValue(x.start) > doubleValue(max)
3651                         || doubleValue(x.end) < doubleValue(min)) {
3652                         continue; // no, continue
3653                     }
3654                     // get restricted range
3655                     //FixedDecimal minOverlap = greaterOrFewerDecimals(min, x.start) ? max : x.start;
3656                     FixedDecimal maxOverlap = lessOrFewerDecimals(max, x.end) ? max : x.end;
3657 
3658                     // replace if better
3659                     if (greaterOrFewerDecimals(maxOverlap, result)) {
3660                         result = maxOverlap;
3661                     }
3662                 }
3663             }
3664             return result;
3665         }
3666 
3667         public static FixedDecimal getNonZeroSampleIfPossible(FixedDecimalSamples exampleList) {
3668             Set<FixedDecimalRange> sampleSet = exampleList.getSamples();
3669             FixedDecimal sampleDecimal = null;
3670             // skip 0 if possible
3671             for (FixedDecimalRange range : sampleSet) {
3672                 sampleDecimal = range.start;
3673                 if (sampleDecimal.getSource() != 0.0) {
3674                     break;
3675                 }
3676                 sampleDecimal = range.end;
3677                 if (sampleDecimal.getSource() != 0.0) {
3678                     break;
3679                 }
3680             }
3681             return sampleDecimal;
3682         }
3683     }
3684 
3685     /**
3686      * @deprecated use {@link #getPlurals(PluralType)} instead
3687      */
3688     public Set<String> getPluralLocales() {
3689         return getPluralLocales(PluralType.cardinal);
3690     }
3691 
3692     /**
3693      * @param type
3694      * @return the set of locales that have rules for the specified plural type
3695      */
3696     public Set<String> getPluralLocales(PluralType type) {
3697         return localeToPluralInfo2.get(type).keySet();
3698     }
3699 
3700     public Set<String> getPluralRangesLocales() {
3701         return localeToPluralRanges.keySet();
3702     }
3703 
3704     public PluralRanges getPluralRanges(String locale) {
3705         return localeToPluralRanges.get(locale);
3706     }
3707 
3708     /**
3709      * @deprecated use {@link #getPlurals(PluralType, String)} instead
3710      */
3711     public PluralInfo getPlurals(String locale) {
3712         return getPlurals(locale, true);
3713     }
3714 
3715     /**
3716      * Returns the plural info for a given locale.
3717      *
3718      * @param locale
3719      * @return
3720      */
3721     public PluralInfo getPlurals(PluralType type, String locale) {
3722         return getPlurals(type, locale, true);
3723     }
3724 
3725     /**
3726      * @deprecated use {@link #getPlurals(PluralType, String, boolean)} instead.
3727      */
3728     public PluralInfo getPlurals(String locale, boolean allowRoot) {
3729         return getPlurals(PluralType.cardinal, locale, allowRoot);
3730     }
3731 
3732     /**
3733      * Returns the plural info for a given locale.
3734      *
3735      * @param locale
3736      * @param allowRoot
3737      * @param type
3738      * @return
3739      */
3740     public PluralInfo getPlurals(PluralType type, String locale, boolean allowRoot) {
3741         Map<String, PluralInfo> infoMap = localeToPluralInfo2.get(type);
3742         while (locale != null) {
3743             if (!allowRoot && locale.equals("root")) {
3744                 break;
3745             }
3746             PluralInfo result = infoMap.get(locale);
3747             if (result != null) {
3748                 return result;
3749             }
3750             locale = LocaleIDParser.getSimpleParent(locale);
3751         }
3752         return null;
3753     }
3754 
3755     public DayPeriodInfo getDayPeriods(DayPeriodInfo.Type type, String locale) {
3756         Map<String, DayPeriodInfo> map1 = typeToLocaleToDayPeriodInfo.get(type);
3757         while (locale != null) {
3758             DayPeriodInfo result = map1.get(locale);
3759             if (result != null) {
3760                 return result;
3761             }
3762             locale = LocaleIDParser.getSimpleParent(locale);
3763         }
3764         return null;
3765     }
3766 
3767     public Set<String> getDayPeriodLocales(DayPeriodInfo.Type type) {
3768         return typeToLocaleToDayPeriodInfo.get(type).keySet();
3769     }
3770 
3771     private static CurrencyNumberInfo DEFAULT_NUMBER_INFO = new CurrencyNumberInfo(2, -1, -1, -1);
3772 
3773     public CurrencyNumberInfo getCurrencyNumberInfo(String currency) {
3774         CurrencyNumberInfo result = currencyToCurrencyNumberInfo.get(currency);
3775         if (result == null) {
3776             result = DEFAULT_NUMBER_INFO;
3777         }
3778         return result;
3779     }
3780 
3781     /**
3782      * Returns ordered set of currency data information
3783      *
3784      * @param territory
3785      * @return
3786      */
3787     public Set<CurrencyDateInfo> getCurrencyDateInfo(String territory) {
3788         return territoryToCurrencyDateInfo.getAll(territory);
3789     }
3790 
3791     /**
3792      * Returns ordered set of currency data information
3793      *
3794      * @param territory
3795      * @return
3796      */
3797     public Set<String> getCurrencyTerritories() {
3798         return territoryToCurrencyDateInfo.keySet();
3799     }
3800 
3801     /**
3802      * Returns the ISO4217 currency code of the default currency for a given
3803      * territory. The default currency is the first one listed which is legal
3804      * tender at the present moment.
3805      *
3806      * @param territory
3807      * @return
3808      */
3809     public String getDefaultCurrency(String territory) {
3810 
3811         Set<CurrencyDateInfo> targetCurrencyInfo = getCurrencyDateInfo(territory);
3812         String result = "XXX";
3813         Date now = new Date();
3814         for (CurrencyDateInfo cdi : targetCurrencyInfo) {
3815             if (cdi.getStart().before(now) && cdi.getEnd().after(now) && cdi.isLegalTender()) {
3816                 result = cdi.getCurrency();
3817                 break;
3818             }
3819         }
3820         return result;
3821     }
3822 
3823     /**
3824      * Returns the ISO4217 currency code of the default currency for a given
3825      * CLDRLocale. The default currency is the first one listed which is legal
3826      * tender at the present moment.
3827      *
3828      * @param territory
3829      * @return
3830      */
3831     public String getDefaultCurrency(CLDRLocale loc) {
3832         return getDefaultCurrency(loc.getCountry());
3833     }
3834 
3835     public Map<String, Set<TelephoneCodeInfo>> getTerritoryToTelephoneCodeInfo() {
3836         return territoryToTelephoneCodeInfo;
3837     }
3838 
3839     public Set<TelephoneCodeInfo> getTelephoneCodeInfoForTerritory(String territory) {
3840         return territoryToTelephoneCodeInfo.get(territory);
3841     }
3842 
3843     public Set<String> getTerritoriesForTelephoneCodeInfo() {
3844         return territoryToTelephoneCodeInfo.keySet();
3845     }
3846 
3847     private List<String> serialElements;
3848     private Collection<String> distinguishingAttributes;
3849 
3850 //    @Deprecated
3851 //    public List<String> getSerialElements() {
3852 //        return serialElements;
3853 //    }
3854 
3855 //    @Deprecated
3856 //    public Collection<String> getDistinguishingAttributes() {
3857 //        return distinguishingAttributes;
3858 //    }
3859 
3860     public List<R4<String, String, Integer, Boolean>> getLanguageMatcherData(String string) {
3861         return languageMatch.get(string);
3862     }
3863 
3864     public Set<String> getLanguageMatcherKeys() {
3865         return languageMatch.keySet();
3866     }
3867 
3868     /**
3869      * Return mapping from type to territory to data. 001 is the default.
3870      */
3871     public Map<MeasurementType, Map<String, String>> getTerritoryMeasurementData() {
3872         return measurementData;
3873     }
3874 
3875     /**
3876      * Return mapping from keys to subtypes
3877      */
3878     public Relation<String, String> getBcp47Keys() {
3879         return bcp47Key2Subtypes;
3880     }
3881 
3882     /**
3883      * Return mapping from extensions to keys
3884      */
3885     public Relation<String, String> getBcp47Extension2Keys() {
3886         return bcp47Extension2Keys;
3887     }
3888 
3889     /**
3890      * Return mapping from &lt;key,subtype> to aliases
3891      */
3892     public Relation<R2<String, String>, String> getBcp47Aliases() {
3893         return bcp47Aliases;
3894     }
3895 
3896     /**
3897      * Return mapping from &lt;key,subtype> to description
3898      */
3899     public Map<R2<String, String>, String> getBcp47Descriptions() {
3900         return bcp47Descriptions;
3901     }
3902 
3903     /**
3904      * Return mapping from &lt;key,subtype> to since
3905      */
3906     public Map<R2<String, String>, String> getBcp47Since() {
3907         return bcp47Since;
3908     }
3909 
3910     /**
3911      * Return mapping from &lt;key,subtype> to preferred
3912      */
3913     public Map<R2<String, String>, String> getBcp47Preferred() {
3914         return bcp47Preferred;
3915     }
3916 
3917     /**
3918      * Return mapping from &lt;key,subtype> to deprecated
3919      */
3920     public Map<R2<String, String>, String> getBcp47Deprecated() {
3921         return bcp47Deprecated;
3922     }
3923 
3924     static Set<String> MainTimeZones;;
3925 
3926     /**
3927      * Return canonical timezones
3928      *
3929      * @return
3930      */
3931     public Set<String> getCanonicalTimeZones() {
3932         synchronized (SupplementalDataInfo.class) {
3933             if (MainTimeZones == null) {
3934                 MainTimeZones = new TreeSet<String>();
3935                 SupplementalDataInfo info = SupplementalDataInfo.getInstance();
3936                 for (Entry<R2<String, String>, Set<String>> entry : info.getBcp47Aliases().keyValuesSet()) {
3937                     R2<String, String> subtype_aliases = entry.getKey();
3938                     if (!subtype_aliases.get0().equals("timezone")) {
3939                         continue;
3940                     }
3941                     MainTimeZones.add(entry.getValue().iterator().next());
3942                 }
3943                 MainTimeZones = Collections.unmodifiableSet(MainTimeZones);
3944             }
3945             return MainTimeZones;
3946         }
3947     }
3948 
3949     public Set<MetaZoneRange> getMetaZoneRanges(String zone) {
3950         return zoneToMetaZoneRanges.get(zone);
3951     }
3952 
3953     /**
3954      * Return the metazone containing this zone at this date
3955      *
3956      * @param zone
3957      * @param date
3958      * @return
3959      */
3960     public MetaZoneRange getMetaZoneRange(String zone, long date) {
3961         Set<MetaZoneRange> metazoneRanges = zoneToMetaZoneRanges.get(zone);
3962         if (metazoneRanges != null) {
3963             for (MetaZoneRange metazoneRange : metazoneRanges) {
3964                 if (metazoneRange.dateRange.getFrom() <= date && date < metazoneRange.dateRange.getTo()) {
3965                     return metazoneRange;
3966                 }
3967             }
3968         }
3969         return null;
3970     }
3971 
3972     public boolean isDeprecated(DtdType type, String element, String attribute, String value) {
3973         return DtdData.getInstance(type).isDeprecated(element, attribute, value);
3974     }
3975 
3976     public boolean isDeprecated(DtdType type, String path) {
3977 
3978         XPathParts parts = XPathParts.getInstance(path);
3979         for (int i = 0; i < parts.size(); ++i) {
3980             String element = parts.getElement(i);
3981             if (isDeprecated(type, element, "*", "*")) {
3982                 return true;
3983             }
3984             for (Entry<String, String> entry : parts.getAttributes(i).entrySet()) {
3985                 String attribute = entry.getKey();
3986                 String value = entry.getValue();
3987                 if (isDeprecated(type, element, attribute, value)) {
3988                     return true;
3989                 }
3990             }
3991         }
3992         return false;
3993     }
3994 
3995     /**
3996      * Returns map of ID/Type/Value, such as id="$integer" type="regex" value=[0-9]+
3997      * @return
3998      */
3999     public Map<String, R2<String, String>> getValidityInfo() {
4000         return validityInfo;
4001     }
4002 
4003     public Set<String> getCLDRLanguageCodes() {
4004         return CLDRLanguageCodes;
4005     }
4006 
4007     public boolean isCLDRLanguageCode(String code) {
4008         return CLDRLanguageCodes.contains(code);
4009     }
4010 
4011     public Set<String> getCLDRScriptCodes() {
4012         return CLDRScriptCodes;
4013     }
4014 
4015     public boolean isCLDRScriptCode(String code) {
4016         return CLDRScriptCodes.contains(code);
4017     }
4018 
4019     private synchronized void initCLDRLocaleBasedData() throws InternalError {
4020         // This initialization depends on SDI being initialized.
4021         if (defaultContentToBase == null) {
4022             Map<CLDRLocale, CLDRLocale> p2c = new TreeMap<CLDRLocale, CLDRLocale>();
4023             Map<CLDRLocale, CLDRLocale> c2p = new TreeMap<CLDRLocale, CLDRLocale>();
4024             TreeSet<CLDRLocale> tmpAllLocales = new TreeSet<CLDRLocale>();
4025             // copied from SupplementalData.java - CLDRLocale based
4026             for (String l : defaultContentLocales) {
4027                 CLDRLocale child = CLDRLocale.getInstance(l);
4028                 tmpAllLocales.add(child);
4029             }
4030 
4031             for (CLDRLocale child : tmpAllLocales) {
4032                 // Find a parent of this locale which is NOT itself also a defaultContent
4033                 CLDRLocale nextParent = child.getParent();
4034                 // /System.err.println(">> considering " + child + " with parent " + nextParent);
4035                 while (nextParent != null) {
4036                     if (!tmpAllLocales.contains(nextParent)) { // Did we find a parent that's also not itself a
4037                         // defaultContent?
4038                         // /System.err.println(">>>> Got 1? considering " + child + " with parent " + nextParent);
4039                         break;
4040                     }
4041                     // /System.err.println(">>>>> considering " + child + " with parent " + nextParent);
4042                     nextParent = nextParent.getParent();
4043                 }
4044                 // parent
4045                 if (nextParent == null) {
4046                     throw new InternalError("SupplementalDataInfo.defaultContentToChild(): No valid parent for "
4047                         + child);
4048                 } else if (nextParent == CLDRLocale.ROOT || nextParent == CLDRLocale.getInstance("root")) {
4049                     throw new InternalError(
4050                         "SupplementalDataInfo.defaultContentToChild(): Parent is root for default content locale "
4051                             + child);
4052                 } else {
4053                     c2p.put(child, nextParent); // wo_Arab_SN -> wo
4054                     CLDRLocale oldChild = p2c.get(nextParent);
4055                     if (oldChild != null) {
4056                         CLDRLocale childParent = child.getParent();
4057                         if (!childParent.equals(oldChild)) {
4058                             throw new InternalError(
4059                                 "SupplementalData.defaultContentToChild(): defaultContent list in wrong order? Tried to map "
4060                                     + nextParent + " -> " + child + ", replacing " + oldChild + " (should have been "
4061                                     + childParent + ")");
4062                         }
4063                     }
4064                     p2c.put(nextParent, child); // wo -> wo_Arab_SN
4065                 }
4066             }
4067 
4068             // done, save the hashtables..
4069             baseToDefaultContent = Collections.unmodifiableMap(p2c); // wo -> wo_Arab_SN
4070             defaultContentToBase = Collections.unmodifiableMap(c2p); // wo_Arab_SN -> wo
4071         }
4072     }
4073 
4074     public Map<String, PreferredAndAllowedHour> getTimeData() {
4075         return timeData;
4076     }
4077 
4078     public String getDefaultScript(String baseLanguage) {
4079         String ls = likelySubtags.get(baseLanguage);
4080         if (ls == null) {
4081             return UNKNOWN_SCRIPT;
4082         }
4083         LocaleIDParser lp = new LocaleIDParser().set(ls);
4084         String defaultScript = lp.getScript();
4085         if (defaultScript.length() > 0) {
4086             return defaultScript;
4087         } else {
4088             return UNKNOWN_SCRIPT;
4089         }
4090     }
4091 
4092     private XEquivalenceClass<String, String> equivalentLocales = null;
4093 
4094     public Set<String> getEquivalentsForLocale(String localeId) {
4095         if (equivalentLocales == null) {
4096             equivalentLocales = getEquivalentsForLocale();
4097         }
4098         Set<String> result = new TreeSet(LENGTH_FIRST);
4099         result.add(localeId);
4100         Set<String> equiv = equivalentLocales.getEquivalences(localeId);
4101         //        if (equiv == null) {
4102         //            result.add(localeId);
4103         //            return result;
4104         //        }
4105         if (equiv != null) {
4106             result.addAll(equivalentLocales.getEquivalences(localeId));
4107         }
4108         Map<String, String> likely = getLikelySubtags();
4109         String newMax = LikelySubtags.maximize(localeId, likely);
4110         if (newMax != null) {
4111             result.add(newMax);
4112             newMax = LikelySubtags.minimize(localeId, likely, true);
4113             if (newMax != null) {
4114                 result.add(newMax);
4115             }
4116             newMax = LikelySubtags.minimize(localeId, likely, false);
4117             if (newMax != null) {
4118                 result.add(newMax);
4119             }
4120         }
4121 
4122         //        if (result.size() == 1) {
4123         //            LanguageTagParser ltp = new LanguageTagParser().set(localeId);
4124         //            if (ltp.getScript().isEmpty()) {
4125         //                String ds = getDefaultScript(ltp.getLanguage());
4126         //                if (ds != null) {
4127         //                    ltp.setScript(ds);
4128         //                    result.add(ltp.toString());
4129         //                }
4130         //            }
4131         //        }
4132         return result;
4133     }
4134 
4135     public final static class LengthFirstComparator<T> implements Comparator<T> {
4136         public int compare(T a, T b) {
4137             String as = a.toString();
4138             String bs = b.toString();
4139             if (as.length() < bs.length())
4140                 return -1;
4141             if (as.length() > bs.length())
4142                 return 1;
4143             return as.compareTo(bs);
4144         }
4145     }
4146 
4147     public static final LengthFirstComparator LENGTH_FIRST = new LengthFirstComparator();
4148 
4149     private synchronized XEquivalenceClass<String, String> getEquivalentsForLocale() {
4150         SupplementalDataInfo sdi = this;
4151         Relation<String, String> localeToDefaultContents = Relation.of(new HashMap<String, Set<String>>(),
4152             LinkedHashSet.class);
4153 
4154         Set<String> dcl = sdi.getDefaultContentLocales();
4155         Map<String, String> likely = sdi.getLikelySubtags();
4156         XEquivalenceClass<String, String> locales = new XEquivalenceClass<String, String>();
4157         LanguageTagParser ltp = new LanguageTagParser();
4158         Set<String> temp = new HashSet<String>();
4159         for (Entry<String, String> entry : likely.entrySet()) {
4160             String source = entry.getKey();
4161             if (source.startsWith("und")) {
4162                 continue;
4163             }
4164             for (String s : getCombinations(source, ltp, likely, temp)) {
4165                 locales.add(source, s);
4166             }
4167             for (String s : getCombinations(entry.getValue(), ltp, likely, temp)) {
4168                 locales.add(source, s);
4169             }
4170         }
4171         //        Set<String> sorted = new TreeSet(locales.getExplicitItems());
4172         //        for (String s : sorted) {
4173         //            System.out.println(locales.getEquivalences(s));
4174         //        }
4175         for (String defaultContentLocale : dcl) {
4176             if (defaultContentLocale.startsWith("zh")) {
4177                 int x = 0;
4178             }
4179             Set<String> set = locales.getEquivalences(defaultContentLocale);
4180 
4181             String parent = LocaleIDParser.getSimpleParent(defaultContentLocale);
4182             if (!set.contains(parent)) {
4183                 localeToDefaultContents.put(parent, defaultContentLocale);
4184                 //System.out.println("Mismatch " + parent + ", " + set);
4185             }
4186             if (parent.contains("_")) {
4187                 continue;
4188             }
4189             // only base locales after this point
4190             String ds = sdi.getDefaultScript(parent);
4191             if (ds != null) {
4192                 ltp.set(parent);
4193                 ltp.setScript(ds);
4194                 String trial = ltp.toString();
4195                 if (!set.contains(trial)) {
4196                     //System.out.println("Mismatch " + trial + ", " + set);
4197                     localeToDefaultContents.put(parent, trial);
4198                 }
4199             }
4200         }
4201         return locales;
4202     }
4203 
4204     private Set<String> getCombinations(String source, LanguageTagParser ltp, Map<String, String> likely,
4205         Set<String> locales) {
4206         locales.clear();
4207 
4208         String max = LikelySubtags.maximize(source, likely);
4209         locales.add(max);
4210 
4211         ltp.set(source);
4212         ltp.setScript("");
4213         String trial = ltp.toString();
4214         String newMax = LikelySubtags.maximize(trial, likely);
4215         if (Objects.equals(newMax, max)) {
4216             locales.add(trial);
4217         }
4218 
4219         ltp.set(source);
4220         ltp.setRegion("");
4221         trial = ltp.toString();
4222         newMax = LikelySubtags.maximize(trial, likely);
4223         if (Objects.equals(newMax, max)) {
4224             locales.add(trial);
4225         }
4226 
4227         return locales;
4228     }
4229 
4230     public VersionInfo getCldrVersion() {
4231         return cldrVersion;
4232     }
4233 
4234     public File getDirectory() {
4235         return directory;
4236     }
4237 
4238     public final static Splitter WHITESPACE_SPLTTER = Splitter.on(PatternCache.get("\\s+")).omitEmptyStrings();
4239 
4240     public static final class AttributeValidityInfo {
4241         //<attributeValues elements="alias" attributes="path" type="path">notDoneYet</attributeValues>
4242 
4243         final String type;
4244         final Set<DtdType> dtds;
4245         final Set<String> elements;
4246         final Set<String> attributes;
4247         final String order;
4248 
4249         @Override
4250         public String toString() {
4251             return "type:" + type
4252                 + ", elements:" + elements
4253                 + ", attributes:" + attributes
4254                 + ", order:" + order;
4255         }
4256 
4257         static void add(Map<String, String> inputAttibutes, String inputValue, Map<AttributeValidityInfo, String> data) {
4258             final AttributeValidityInfo key = new AttributeValidityInfo(
4259                 inputAttibutes.get("dtds"),
4260                 inputAttibutes.get("type"),
4261                 inputAttibutes.get("attributes"),
4262                 inputAttibutes.get("elements"),
4263                 inputAttibutes.get("order"));
4264             if (data.containsKey(key)) {
4265                 throw new IllegalArgumentException(key + " declared twice");
4266             }
4267             data.put(key, inputValue);
4268         }
4269 
4270         public AttributeValidityInfo(String dtds, String type, String attributes, String elements, String order) {
4271             if (dtds == null) {
4272                 this.dtds = Collections.singleton(DtdType.ldml);
4273             } else {
4274                 Set<DtdType> temp = EnumSet.noneOf(DtdType.class);
4275                 for (String s : WHITESPACE_SPLTTER.split(dtds)) {
4276                     temp.add(DtdType.valueOf(s));
4277                 }
4278                 this.dtds = Collections.unmodifiableSet(temp);
4279             }
4280             this.type = type != null ? type : order != null ? "choice" : null;
4281             this.elements = elements == null ? Collections.EMPTY_SET
4282                 : With.in(WHITESPACE_SPLTTER.split(elements)).toUnmodifiableCollection(new HashSet<String>());
4283             this.attributes = With.in(WHITESPACE_SPLTTER.split(attributes)).toUnmodifiableCollection(new HashSet<String>());
4284             this.order = order;
4285         }
4286 
4287         public String getType() {
4288             return type;
4289         }
4290 
4291         public Set<DtdType> getDtds() {
4292             return dtds;
4293         }
4294 
4295         public Set<String> getElements() {
4296             return elements;
4297         }
4298 
4299         public Set<String> getAttributes() {
4300             return attributes;
4301         }
4302 
4303         public String getOrder() {
4304             return order;
4305         }
4306 
4307         @Override
4308         public boolean equals(Object obj) {
4309             AttributeValidityInfo other = (AttributeValidityInfo) obj;
4310             return CldrUtility.deepEquals(
4311                 type, other.type,
4312                 dtds, other.dtds,
4313                 elements, other.elements,
4314                 attributes, other.attributes,
4315                 order, other.order);
4316         }
4317 
4318         @Override
4319         public int hashCode() {
4320             return Objects.hash(type, dtds, elements, attributes, order);
4321         }
4322     }
4323 
4324     public Map<AttributeValidityInfo, String> getAttributeValidity() {
4325         return attributeValidityInfo;
4326     }
4327 
4328     public Multimap<String, String> getLanguageGroups() {
4329         return languageGroups;
4330     }
4331 }
4332