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