1 package org.unicode.cldr.util;
2 
3 import java.util.Arrays;
4 import java.util.Collections;
5 import java.util.EnumMap;
6 import java.util.HashMap;
7 import java.util.HashSet;
8 import java.util.Iterator;
9 import java.util.LinkedHashMap;
10 import java.util.LinkedHashSet;
11 import java.util.List;
12 import java.util.Locale;
13 import java.util.Map;
14 import java.util.Map.Entry;
15 import java.util.Set;
16 import java.util.TreeMap;
17 import java.util.TreeSet;
18 import java.util.regex.Matcher;
19 import java.util.regex.Pattern;
20 
21 import org.unicode.cldr.draft.ScriptMetadata;
22 import org.unicode.cldr.draft.ScriptMetadata.Info;
23 import org.unicode.cldr.tool.LikelySubtags;
24 import org.unicode.cldr.util.RegexLookup.Finder;
25 import org.unicode.cldr.util.With.SimpleIterator;
26 
27 import com.google.common.base.Splitter;
28 import com.ibm.icu.impl.Relation;
29 import com.ibm.icu.impl.Row;
30 import com.ibm.icu.lang.UCharacter;
31 import com.ibm.icu.text.Collator;
32 import com.ibm.icu.text.Transform;
33 import com.ibm.icu.util.ICUException;
34 import com.ibm.icu.util.Output;
35 import com.ibm.icu.util.ULocale;
36 
37 /**
38  * Provides a mechanism for dividing up LDML paths into understandable
39  * categories, eg for the Survey tool.
40  */
41 public class PathHeader implements Comparable<PathHeader> {
42     /**
43      * Link to a section. Commenting out the page switch for now.
44      */
45     public static final String SECTION_LINK = "<a " + /* "target='CLDR_ST-SECTION' "+*/"href='";
46     static boolean UNIFORM_CONTINENTS = true;
47     static Factory factorySingleton = null;
48 
49     static final boolean SKIP_ORIGINAL_PATH = true;
50 
51     /**
52      * What status the survey tool should use. Can be overridden in
53      * Phase.getAction()
54      */
55     public enum SurveyToolStatus {
56         /**
57          * Never show.
58          */
59         DEPRECATED,
60         /**
61          * Hide. Can be overridden in Phase.getAction()
62          */
63         HIDE,
64         /**
65          * Don't allow Change box (except TC), instead show ticket. But allow
66          * votes. Can be overridden in Phase.getAction()
67          */
68         READ_ONLY,
69         /**
70          * Allow change box and votes. Can be overridden in Phase.getAction()
71          */
72         READ_WRITE,
73         /**
74          * Changes are allowed as READ_WRITE, but field is always displayed as
75          * LTR, even in RTL locales (used for patterns).
76          */
77         LTR_ALWAYS
78     }
79 
80     private static EnumNames<SectionId> SectionIdNames = new EnumNames<>();
81 
82     /**
83      * The Section for a path. Don't change these without committee buy-in. The
84      * 'name' may be 'Core_Data' and the toString is 'Core Data' toString gives
85      * the human name
86      */
87     public enum SectionId {
88         Core_Data("Core Data"), Locale_Display_Names("Locale Display Names"), DateTime("Date & Time"), Timezones, Numbers, Currencies, Units, Characters, Misc(
89             "Miscellaneous"), BCP47, Supplemental, Special;
90 
SectionId(String... alternateNames)91         private SectionId(String... alternateNames) {
92             SectionIdNames.add(this, alternateNames);
93         }
94 
forString(String name)95         public static SectionId forString(String name) {
96             return SectionIdNames.forString(name);
97         }
98 
99         @Override
toString()100         public String toString() {
101             return SectionIdNames.toString(this);
102         }
103     }
104 
105     private static EnumNames<PageId> PageIdNames = new EnumNames<>();
106     private static Relation<SectionId, PageId> SectionIdToPageIds = Relation.of(new TreeMap<SectionId, Set<PageId>>(),
107         TreeSet.class);
108 
109     private static class SubstringOrder implements Comparable<SubstringOrder> {
110         final String mainOrder;
111         final int order;
112 
SubstringOrder(String source)113         public SubstringOrder(String source) {
114             int pos = source.lastIndexOf('-') + 1;
115             int ordering = COUNTS.indexOf(source.substring(pos));
116             // account for digits, and "some" future proofing.
117             order = ordering < 0
118                 ? source.charAt(pos)
119                     : 0x10000 + ordering;
120                 mainOrder = source.substring(0, pos);
121         }
122 
123         @Override
124         public String toString() {
125             return "{" + mainOrder + ", " + order + "}";
126         }
127 
128         @Override
129         public int compareTo(SubstringOrder other) {
130             int diff = alphabeticCompare(mainOrder, other.mainOrder);
131             if (diff != 0) {
132                 return diff;
133             }
134             return order - other.order;
135         }
136     }
137 
138     /**
139      * The Page for a path (within a Section). Don't change these without
140      * committee buy-in. the name is for example WAsia where toString gives
141      * Western Asia
142      */
143     public enum PageId {
144         Alphabetic_Information(SectionId.Core_Data, "Alphabetic Information"),
145         Numbering_Systems(SectionId.Core_Data, "Numbering Systems"),
146         LinguisticElements(SectionId.Core_Data, "Linguistic Elements"),
147 
148         Locale_Name_Patterns(SectionId.Locale_Display_Names, "Locale Name Patterns"),
149         Languages_A_D(SectionId.Locale_Display_Names, "Languages (A-D)"),
150         Languages_E_J(SectionId.Locale_Display_Names, "Languages (E-J)"),
151         Languages_K_N(SectionId.Locale_Display_Names, "Languages (K-N)"),
152         Languages_O_S(SectionId.Locale_Display_Names, "Languages (O-S)"),
153         Languages_T_Z(SectionId.Locale_Display_Names, "Languages (T-Z)"),
154         Scripts(SectionId.Locale_Display_Names),
155         Territories(SectionId.Locale_Display_Names, "Geographic Regions"),
156         T_NAmerica(SectionId.Locale_Display_Names, "Territories (North America)"),
157         T_SAmerica( SectionId.Locale_Display_Names, "Territories (South America)"),
158         T_Africa(SectionId.Locale_Display_Names, "Territories (Africa)"),
159         T_Europe( SectionId.Locale_Display_Names, "Territories (Europe)"),
160         T_Asia(SectionId.Locale_Display_Names, "Territories (Asia)"),
161         T_Oceania( SectionId.Locale_Display_Names, "Territories (Oceania)"),
162         Locale_Variants(SectionId.Locale_Display_Names, "Locale Variants"),
163         Keys( SectionId.Locale_Display_Names),
164 
165         Fields(SectionId.DateTime),
166         Gregorian(SectionId.DateTime),
167         Generic( SectionId.DateTime),
168         Buddhist(SectionId.DateTime),
169         Chinese(SectionId.DateTime),
170         Coptic( SectionId.DateTime),
171         Dangi(SectionId.DateTime),
172         Ethiopic(SectionId.DateTime),
173         Ethiopic_Amete_Alem( SectionId.DateTime, "Ethiopic-Amete-Alem"),
174         Hebrew(SectionId.DateTime),
175         Indian( SectionId.DateTime),
176         Islamic(SectionId.DateTime),
177         Japanese(SectionId.DateTime),
178         Persian( SectionId.DateTime),
179         Minguo(SectionId.DateTime),
180 
181         Timezone_Display_Patterns(SectionId.Timezones, "Timezone Display Patterns"),
182         NAmerica(SectionId.Timezones, "North America"),
183         SAmerica( SectionId.Timezones, "South America"),
184         Africa(SectionId.Timezones),
185         Europe( SectionId.Timezones),
186         Russia(SectionId.Timezones),
187         WAsia(SectionId.Timezones, "Western Asia"),
188         CAsia(SectionId.Timezones, "Central Asia"),
189         EAsia( SectionId.Timezones, "Eastern Asia"),
190         SAsia(SectionId.Timezones, "Southern Asia"),
191         SEAsia( SectionId.Timezones, "Southeast Asia"),
192         Australasia(SectionId.Timezones),
193         Antarctica( SectionId.Timezones),
194         Oceania(SectionId.Timezones),
195         UnknownT( SectionId.Timezones, "Unknown Region"),
196         Overrides(SectionId.Timezones),
197 
198         Symbols( SectionId.Numbers),
199         Number_Formatting_Patterns( SectionId.Numbers, "Number Formatting Patterns"),
200         Compact_Decimal_Formatting( SectionId.Numbers, "Compact Decimal Formatting"),
201         Compact_Decimal_Formatting_Other( SectionId.Numbers, "Compact Decimal Formatting (Other Numbering Systems)"),
202 
203         Measurement_Systems( SectionId.Units, "Measurement Systems"),
204         Duration( SectionId.Units),
205         Graphics( SectionId.Units),
206         Length( SectionId.Units),
207         Area( SectionId.Units),
208         Volume( SectionId.Units),
209         SpeedAcceleration( SectionId.Units, "Speed and Acceleration"),
210         MassWeight( SectionId.Units, "Mass and Weight"),
211         EnergyPower( SectionId.Units, "Energy and Power"),
212         ElectricalFrequency( SectionId.Units, "Electrical and Frequency"),
213         Weather( SectionId.Units),
214         Digital( SectionId.Units),
215         Coordinates( SectionId.Units),
216         OtherUnits( SectionId.Units, "Other Units"),
217         CompoundUnits( SectionId.Units, "Compound Units"),
218 
219 
220         Displaying_Lists( SectionId.Misc, "Displaying Lists"),
221         MinimalPairs(SectionId.Misc, "Minimal Pairs"),
222         Transforms( SectionId.Misc),
223 
224         Identity( SectionId.Special),
225         Version( SectionId.Special),
226         Suppress( SectionId.Special),
227         Deprecated( SectionId.Special),
228         Unknown( SectionId.Special),
229 
230         C_NAmerica( SectionId.Currencies, "North America (C)"),
231         //need to add (C) to differentiate from Timezone territories
232         C_SAmerica(SectionId.Currencies, "South America (C)"),
233         C_NWEurope(SectionId.Currencies, "Northern/Western Europe"),
234         C_SEEurope(SectionId.Currencies, "Southern/Eastern Europe"),
235         C_NAfrica(SectionId.Currencies, "Northern Africa"),
236         C_WAfrica(SectionId.Currencies, "Western Africa"),
237         C_MAfrica( SectionId.Currencies, "Middle Africa"),
238         C_EAfrica(SectionId.Currencies, "Eastern Africa"),
239         C_SAfrica(SectionId.Currencies, "Southern Africa"),
240         C_WAsia(SectionId.Currencies, "Western Asia (C)"),
241         C_CAsia(SectionId.Currencies, "Central Asia (C)"),
242         C_EAsia( SectionId.Currencies, "Eastern Asia (C)"),
243         C_SAsia(SectionId.Currencies, "Southern Asia (C)"),
244         C_SEAsia(SectionId.Currencies, "Southeast Asia (C)"),
245         C_Oceania(SectionId.Currencies, "Oceania (C)"),
246         C_Unknown(SectionId.Currencies, "Unknown Region (C)"),
247 
248         // BCP47
249         u_Extension(SectionId.BCP47),
250         t_Extension(SectionId.BCP47),
251 
252         // Supplemental
253         Alias(SectionId.Supplemental),
254         IdValidity(SectionId.Supplemental),
255         Locale(SectionId.Supplemental),
256         RegionMapping(SectionId.Supplemental),
257         WZoneMapping( SectionId.Supplemental),
258         Transform(SectionId.Supplemental),
259         Units(SectionId.Supplemental),
260         Likely(SectionId.Supplemental),
261         LanguageMatch( SectionId.Supplemental),
262         TerritoryInfo(SectionId.Supplemental),
263         LanguageInfo(SectionId.Supplemental),
264         LanguageGroup( SectionId.Supplemental),
265         Fallback(SectionId.Supplemental),
266         Gender(SectionId.Supplemental),
267         Grammar(SectionId.Supplemental),
268         Metazone(SectionId.Supplemental),
269         NumberSystem( SectionId.Supplemental),
270         Plural(SectionId.Supplemental),
271         PluralRange(SectionId.Supplemental),
272         Containment( SectionId.Supplemental),
273         Currency(SectionId.Supplemental),
274         Calendar(SectionId.Supplemental),
275         WeekData( SectionId.Supplemental),
276         Measurement(SectionId.Supplemental),
277         Language(SectionId.Supplemental),
278         RBNF( SectionId.Supplemental),
279         Segmentation(SectionId.Supplemental),
280         DayPeriod(SectionId.Supplemental),
281 
282         Category(SectionId.Characters),
283 
284         // [Smileys, People, Animals & Nature, Food & Drink, Travel & Places, Activities, Objects, Symbols, Flags]
285         Smileys(SectionId.Characters, "Smileys & Emotion"),
286         People(SectionId.Characters, "People & Body"),
287         Animals_Nature(SectionId.Characters, "Animals & Nature"),
288         Food_Drink(SectionId.Characters, "Food & Drink"),
289         Travel_Places(SectionId.Characters, "Travel & Places"),
290         Activities(SectionId.Characters),
291         Objects( SectionId.Characters),
292         Symbols2(SectionId.Characters),
293         Flags(SectionId.Characters),
294         Component(SectionId.Characters),
295         Typography(SectionId.Characters),
296         ;
297 
298         private final SectionId sectionId;
299 
300         private PageId(SectionId sectionId, String... alternateNames) {
301             this.sectionId = sectionId;
302             SectionIdToPageIds.put(sectionId, this);
303             PageIdNames.add(this, alternateNames);
304         }
305 
306         /**
307          * Construct a pageId given a string
308          *
309          * @param name
310          * @return
311          */
312         public static PageId forString(String name) {
313             try {
314                 return PageIdNames.forString(name);
315             } catch (Exception e) {
316                 throw new ICUException("No PageId for " + name, e);
317             }
318         }
319 
320         /**
321          * Returns the page id
322          *
323          * @return a page ID, such as 'Languages'
324          */
325         @Override
326         public String toString() {
327             return PageIdNames.toString(this);
328         }
329 
330         /**
331          * Get the containing section id, such as 'Code Lists'
332          *
333          * @return the containing section ID
334          */
335         public SectionId getSectionId() {
336             return sectionId;
337         }
338     }
339 
340     private final SectionId sectionId;
341     private final PageId pageId;
342     private final String header;
343     private final String code;
344     private final String originalPath;
345     private final SurveyToolStatus status;
346 
347     // Used for ordering
348     private final int headerOrder;
349     private final long codeOrder;
350     private final SubstringOrder codeSuborder;
351 
352     static final Pattern SEMI = PatternCache.get("\\s*;\\s*");
353     static final Matcher ALT_MATCHER = PatternCache.get(
354         "\\[@alt=\"([^\"]*+)\"]")
355         .matcher("");
356 
357     static final Collator alphabetic = CLDRConfig.getInstance().getCollatorRoot();
358 
359 //    static final RuleBasedCollator alphabetic = (RuleBasedCollator) Collator
360 //            .getInstance(ULocale.ENGLISH);
361 //    static {
362 //        alphabetic.setNumericCollation(true);
363 //        alphabetic.freeze();
364 //    }
365 
366     static final SupplementalDataInfo supplementalDataInfo = SupplementalDataInfo.getInstance();
367     static final Map<String, String> metazoneToContinent = supplementalDataInfo
368         .getMetazoneToContinentMap();
369     static final StandardCodes standardCode = StandardCodes.make();
370     static final Map<String, String> metazoneToPageTerritory = new HashMap<>();
371     static {
372         Map<String, Map<String, String>> metazoneToRegionToZone = supplementalDataInfo.getMetazoneToRegionToZone();
373         for (Entry<String, Map<String, String>> metazoneEntry : metazoneToRegionToZone.entrySet()) {
374             String metazone = metazoneEntry.getKey();
375             String worldZone = metazoneEntry.getValue().get("001");
376             String territory = Containment.getRegionFromZone(worldZone);
377             if (territory == null) {
378                 territory = "ZZ";
379             }
380             // Russia, Antarctica => territory
381             // in Australasia, Asia, S. America => subcontinent
382             // in N. America => N. America (grouping of 3 subcontinents)
383             // in everything else => continent
384             if (territory.equals("RU") || territory.equals("AQ")) {
385                 metazoneToPageTerritory.put(metazone, territory);
386             } else {
387                 String continent = Containment.getContinent(territory);
388                 String subcontinent = Containment.getSubcontinent(territory);
389                 if (continent.equals("142")) { // Asia
390                     metazoneToPageTerritory.put(metazone, subcontinent);
391                 } else if (continent.equals("019")) { // Americas
392                     metazoneToPageTerritory.put(metazone, subcontinent.equals("005") ? subcontinent : "003");
393                 } else if (subcontinent.equals("053")) { // Australasia
394                     metazoneToPageTerritory.put(metazone, subcontinent);
395                 } else {
396                     metazoneToPageTerritory.put(metazone, continent);
397                 }
398             }
399         }
400     }
401 
402     /**
403      * @param section
404      * @param sectionOrder
405      * @param page
406      * @param pageOrder
407      * @param header
408      * @param headerOrder
409      * @param code
410      * @param codeOrder
411      * @param suborder
412      * @param status
413      */
414     private PathHeader(SectionId sectionId, PageId pageId, String header,
415         int headerOrder, String code, long codeOrder, SubstringOrder suborder, SurveyToolStatus status,
416         String originalPath) {
417         this.sectionId = sectionId;
418         this.pageId = pageId;
419         this.header = header;
420         this.headerOrder = headerOrder;
421         this.code = code;
422         this.codeOrder = codeOrder;
423         this.codeSuborder = suborder;
424         this.originalPath = originalPath;
425         this.status = status;
426     }
427 
428     /**
429      * Return a factory for use in creating the headers. This is cached after first use.
430      * The calls are thread-safe. Null gets the default (CLDRConfig) english file.
431      *
432      * @param englishFile
433      */
434     public static Factory getFactory(CLDRFile englishFile) {
435         if (factorySingleton == null) {
436             if (englishFile == null) {
437                 englishFile = CLDRConfig.getInstance().getEnglish();
438             }
439             if (!englishFile.getLocaleID().equals(ULocale.ENGLISH.getBaseName())) {
440                 throw new IllegalArgumentException("PathHeader's CLDRFile must be '" +
441                     ULocale.ENGLISH.getBaseName() + "', but found '" + englishFile.getLocaleID() + "'");
442             }
443             factorySingleton = new Factory(englishFile);
444         }
445         return factorySingleton;
446     }
447 
448     /**
449      * Convenience method for common case. See {{@link #getFactory(CLDRFile)}}
450      */
451     public static Factory getFactory() {
452         return getFactory(null);
453     }
454 
455     /**
456      * @deprecated
457      */
458     @Deprecated
459     public String getSection() {
460         return sectionId.toString();
461     }
462 
463     public SectionId getSectionId() {
464         return sectionId;
465     }
466 
467     /**
468      * @deprecated
469      */
470     @Deprecated
471     public String getPage() {
472         return pageId.toString();
473     }
474 
475     public PageId getPageId() {
476         return pageId;
477     }
478 
479     public String getHeader() {
480         return header == null ? "" : header;
481     }
482 
483     public String getCode() {
484         return code;
485     }
486 
487     public String getHeaderCode() {
488         return getHeader() + ": " + getCode();
489     }
490 
491     public String getOriginalPath() {
492         return originalPath;
493     }
494 
495     public SurveyToolStatus getSurveyToolStatus() {
496         return status;
497     }
498 
499     @Override
500     public String toString() {
501         return sectionId
502             + "\t" + pageId
503             + "\t" + header // + "\t" + headerOrder
504             + "\t" + code // + "\t" + codeOrder
505             ;
506     }
507 
508     @Override
509     public int compareTo(PathHeader other) {
510         // Within each section, order alphabetically if the integer orders are
511         // not different.
512         try {
513             int result;
514             if (0 != (result = sectionId.compareTo(other.sectionId))) {
515                 return result;
516             }
517             if (0 != (result = pageId.compareTo(other.pageId))) {
518                 return result;
519             }
520             if (0 != (result = headerOrder - other.headerOrder)) {
521                 return result;
522             }
523             if (0 != (result = alphabeticCompare(header, other.header))) {
524                 return result;
525             }
526             long longResult;
527             if (0 != (longResult = codeOrder - other.codeOrder)) {
528                 return longResult < 0 ? -1 : longResult > 0 ? 1 : 0;
529             }
530             if (codeSuborder != null) { // do all three cases, for transitivity
531                 if (other.codeSuborder != null) {
532                     if (0 != (result = codeSuborder.compareTo(other.codeSuborder))) {
533                         return result;
534                     }
535                 } else {
536                     return 1; // if codeSuborder != null (and other.codeSuborder
537                     // == null), it is greater
538                 }
539             } else if (other.codeSuborder != null) {
540                 return -1; // if codeSuborder == null (and other.codeSuborder !=
541                 // null), it is greater
542             }
543             if (0 != (result = alphabeticCompare(code, other.code))) {
544                 return result;
545             }
546             if (!SKIP_ORIGINAL_PATH && 0 != (result = alphabeticCompare(originalPath, other.originalPath))) {
547                 return result;
548             }
549             return 0;
550         } catch (RuntimeException e) {
551             throw new IllegalArgumentException("Internal problem comparing " + this + " and " + other, e);
552         }
553     }
554 
555     public int compareHeader(PathHeader other) {
556         int result;
557         if (0 != (result = headerOrder - other.headerOrder)) {
558             return result;
559         }
560         if (0 != (result = alphabeticCompare(header, other.header))) {
561             return result;
562         }
563         return result;
564     }
565 
566     public int compareCode(PathHeader other) {
567         int result;
568         long longResult;
569         if (0 != (longResult = codeOrder - other.codeOrder)) {
570             return longResult < 0 ? -1 : longResult > 0 ? 1 : 0;
571         }
572         if (codeSuborder != null) { // do all three cases, for transitivity
573             if (other.codeSuborder != null) {
574                 if (0 != (result = codeSuborder.compareTo(other.codeSuborder))) {
575                     return result;
576                 }
577             } else {
578                 return 1; // if codeSuborder != null (and other.codeSuborder
579                 // == null), it is greater
580             }
581         } else if (other.codeSuborder != null) {
582             return -1; // if codeSuborder == null (and other.codeSuborder !=
583             // null), it is greater
584         }
585         if (0 != (result = alphabeticCompare(code, other.code))) {
586             return result;
587         }
588         return result;
589     }
590 
591     @Override
592     public boolean equals(Object obj) {
593         PathHeader other;
594         try {
595             other = (PathHeader) obj;
596         } catch (Exception e) {
597             return false;
598         }
599         return sectionId == other.sectionId && pageId == other.pageId
600             && header.equals(other.header) && code.equals(other.code);
601     }
602 
603     @Override
604     public int hashCode() {
605         return sectionId.hashCode() ^ pageId.hashCode() ^ header.hashCode() ^ code.hashCode();
606     }
607 
608     public static class Factory implements Transform<String, PathHeader> {
609         static final RegexLookup<RawData> lookup = RegexLookup
610             .of(new PathHeaderTransform())
611             .setPatternTransform(
612                 RegexLookup.RegexFinderTransformPath)
613             .loadFromFile(
614                 PathHeader.class,
615                 "data/PathHeader.txt");
616         // synchronized with lookup
617         static final Output<String[]> args = new Output<>();
618         // synchronized with lookup
619         static final Counter<RawData> counter = new Counter<>();
620         // synchronized with lookup
621         static final Map<RawData, String> samples = new HashMap<>();
622         // synchronized with lookup
623         static long order;
624         static SubstringOrder suborder;
625 
626         static final Map<String, PathHeader> cache = new HashMap<>();
627         // synchronized with cache
628         static final Map<SectionId, Map<PageId, SectionPage>> sectionToPageToSectionPage = new EnumMap<>(
629             SectionId.class);
630         static final Relation<SectionPage, String> sectionPageToPaths = Relation
631             .of(new TreeMap<SectionPage, Set<String>>(),
632                 HashSet.class);
633         private static CLDRFile englishFile;
634         private Set<String> matchersFound = new HashSet<>();
635 
636         /**
637          * Create a factory for creating PathHeaders.
638          *
639          * @param englishFile
640          *            - only sets the file (statically!) if not already set.
641          */
642         private Factory(CLDRFile englishFile) {
643             setEnglishCLDRFileIfNotSet(englishFile); // temporary
644         }
645 
646         /**
647          * Returns true if we set it, false if set before.
648          *
649          * @param englishFile2
650          * @return
651          */
652         private static boolean setEnglishCLDRFileIfNotSet(CLDRFile englishFile2) {
653             synchronized (Factory.class) {
654                 if (englishFile != null) {
655                     return false;
656                 }
657                 englishFile = englishFile2;
658                 return true;
659             }
660         }
661 
662         /**
663          * Use only when trying to find unmatched patterns
664          */
665         public void clearCache() {
666             synchronized (cache) {
667                 cache.clear();
668             }
669         }
670 
671         /**
672          * Return the PathHeader for a given path. Thread-safe.
673          */
674         public PathHeader fromPath(String path) {
675             return fromPath(path, null);
676         }
677 
678         /**
679          * Return the PathHeader for a given path. Thread-safe.
680          */
681         @Override
682         public PathHeader transform(String path) {
683             return fromPath(path, null);
684         }
685 
686         /**
687          * Return the PathHeader for a given path. Thread-safe.
688          * @param failures a list of failures to add to.
689          */
690         public PathHeader fromPath(final String path, List<String> failures) {
691             if (path == null) {
692                 throw new NullPointerException("Path cannot be null");
693             }
694             synchronized (cache) {
695                 PathHeader old = cache.get(path);
696                 if (old != null) {
697                     return old;
698                 }
699             }
700             synchronized (lookup) {
701                 String cleanPath = path;
702                 // special handling for alt
703                 String alt = null;
704                 int altPos = cleanPath.indexOf("[@alt=");
705                 if (altPos >= 0 && !cleanPath.endsWith("/symbol[@alt=\"narrow\"]")) {
706                     if (ALT_MATCHER.reset(cleanPath).find()) {
707                         alt = ALT_MATCHER.group(1);
708                         cleanPath = cleanPath.substring(0, ALT_MATCHER.start())
709                             + cleanPath.substring(ALT_MATCHER.end());
710                         int pos = alt.indexOf("proposed");
711                         if (pos >= 0 && !path.startsWith("//ldml/collations")) {
712                             alt = pos == 0 ? null : alt.substring(0, pos - 1);
713                             // drop "proposed",
714                             // change "xxx-proposed" to xxx.
715                         }
716                     } else {
717                         throw new IllegalArgumentException();
718                     }
719                 }
720                 Output<Finder> matcherFound = new Output<>();
721                 RawData data = lookup.get(cleanPath, null, args, matcherFound, failures);
722                 if (data == null) {
723                     return null;
724                 }
725                 matchersFound.add(matcherFound.value.toString());
726                 counter.add(data, 1);
727                 if (!samples.containsKey(data)) {
728                     samples.put(data, cleanPath);
729                 }
730                 try {
731                     PathHeader result = new PathHeader(
732                         SectionId.forString(fix(data.section, 0)),
733                         PageId.forString(fix(data.page, 0)),
734                         fix(data.header, data.headerOrder),
735                         (int)order, // only valid after call to fix. TODO, make
736                         // this cleaner
737                         fix(data.code + (alt == null ? "" : ("-" + alt)), data.codeOrder),
738                         order, // only valid after call to fix
739                         suborder,
740                         data.status,
741                         path);
742                     synchronized (cache) {
743                         PathHeader old = cache.get(path);
744                         if (old == null) {
745                             cache.put(path, result);
746                         } else {
747                             result = old;
748                         }
749                         Map<PageId, SectionPage> pageToPathHeaders = sectionToPageToSectionPage
750                             .get(result.sectionId);
751                         if (pageToPathHeaders == null) {
752                             sectionToPageToSectionPage.put(result.sectionId, pageToPathHeaders = new EnumMap<>(PageId.class));
753                         }
754                         SectionPage sectionPage = pageToPathHeaders.get(result.pageId);
755                         if (sectionPage == null) {
756                             sectionPage = new SectionPage(result.sectionId, result.pageId);
757                             pageToPathHeaders.put(result.pageId, sectionPage);
758                         }
759                         sectionPageToPaths.put(sectionPage, path);
760                     }
761                     return result;
762                 } catch (Exception e) {
763                     throw new IllegalArgumentException(
764                         "Probably mismatch in Page/Section enum, or too few capturing groups in regex for " + path,
765                         e);
766                 }
767             }
768         }
769 
770         private static class SectionPage implements Comparable<SectionPage> {
771             private final SectionId sectionId;
772             private final PageId pageId;
773 
SectionPage(SectionId sectionId, PageId pageId)774             public SectionPage(SectionId sectionId, PageId pageId) {
775                 this.sectionId = sectionId;
776                 this.pageId = pageId;
777             }
778 
779             @Override
compareTo(SectionPage other)780             public int compareTo(SectionPage other) {
781                 // Within each section, order alphabetically if the integer
782                 // orders are
783                 // not different.
784                 int result;
785                 if (0 != (result = sectionId.compareTo(other.sectionId))) {
786                     return result;
787                 }
788                 if (0 != (result = pageId.compareTo(other.pageId))) {
789                     return result;
790                 }
791                 return 0;
792             }
793 
794             @Override
equals(Object obj)795             public boolean equals(Object obj) {
796                 PathHeader other;
797                 try {
798                     other = (PathHeader) obj;
799                 } catch (Exception e) {
800                     return false;
801                 }
802                 return sectionId == other.sectionId && pageId == other.pageId;
803             }
804 
805             @Override
hashCode()806             public int hashCode() {
807                 return sectionId.hashCode() ^ pageId.hashCode();
808             }
809             @Override
toString()810             public String toString() {
811                 return sectionId + " > " + pageId;
812             }
813         }
814 
815         /**
816          * Returns a set of paths currently associated with the given section
817          * and page.
818          * <p>
819          * <b>Warning:</b>
820          * <ol>
821          * <li>The set may not be complete for a cldrFile unless all of paths in
822          * the file have had fromPath called. And this includes getExtraPaths().
823          * </li>
824          * <li>The set may include paths that have no value in the current
825          * cldrFile.</li>
826          * <li>The set may be empty, if the section/page aren't valid.</li>
827          * </ol>
828          * Thread-safe.
829          *
830          * @target a collection where the paths are to be returned.
831          */
getCachedPaths(SectionId sectionId, PageId page)832         public static Set<String> getCachedPaths(SectionId sectionId, PageId page) {
833             Set<String> target = new HashSet<>();
834             synchronized (cache) {
835                 Map<PageId, SectionPage> pageToSectionPage = sectionToPageToSectionPage
836                     .get(sectionId);
837                 if (pageToSectionPage == null) {
838                     return target;
839                 }
840                 SectionPage sectionPage = pageToSectionPage.get(page);
841                 if (sectionPage == null) {
842                     return target;
843                 }
844                 Set<String> set = sectionPageToPaths.getAll(sectionPage);
845                 target.addAll(set);
846             }
847             return target;
848         }
849 
850         /**
851          * Return the Sections and Pages that are in defined, for display in
852          * menus. Both are ordered.
853          */
getSectionIdsToPageIds()854         public static Relation<SectionId, PageId> getSectionIdsToPageIds() {
855             SectionIdToPageIds.freeze(); // just in case
856             return SectionIdToPageIds;
857         }
858 
859         /**
860          * Return paths that have the designated section and page.
861          *
862          * @param sectionId
863          * @param pageId
864          * @param file
865          */
filterCldr(SectionId sectionId, PageId pageId, CLDRFile file)866         public Iterable<String> filterCldr(SectionId sectionId, PageId pageId, CLDRFile file) {
867             return new FilteredIterable(sectionId, pageId, file);
868         }
869 
870         /**
871          * Return the names for Sections and Pages that are defined, for display
872          * in menus. Both are ordered.
873          *
874          * @deprecated Use getSectionIdsToPageIds
875          */
876         @Deprecated
getSectionsToPages()877         public static LinkedHashMap<String, Set<String>> getSectionsToPages() {
878             LinkedHashMap<String, Set<String>> sectionsToPages = new LinkedHashMap<>();
879             for (PageId pageId : PageId.values()) {
880                 String sectionId2 = pageId.getSectionId().toString();
881                 Set<String> pages = sectionsToPages.get(sectionId2);
882                 if (pages == null) {
883                     sectionsToPages.put(sectionId2, pages = new LinkedHashSet<>());
884                 }
885                 pages.add(pageId.toString());
886             }
887             return sectionsToPages;
888         }
889 
890         /**
891          * @deprecated, use the filterCldr with the section/page ids.
892          */
filterCldr(String section, String page, CLDRFile file)893         public Iterable<String> filterCldr(String section, String page, CLDRFile file) {
894             return new FilteredIterable(section, page, file);
895         }
896 
897         private class FilteredIterable implements Iterable<String>, SimpleIterator<String> {
898             private final SectionId sectionId;
899             private final PageId pageId;
900             private final Iterator<String> fileIterator;
901 
FilteredIterable(SectionId sectionId, PageId pageId, CLDRFile file)902             FilteredIterable(SectionId sectionId, PageId pageId, CLDRFile file) {
903                 this.sectionId = sectionId;
904                 this.pageId = pageId;
905                 this.fileIterator = file.fullIterable().iterator();
906             }
907 
FilteredIterable(String section, String page, CLDRFile file)908             public FilteredIterable(String section, String page, CLDRFile file) {
909                 this(SectionId.forString(section), PageId.forString(page), file);
910             }
911 
912             @Override
iterator()913             public Iterator<String> iterator() {
914                 return With.toIterator(this);
915             }
916 
917             @Override
next()918             public String next() {
919                 while (fileIterator.hasNext()) {
920                     String path = fileIterator.next();
921                     PathHeader pathHeader = fromPath(path);
922                     if (sectionId == pathHeader.sectionId && pageId == pathHeader.pageId) {
923                         return path;
924                     }
925                 }
926                 return null;
927             }
928         }
929 
930         private static class ChronologicalOrder {
931             private Map<String, Integer> map = new HashMap<>();
932             private String item;
933             private int order;
934             private ChronologicalOrder toClear;
935 
ChronologicalOrder(ChronologicalOrder toClear)936             ChronologicalOrder(ChronologicalOrder toClear) {
937                 this.toClear = toClear;
938             }
939 
getOrder()940             int getOrder() {
941                 return order;
942             }
943 
set(String itemToOrder)944             public String set(String itemToOrder) {
945                 if (itemToOrder.startsWith("*")) {
946                     item = itemToOrder.substring(1, itemToOrder.length());
947                     return item; // keep old order
948                 }
949                 item = itemToOrder;
950                 Integer old = map.get(item);
951                 if (old != null) {
952                     order = old.intValue();
953                 } else {
954                     order = map.size();
955                     map.put(item, order);
956                     clearLower();
957                 }
958                 return item;
959             }
960 
clearLower()961             private void clearLower() {
962                 if (toClear != null) {
963                     toClear.map.clear();
964                     toClear.order = 0;
965                     toClear.clearLower();
966                 }
967             }
968         }
969 
970         static class RawData {
971             static ChronologicalOrder codeOrdering = new ChronologicalOrder(null);
972             static ChronologicalOrder headerOrdering = new ChronologicalOrder(codeOrdering);
973 
RawData(String source)974             public RawData(String source) {
975                 String[] split = SEMI.split(source);
976                 section = split[0];
977                 // HACK
978                 if (section.equals("Timezones") && split[1].equals("Indian")) {
979                     page = "Indian2";
980                 } else {
981                     page = split[1];
982                 }
983 
984                 header = headerOrdering.set(split[2]);
985                 headerOrder = headerOrdering.getOrder();
986 
987                 code = codeOrdering.set(split[3]);
988                 codeOrder = codeOrdering.getOrder();
989 
990                 status = split.length < 5 ? SurveyToolStatus.READ_WRITE : SurveyToolStatus.valueOf(split[4]);
991             }
992 
993             public final String section;
994             public final String page;
995             public final String header;
996             public final int headerOrder;
997             public final String code;
998             public final int codeOrder;
999             public final SurveyToolStatus status;
1000 
1001             @Override
1002             public String toString() {
1003                 return section + "\t"
1004                     + page + "\t"
1005                     + header + "\t" + headerOrder + "\t"
1006                     + code + "\t" + codeOrder + "\t"
1007                     + status;
1008             }
1009         }
1010 
1011         static class PathHeaderTransform implements Transform<String, RawData> {
1012             @Override
1013             public RawData transform(String source) {
1014                 return new RawData(source);
1015             }
1016         }
1017 
1018         /**
1019          * Internal data, for testing and debugging.
1020          *
1021          * @deprecated
1022          */
1023         @Deprecated
1024         public class CounterData extends Row.R4<String, RawData, String, String> {
1025             public CounterData(String a, RawData b, String c) {
1026                 super(a, b, c == null ? "no sample" : c, c == null ? "no sample" : fromPath(c)
1027                     .toString());
1028             }
1029         }
1030 
1031         /**
1032          * Get the internal data, for testing and debugging.
1033          *
1034          * @deprecated
1035          */
1036         @Deprecated
1037         public Counter<CounterData> getInternalCounter() {
1038             synchronized (lookup) {
1039                 Counter<CounterData> result = new Counter<>();
1040                 for (Map.Entry<Finder, RawData> foo : lookup) {
1041                     Finder finder = foo.getKey();
1042                     RawData data = foo.getValue();
1043                     long count = counter.get(data);
1044                     result.add(new CounterData(finder.toString(), data, samples.get(data)), count);
1045                 }
1046                 return result;
1047             }
1048         }
1049 
1050         static Map<String, Transform<String, String>> functionMap = new HashMap<>();
1051         static String[] months = { "Jan", "Feb", "Mar",
1052             "Apr", "May", "Jun",
1053             "Jul", "Aug", "Sep",
1054             "Oct", "Nov", "Dec",
1055         "Und" };
1056         static List<String> days = Arrays.asList("sun", "mon",
1057             "tue", "wed", "thu",
1058             "fri", "sat");
1059         static List<String> unitOrder = DtdData.unitOrder.getOrder();
1060         static final MapComparator<String> dayPeriods = new MapComparator<String>().add(
1061             "am", "pm", "midnight", "noon",
1062             "morning1", "morning2", "afternoon1", "afternoon2", "evening1", "evening2", "night1", "night2").freeze();
1063         // static Map<String, String> likelySubtags =
1064         // supplementalDataInfo.getLikelySubtags();
1065         static LikelySubtags likelySubtags = new LikelySubtags();
1066         static HyphenSplitter hyphenSplitter = new HyphenSplitter();
1067         static Transform<String, String> catFromTerritory;
1068         static Transform<String, String> catFromTimezone;
1069         static {
1070             // Put any new functions used in PathHeader.txt in here.
1071             // To change the order of items within a section or heading, set
1072             // order/suborder to be the relative position of the current item.
1073             functionMap.put("month", new Transform<String, String>() {
1074                 @Override
1075                 public String transform(String source) {
1076                     int m = Integer.parseInt(source);
1077                     order = m;
1078                     return months[m - 1];
1079                 }
1080             });
1081             functionMap.put("count", new Transform<String, String>() {
1082                 @Override
1083                 public String transform(String source) {
1084                     suborder = new SubstringOrder(source);
1085                     return source;
1086                 }
1087             });
1088             functionMap.put("count2", new Transform<String, String>() {
1089                 @Override
1090                 public String transform(String source) {
1091                     int pos = source.indexOf('-');
1092                     source = pos + source.substring(pos);
1093                     suborder = new SubstringOrder(source); // make 10000-...
1094                     // into 5-
1095                     return source;
1096                 }
1097             });
1098             functionMap.put("currencySymbol", new Transform<String, String>() {
1099                 @Override
1100                 public String transform(String source) {
1101                     order = 901;
1102                     if (source.endsWith("narrow")) {
1103                         order = 902;
1104                     }
1105                     if (source.endsWith("variant")) {
1106                         order = 903;
1107                     }
1108                     return source;
1109                 }
1110             });
1111             functionMap.put("unitCount", new Transform<String, String>() {
1112                 @Override
1113                 public String transform(String source) {
1114                     String[] unitLengths = { "long", "short", "narrow" };
1115                     int pos = 9;
1116                     for (int i = 0; i < unitLengths.length; i++) {
1117                         if (source.startsWith(unitLengths[i])) {
1118                             pos = i;
1119                             continue;
1120                         }
1121                     }
1122                     order = pos;
1123                     suborder = new SubstringOrder(pos + "-" + source); //
1124                     return source;
1125                 }
1126             });
1127             functionMap.put("day", new Transform<String, String>() {
1128                 @Override
1129                 public String transform(String source) {
1130                     int m = days.indexOf(source);
1131                     order = m;
1132                     return source;
1133                 }
1134             });
1135             functionMap.put("dayPeriod", new Transform<String, String>() {
1136                 @Override
1137                 public String transform(String source) {
1138                     try {
1139                         order = dayPeriods.getNumericOrder(source);
1140                     } catch (Exception e) {
1141                         // if an old item is tried, like "evening", this will fail.
1142                         // so that old data still works, hack this.
1143                         order = Math.abs(source.hashCode() << 16);
1144                     }
1145                     return source;
1146                 }
1147             });
1148             functionMap.put("calendar", new Transform<String, String>() {
1149                 Map<String, String> fixNames = Builder.with(new HashMap<String, String>())
1150                     .put("islamicc", "Islamic Civil")
1151                     .put("roc", "Minguo")
1152                     .put("Ethioaa", "Ethiopic Amete Alem")
1153                     .put("Gregory", "Gregorian")
1154                     .put("iso8601", "ISO 8601")
1155                     .freeze();
1156 
1157                 @Override
1158                 public String transform(String source) {
1159                     String result = fixNames.get(source);
1160                     return result != null ? result : UCharacter.toTitleCase(source, null);
1161                 }
1162             });
1163 
1164             functionMap.put("calField", new Transform<String, String>() {
1165                 @Override
1166                 public String transform(String source) {
1167                     String[] fields = source.split(":", 3);
1168                     order = 0;
1169                     final List<String> widthValues = Arrays.asList(
1170                         "wide", "abbreviated", "short", "narrow");
1171                     final List<String> calendarFieldValues = Arrays.asList(
1172                         "Eras",
1173                         "Quarters",
1174                         "Months",
1175                         "Days",
1176                         "DayPeriods",
1177                         "Formats");
1178                     final List<String> calendarFormatTypes = Arrays.asList(
1179                         "Standard",
1180                         "Flexible",
1181                         "Intervals");
1182                     final List<String> calendarContextTypes = Arrays.asList(
1183                         "none",
1184                         "format",
1185                         "stand-alone");
1186                     final List<String> calendarFormatSubtypes = Arrays.asList(
1187                         "date",
1188                         "time",
1189                         "time12",
1190                         "time24",
1191                         "dateTime",
1192                         "fallback");
1193 
1194                     Map<String, String> fixNames = Builder.with(new HashMap<String, String>())
1195                         .put("DayPeriods", "Day Periods")
1196                         .put("format", "Formatting")
1197                         .put("stand-alone", "Standalone")
1198                         .put("none", "")
1199                         .put("date", "Date Formats")
1200                         .put("time", "Time Formats")
1201                         .put("time12", "12 Hour Time Formats")
1202                         .put("time24", "24 Hour Time Formats")
1203                         .put("dateTime", "Date & Time Combination Formats")
1204                         .freeze();
1205 
1206                     if (calendarFieldValues.contains(fields[0])) {
1207                         order = calendarFieldValues.indexOf(fields[0]) * 100;
1208                     } else {
1209                         order = calendarFieldValues.size() * 100;
1210                     }
1211 
1212                     if (fields[0].equals("Formats")) {
1213                         if (calendarFormatTypes.contains(fields[1])) {
1214                             order += calendarFormatTypes.indexOf(fields[1]) * 10;
1215                         } else {
1216                             order += calendarFormatTypes.size() * 10;
1217                         }
1218                         if (calendarFormatSubtypes.contains(fields[2])) {
1219                             order += calendarFormatSubtypes.indexOf(fields[2]);
1220                         } else {
1221                             order += calendarFormatSubtypes.size();
1222                         }
1223                     } else {
1224                         if (widthValues.contains(fields[1])) {
1225                             order += widthValues.indexOf(fields[1]) * 10;
1226                         } else {
1227                             order += widthValues.size() * 10;
1228                         }
1229                         if (calendarContextTypes.contains(fields[2])) {
1230                             order += calendarContextTypes.indexOf(fields[2]);
1231                         } else {
1232                             order += calendarContextTypes.size();
1233                         }
1234                     }
1235 
1236                     String[] fixedFields = new String[fields.length];
1237                     for (int i = 0; i < fields.length; i++) {
1238                         String s = fixNames.get(fields[i]);
1239                         fixedFields[i] = s != null ? s : fields[i];
1240                     }
1241 
1242                     return fixedFields[0] +
1243                         " - " + fixedFields[1] +
1244                         (fixedFields[2].length() > 0 ? " - " + fixedFields[2] : "");
1245                 }
1246             });
1247 
1248             functionMap.put("titlecase", new Transform<String, String>() {
1249                 @Override
1250                 public String transform(String source) {
1251                     return UCharacter.toTitleCase(source, null);
1252                 }
1253             });
1254             functionMap.put("categoryFromScript", new Transform<String, String>() {
1255                 @Override
1256                 public String transform(String source) {
1257                     String script = hyphenSplitter.split(source);
1258                     Info info = ScriptMetadata.getInfo(script);
1259                     if (info == null) {
1260                         info = ScriptMetadata.getInfo("Zzzz");
1261                     }
1262                     order = 100 - info.idUsage.ordinal();
1263                     return info.idUsage.name;
1264                 }
1265             });
1266             functionMap.put("categoryFromKey", new Transform<String, String>() {
1267                 Map<String, String> fixNames = Builder.with(new HashMap<String, String>())
1268                     .put("lb", "Line Break")
1269                     .put("hc", "Hour Cycle")
1270                     .put("ms", "Measurement System")
1271                     .put("cf", "Currency Format")
1272                     .freeze();
1273 
1274                 @Override
1275                 public String transform(String source) {
1276                     String fixedName = fixNames.get(source);
1277                     return fixedName != null ? fixedName : source;
1278                 }
1279             });
1280             functionMap.put("languageSection", new Transform<String, String>() {
1281                 char[] languageRangeStartPoints = { 'A', 'E', 'K', 'O', 'T' };
1282                 char[] languageRangeEndPoints = { 'D', 'J', 'N', 'S', 'Z' };
1283 
1284                 @Override
1285                 public String transform(String source0) {
1286                     char firstLetter = getEnglishFirstLetter(source0).charAt(0);
1287                     for (int i = 0; i < languageRangeStartPoints.length; i++) {
1288                         if (firstLetter >= languageRangeStartPoints[i] && firstLetter <= languageRangeEndPoints[i]) {
1289                             return "Languages (" + Character.toUpperCase(languageRangeStartPoints[i]) + "-" + Character.toUpperCase(languageRangeEndPoints[i])
1290                             + ")";
1291                         }
1292                     }
1293                     return "Languages";
1294                 }
1295             });
1296             functionMap.put("firstLetter", new Transform<String, String>() {
1297                 @Override
1298                 public String transform(String source0) {
1299                     return getEnglishFirstLetter(source0);
1300                 }
1301             });
1302             functionMap.put("languageSort", new Transform<String, String>() {
1303                 @Override
1304                 public String transform(String source0) {
1305                     String languageOnlyPart;
1306                     int underscorePos = source0.indexOf("_");
1307                     if (underscorePos > 0) {
1308                         languageOnlyPart = source0.substring(0, underscorePos);
1309                     } else {
1310                         languageOnlyPart = source0;
1311                     }
1312 
1313                     return englishFile.getName(CLDRFile.LANGUAGE_NAME, languageOnlyPart) + " \u25BA " + source0;
1314                 }
1315             });
1316             functionMap.put("scriptFromLanguage", new Transform<String, String>() {
1317                 @Override
1318                 public String transform(String source0) {
1319                     String language = hyphenSplitter.split(source0);
1320                     String script = likelySubtags.getLikelyScript(language);
1321                     if (script == null) {
1322                         script = likelySubtags.getLikelyScript(language);
1323                     }
1324                     String scriptName = englishFile.getName(CLDRFile.SCRIPT_NAME, script);
1325                     return "Languages in " + (script.equals("Hans") || script.equals("Hant") ? "Han Script"
1326                         : scriptName.endsWith(" Script") ? scriptName
1327                             : scriptName + " Script");
1328                 }
1329             });
1330             functionMap.put("categoryFromTerritory",
1331                 catFromTerritory = new Transform<String, String>() {
1332                 @Override
1333                 public String transform(String source) {
1334                     String territory = getSubdivisionsTerritory(source, null);
1335                     String container = Containment.getContainer(territory);
1336                     order = Containment.getOrder(territory);
1337                     return englishFile.getName(CLDRFile.TERRITORY_NAME, container);
1338                 }
1339             });
1340             functionMap.put("territorySection", new Transform<String, String>() {
1341                 final Set<String> specialRegions = new HashSet<>(Arrays.asList("EZ", "EU", "QO", "UN", "ZZ"));
1342 
1343                 @Override
1344                 public String transform(String source0) {
1345                     // support subdivisions
1346                     String theTerritory = getSubdivisionsTerritory(source0, null);
1347                     try {
1348                         if (specialRegions.contains(theTerritory)
1349                             || theTerritory.charAt(0) < 'A' && Integer.valueOf(theTerritory) > 0) {
1350                             return "Geographic Regions";
1351                         }
1352                     } catch (NumberFormatException ex) {
1353                     }
1354                     String theContinent = Containment.getContinent(theTerritory);
1355                     String theSubContinent;
1356                     switch (theContinent) { // was Integer.valueOf
1357                     case "019": // Americas - For the territorySection, we just group North America & South America
1358                         final String subcontinent = Containment.getSubcontinent(theTerritory);
1359                         theSubContinent = subcontinent.equals("005") ? "005" : "003"; // was Integer.valueOf(subcontinent) == 5
1360                         return "Territories (" + englishFile.getName(CLDRFile.TERRITORY_NAME, theSubContinent) + ")";
1361                     case "001":
1362                     case "ZZ":
1363                         return "Geographic Regions"; // not in containment
1364                     default:
1365                         return "Territories (" + englishFile.getName(CLDRFile.TERRITORY_NAME, theContinent) + ")";
1366                     }
1367                 }
1368             });
1369             functionMap.put("categoryFromTimezone",
1370                 catFromTimezone = new Transform<String, String>() {
1371                 @Override
1372                 public String transform(String source0) {
1373                     String territory = Containment.getRegionFromZone(source0);
1374                     if (territory == null) {
1375                         territory = "ZZ";
1376                     }
1377                     return catFromTerritory.transform(territory);
1378                 }
1379             });
1380             functionMap.put("timeZonePage", new Transform<String, String>() {
1381                 Set<String> singlePageTerritories = new HashSet<>(Arrays.asList("AQ", "RU", "ZZ"));
1382 
1383                 @Override
1384                 public String transform(String source0) {
1385                     String theTerritory = Containment.getRegionFromZone(source0);
1386                     if (theTerritory == null || theTerritory == "001") {
1387                         theTerritory = "ZZ";
1388                     }
1389                     if (singlePageTerritories.contains(theTerritory)) {
1390                         return englishFile.getName(CLDRFile.TERRITORY_NAME, theTerritory);
1391                     }
1392                     String theContinent = Containment.getContinent(theTerritory);
1393                     final String subcontinent = Containment.getSubcontinent(theTerritory);
1394                     String theSubContinent;
1395                     switch (Integer.valueOf(theContinent)) {
1396                     case 9: // Oceania - For the timeZonePage, we group Australasia on one page, and the rest of Oceania on the other.
1397                         try {
1398                             theSubContinent = subcontinent.equals("053") ? "053" : "009"; // was Integer.valueOf(subcontinent) == 53
1399                         } catch (NumberFormatException ex) {
1400                             theSubContinent = "009";
1401                         }
1402                         return englishFile.getName(CLDRFile.TERRITORY_NAME, theSubContinent);
1403                     case 19: // Americas - For the timeZonePage, we just group North America & South America
1404                         theSubContinent = Integer.valueOf(subcontinent) == 5 ? "005" : "003";
1405                         return englishFile.getName(CLDRFile.TERRITORY_NAME, theSubContinent);
1406                     case 142: // Asia
1407                         return englishFile.getName(CLDRFile.TERRITORY_NAME, subcontinent);
1408                     default:
1409                         return englishFile.getName(CLDRFile.TERRITORY_NAME, theContinent);
1410                     }
1411                 }
1412             });
1413 
1414             functionMap.put("timezoneSorting", new Transform<String, String>() {
1415                 @Override
1416                 public String transform(String source) {
1417                     final List<String> codeValues = Arrays.asList(
1418                         "generic-long",
1419                         "generic-short",
1420                         "standard-long",
1421                         "standard-short",
1422                         "daylight-long",
1423                         "daylight-short");
1424                     if (codeValues.contains(source)) {
1425                         order = codeValues.indexOf(source);
1426                     } else {
1427                         order = codeValues.size();
1428                     }
1429                     return source;
1430                 }
1431             });
1432 
1433             functionMap.put("tzdpField", new Transform<String, String>() {
1434                 @Override
1435                 public String transform(String source) {
1436                     Map<String, String> fieldNames = Builder.with(new HashMap<String, String>())
1437                         .put("regionFormat", "Region Format - Generic")
1438                         .put("regionFormat-standard", "Region Format - Standard")
1439                         .put("regionFormat-daylight", "Region Format - Daylight")
1440                         .put("gmtFormat", "GMT Format")
1441                         .put("hourFormat", "GMT Hours/Minutes Format")
1442                         .put("gmtZeroFormat", "GMT Zero Format")
1443                         .put("fallbackFormat", "Location Fallback Format")
1444                         .freeze();
1445                     final List<String> fieldOrder = Arrays.asList(
1446                         "regionFormat",
1447                         "regionFormat-standard",
1448                         "regionFormat-daylight",
1449                         "gmtFormat",
1450                         "hourFormat",
1451                         "gmtZeroFormat",
1452                         "fallbackFormat");
1453 
1454                     if (fieldOrder.contains(source)) {
1455                         order = fieldOrder.indexOf(source);
1456                     } else {
1457                         order = fieldOrder.size();
1458                     }
1459 
1460                     String result = fieldNames.get(source);
1461                     return result == null ? source : result;
1462                 }
1463             });
1464             functionMap.put("unit", new Transform<String, String>() {
1465                 @Override
1466                 public String transform(String source) {
1467                     int m = unitOrder.indexOf(source);
1468                     order = m;
1469                     return source.substring(source.indexOf('-') + 1);
1470                 }
1471             });
1472 
1473             functionMap.put("numericSort", new Transform<String, String>() {
1474                 // Probably only works well for small values, like -5 through +4.
1475                 @Override
1476                 public String transform(String source) {
1477                     Integer pos = Integer.valueOf(source) + 5;
1478                     suborder = new SubstringOrder(pos.toString());
1479                     return source;
1480                 }
1481             });
1482 
1483             functionMap.put("metazone", new Transform<String, String>() {
1484 
1485                 @Override
1486                 public String transform(String source) {
1487                     if (PathHeader.UNIFORM_CONTINENTS) {
1488                         String container = getMetazonePageTerritory(source);
1489                         order = Containment.getOrder(container);
1490                         return englishFile.getName(CLDRFile.TERRITORY_NAME, container);
1491                     } else {
1492                         String continent = metazoneToContinent.get(source);
1493                         if (continent == null) {
1494                             continent = "UnknownT";
1495                         }
1496                         return continent;
1497                     }
1498                 }
1499             });
1500 
1501             Object[][] ctto = {
1502                 { "BUK", "MM" },
1503                 { "CSD", "RS" },
1504                 { "CSK", "CZ" },
1505                 { "DDM", "DE" },
1506                 { "EUR", "ZZ" },
1507                 { "RHD", "ZW" },
1508                 { "SUR", "RU" },
1509                 { "TPE", "TL" },
1510                 { "XAG", "ZZ" },
1511                 { "XAU", "ZZ" },
1512                 { "XBA", "ZZ" },
1513                 { "XBB", "ZZ" },
1514                 { "XBC", "ZZ" },
1515                 { "XBD", "ZZ" },
1516                 { "XDR", "ZZ" },
1517                 { "XEU", "ZZ" },
1518                 { "XFO", "ZZ" },
1519                 { "XFU", "ZZ" },
1520                 { "XPD", "ZZ" },
1521                 { "XPT", "ZZ" },
1522                 { "XRE", "ZZ" },
1523                 { "XSU", "ZZ" },
1524                 { "XTS", "ZZ" },
1525                 { "XUA", "ZZ" },
1526                 { "XXX", "ZZ" },
1527                 { "YDD", "YE" },
1528                 { "YUD", "RS" },
1529                 { "YUM", "RS" },
1530                 { "YUN", "RS" },
1531                 { "YUR", "RS" },
1532                 { "ZRN", "CD" },
1533                 { "ZRZ", "CD" },
1534             };
1535 
1536             Object[][] sctc = {
1537                 { "Northern America", "North America (C)" },
1538                 { "Central America", "North America (C)" },
1539                 { "Caribbean", "North America (C)" },
1540                 { "South America", "South America (C)" },
1541                 { "Northern Africa", "Northern Africa" },
1542                 { "Western Africa", "Western Africa" },
1543                 { "Middle Africa", "Middle Africa" },
1544                 { "Eastern Africa", "Eastern Africa" },
1545                 { "Southern Africa", "Southern Africa" },
1546                 { "Europe", "Northern/Western Europe" },
1547                 { "Northern Europe", "Northern/Western Europe" },
1548                 { "Western Europe", "Northern/Western Europe" },
1549                 { "Eastern Europe", "Southern/Eastern Europe" },
1550                 { "Southern Europe", "Southern/Eastern Europe" },
1551                 { "Western Asia", "Western Asia (C)" },
1552                 { "Central Asia", "Central Asia (C)" },
1553                 { "Eastern Asia", "Eastern Asia (C)" },
1554                 { "Southern Asia", "Southern Asia (C)" },
1555                 { "Southeast Asia", "Southeast Asia (C)" },
1556                 { "Australasia", "Oceania (C)" },
1557                 { "Melanesia", "Oceania (C)" },
1558                 { "Micronesian Region", "Oceania (C)" }, // HACK
1559                 { "Polynesia", "Oceania (C)" },
1560                 { "Unknown Region", "Unknown Region (C)" },
1561             };
1562 
1563             final Map<String, String> currencyToTerritoryOverrides = CldrUtility.asMap(ctto);
1564             final Map<String, String> subContinentToContinent = CldrUtility.asMap(sctc);
1565             final Set<String> fundCurrencies = new HashSet<>(Arrays.asList("CHE", "CHW", "CLF", "COU", "ECV", "MXV", "USN", "USS", "UYI", "XEU", "ZAL"));
1566             final Set<String> offshoreCurrencies = new HashSet<>(Arrays.asList("CNH"));
1567             // TODO: Put this into supplementalDataInfo ?
1568 
1569             functionMap.put("categoryFromCurrency", new Transform<String, String>() {
1570                 @Override
1571                 public String transform(String source0) {
1572                     String tenderOrNot = "";
1573                     String territory = likelySubtags.getLikelyTerritoryFromCurrency(source0);
1574                     if (territory == null) {
1575                         String tag;
1576                         if (fundCurrencies.contains(source0)) {
1577                             tag = " (fund)";
1578                         } else if (offshoreCurrencies.contains(source0)) {
1579                             tag = " (offshore)";
1580                         } else {
1581                             tag = " (old)";
1582                         }
1583                         tenderOrNot = ": " + source0 + tag;
1584                     }
1585                     if (currencyToTerritoryOverrides.keySet().contains(source0)) {
1586                         territory = currencyToTerritoryOverrides.get(source0);
1587                     } else if (territory == null) {
1588                         territory = source0.substring(0, 2);
1589                     }
1590 
1591                     if (territory.equals("ZZ")) {
1592                         order = 999;
1593                         return englishFile.getName(CLDRFile.TERRITORY_NAME, territory) + ": " + source0;
1594                     } else {
1595                         return catFromTerritory.transform(territory) + ": "
1596                             + englishFile.getName(CLDRFile.TERRITORY_NAME, territory)
1597                             + tenderOrNot;
1598                     }
1599                 }
1600             });
1601             functionMap.put("continentFromCurrency", new Transform<String, String>() {
1602                 @Override
1603                 public String transform(String source0) {
1604                     String subContinent;
1605                     String territory = likelySubtags.getLikelyTerritoryFromCurrency(source0);
1606                     if (currencyToTerritoryOverrides.keySet().contains(source0)) {
1607                         territory = currencyToTerritoryOverrides.get(source0);
1608                     } else if (territory == null) {
1609                         territory = source0.substring(0, 2);
1610                     }
1611 
1612                     if (territory.equals("ZZ")) {
1613                         order = 999;
1614                         subContinent = englishFile.getName(CLDRFile.TERRITORY_NAME, territory);
1615                     } else {
1616                         subContinent = catFromTerritory.transform(territory);
1617                     }
1618 
1619                     String result = subContinentToContinent.get(subContinent); //the continent is the last word in the territory representation
1620                     return result;
1621                 }
1622             });
1623             functionMap.put("numberingSystem", new Transform<String, String>() {
1624                 @Override
1625                 public String transform(String source0) {
1626                     if ("latn".equals(source0)) {
1627                         return "";
1628                     }
1629                     String displayName = englishFile.getStringValue("//ldml/localeDisplayNames/types/type[@key=\"numbers\"][@type=\""
1630                         + source0 + "\"]");
1631                     return "using " + (displayName == null ? source0 : displayName + " (" + source0 + ")");
1632                 }
1633             });
1634 
1635             functionMap.put("datefield", new Transform<String, String>() {
1636                 private final String[] datefield = {
1637                     "era", "era-short", "era-narrow",
1638                     "century", "century-short", "century-narrow",
1639                     "year", "year-short", "year-narrow",
1640                     "quarter", "quarter-short", "quarter-narrow",
1641                     "month", "month-short", "month-narrow",
1642                     "week", "week-short", "week-narrow",
1643                     "weekOfMonth", "weekOfMonth-short", "weekOfMonth-narrow",
1644                     "day", "day-short", "day-narrow",
1645                     "dayOfYear", "dayOfYear-short", "dayOfYear-narrow",
1646                     "weekday", "weekday-short", "weekday-narrow",
1647                     "weekdayOfMonth", "weekdayOfMonth-short", "weekdayOfMonth-narrow",
1648                     "dayperiod", "dayperiod-short", "dayperiod-narrow",
1649                     "zone", "zone-short", "zone-narrow",
1650                     "hour", "hour-short", "hour-narrow",
1651                     "minute", "minute-short", "minute-narrow",
1652                     "second", "second-short", "second-narrow",
1653                     "millisecond", "millisecond-short", "millisecond-narrow",
1654                     "microsecond", "microsecond-short", "microsecond-narrow",
1655                     "nanosecond", "nanosecond-short", "nanosecond-narrow",
1656 
1657                 };
1658 
1659                 @Override
1660                 public String transform(String source) {
1661                     order = getIndex(source, datefield);
1662                     return source;
1663                 }
1664             });
1665             // //ldml/dates/fields/field[@type="%A"]/relative[@type="%A"]
1666             functionMap.put("relativeDate", new Transform<String, String>() {
1667                 private final String[] relativeDateField = {
1668                     "year", "year-short", "year-narrow",
1669                     "quarter", "quarter-short", "quarter-narrow",
1670                     "month", "month-short", "month-narrow",
1671                     "week", "week-short", "week-narrow",
1672                     "day", "day-short", "day-narrow",
1673                     "hour", "hour-short", "hour-narrow",
1674                     "minute", "minute-short", "minute-narrow",
1675                     "second", "second-short", "second-narrow",
1676                     "sun", "sun-short", "sun-narrow",
1677                     "mon", "mon-short", "mon-narrow",
1678                     "tue", "tue-short", "tue-narrow",
1679                     "wed", "wed-short", "wed-narrow",
1680                     "thu", "thu-short", "thu-narrow",
1681                     "fri", "fri-short", "fri-narrow",
1682                     "sat", "sat-short", "sat-narrow",
1683                 };
1684                 private final String[] longNames = {
1685                     "Year", "Year Short", "Year Narrow",
1686                     "Quarter", "Quarter Short", "Quarter Narrow",
1687                     "Month", "Month Short", "Month Narrow",
1688                     "Week", "Week Short", "Week Narrow",
1689                     "Day", "Day Short", "Day Narrow",
1690                     "Hour", "Hour Short", "Hour Narrow",
1691                     "Minute", "Minute Short", "Minute Narrow",
1692                     "Second", "Second Short", "Second Narrow",
1693                     "Sunday", "Sunday Short", "Sunday Narrow",
1694                     "Monday", "Monday Short", "Monday Narrow",
1695                     "Tuesday", "Tuesday Short", "Tuesday Narrow",
1696                     "Wednesday", "Wednesday Short", "Wednesday Narrow",
1697                     "Thursday", "Thursday Short", "Thursday Narrow",
1698                     "Friday", "Friday Short", "Friday Narrow",
1699                     "Saturday", "Saturday Short", "Saturday Narrow",
1700                 };
1701 
1702                 @Override
1703                 public String transform(String source) {
1704                     order = getIndex(source, relativeDateField) + 100;
1705                     return "Relative " + longNames[getIndex(source, relativeDateField)];
1706                 }
1707             });
1708             // Sorts numberSystem items (except for decimal formats).
1709             functionMap.put("number", new Transform<String, String>() {
1710                 private final String[] symbols = { "decimal", "group",
1711                     "plusSign", "minusSign", "approximatelySign",
1712                     "percentSign", "perMille",
1713                     "exponential", "superscriptingExponent",
1714                     "infinity", "nan", "list", "currencies"
1715                 };
1716 
1717                 @Override
1718                 public String transform(String source) {
1719                     String[] parts = source.split("-");
1720                     order = getIndex(parts[0], symbols);
1721                     // e.g. "currencies-one"
1722                     if (parts.length > 1) {
1723                         suborder = new SubstringOrder(parts[1]);
1724                     }
1725                     return source;
1726                 }
1727             });
1728             functionMap.put("numberFormat", new Transform<String, String>() {
1729                 @Override
1730                 public String transform(String source) {
1731                     final List<String> fieldOrder = Arrays.asList(
1732                         "standard-decimal",
1733                         "standard-currency",
1734                         "standard-currency-accounting",
1735                         "standard-percent",
1736                         "standard-scientific");
1737 
1738                     if (fieldOrder.contains(source)) {
1739                         order = fieldOrder.indexOf(source);
1740                     } else {
1741                         order = fieldOrder.size();
1742                     }
1743 
1744                     return source;
1745                 }
1746             });
1747 
1748             functionMap.put("localePattern", new Transform<String, String>() {
1749                 @Override
1750                 public String transform(String source) {
1751                     // Put localeKeyTypePattern behind localePattern and
1752                     // localeSeparator.
1753                     if (source.equals("localeKeyTypePattern")) {
1754                         order = 10;
1755                     }
1756                     return source;
1757                 }
1758             });
1759             functionMap.put("listOrder", new Transform<String, String>() {
1760                 private String[] listParts = { "2", "start", "middle", "end" };
1761 
1762                 @Override
1763                 public String transform(String source) {
1764                     order = getIndex(source, listParts);
1765                     return source;
1766                 }
1767             });
1768             functionMap.put("alphaOrder", new Transform<String, String>() {
1769                 @Override
1770                 public String transform(String source) {
1771                     order = 0;
1772                     return source;
1773                 }
1774             });
1775             functionMap.put("transform", new Transform<String, String>() {
1776                 Splitter commas = Splitter.on(',').trimResults();
1777 
1778                 @Override
1779                 public String transform(String source) {
1780                     List<String> parts = commas.splitToList(source);
1781                     return parts.get(1)
1782                         + (parts.get(0).equals("both") ? "↔︎" : "→")
1783                         + parts.get(2)
1784                         + (parts.size() > 3 ? "/" + parts.get(3) : "");
1785                 }
1786             });
1787             functionMap.put("major", new Transform<String, String>() {
1788                 @Override
1789                 public String transform(String source) {
1790                     String major = Emoji.getMajorCategory(source);
1791                     // check that result is reasonable by running through PageId.
1792                     switch(major) {
1793                     default:
1794                         PageId pageId2 = PageId.forString(major);
1795                         if (pageId2.getSectionId() != SectionId.Characters) {
1796                             if (pageId2 == PageId.Symbols) {
1797                                 pageId2 = PageId.Symbols2;
1798                             }
1799                         }
1800                         return pageId2.toString();
1801                     case "Smileys & People":
1802                         String minorCat = Emoji.getMinorCategory(source);
1803                         if (minorCat.equals("skin-tone") || minorCat.equals("hair-style")) {
1804                             return PageId.Component.toString();
1805                         } else if (!minorCat.contains("face")) {
1806                             return PageId.People.toString();
1807                         } else {
1808                             return PageId.Smileys.toString();
1809                         }
1810                     }
1811                 }
1812             });
1813             functionMap.put("minor", new Transform<String, String>() {
1814                 @Override
1815                 public String transform(String source) {
1816                     String minorCat = Emoji.getMinorCategory(source);
1817                     order = Emoji.getEmojiMinorOrder(minorCat);
1818                     return minorCat;
1819                 }
1820             });
1821             /**
1822              * Use the ordering of the emoji in getEmojiToOrder rather than alphabetic,
1823              * since the collator data won't be ready until the candidates are final.
1824              */
1825             functionMap.put("emoji", new Transform<String, String>() {
1826                 @Override
1827                 public String transform(String source) {
1828                     int dashPos = source.indexOf(' ');
1829                     String emoji = source.substring(0, dashPos);
1830                     order = (Emoji.getEmojiToOrder(emoji) << 1) + (source.endsWith("name") ? 0 : 1);
1831                     return source;
1832                 }
1833             });
1834 
1835         }
1836 
1837         private static int getIndex(String item, String[] array) {
1838             for (int i = 0; i < array.length; i++) {
1839                 if (item.equals(array[i])) {
1840                     return i;
1841                 }
1842             }
1843             return -1;
1844         }
1845 
1846         private static String getEnglishFirstLetter(String s) {
1847             String languageOnlyPart;
1848             int underscorePos = s.indexOf("_");
1849             if (underscorePos > 0) {
1850                 languageOnlyPart = s.substring(0, underscorePos);
1851             } else {
1852                 languageOnlyPart = s;
1853             }
1854             final String name = englishFile.getName(CLDRFile.LANGUAGE_NAME, languageOnlyPart);
1855             return name == null ? "?" : name.substring(0, 1).toUpperCase();
1856         }
1857 
1858         static class HyphenSplitter {
1859             String main;
1860             String extras;
1861 
1862             String split(String source) {
1863                 int hyphenPos = source.indexOf('-');
1864                 if (hyphenPos < 0) {
1865                     main = source;
1866                     extras = "";
1867                 } else {
1868                     main = source.substring(0, hyphenPos);
1869                     extras = source.substring(hyphenPos);
1870                 }
1871                 return main;
1872             }
1873         }
1874 
1875         /**
1876          * This converts "functions", like &month, and sets the order.
1877          *
1878          * @param input
1879          * @param order
1880          * @return
1881          */
1882         private static String fix(String input, int orderIn) {
1883             if (input.contains("��")) {
1884                 int debug = 0;
1885             }
1886             String oldInput = input;
1887             input = RegexLookup.replace(input, args.value);
1888             order = orderIn;
1889             suborder = null;
1890             int pos = 0;
1891             while (true) {
1892                 int functionStart = input.indexOf('&', pos);
1893                 if (functionStart < 0) {
1894                     return input;
1895                 }
1896                 int functionEnd = input.indexOf('(', functionStart);
1897                 int argEnd = input.indexOf(')', functionEnd+2); // we must insert at least one character
1898                 Transform<String, String> func = functionMap.get(input.substring(functionStart + 1,
1899                     functionEnd));
1900                 final String arg = input.substring(functionEnd + 1, argEnd);
1901                 String temp = func.transform(arg);
1902                 if (temp == null) {
1903                     func.transform(arg);
1904                     throw new IllegalArgumentException("Function returns invalid results for «" + arg + "».");
1905                 }
1906                 input = input.substring(0, functionStart) + temp + input.substring(argEnd + 1);
1907                 pos = functionStart + temp.length();
1908             }
1909         }
1910 
1911         /**
1912          * Collect all the paths for a CLDRFile, and make sure that they have
1913          * cached PathHeaders
1914          *
1915          * @param file
1916          * @return immutable set of paths in the file
1917          */
1918         public Set<String> pathsForFile(CLDRFile file) {
1919             // make sure we cache all the path headers
1920             HashSet<String> filePaths = new HashSet<>();
1921             file.fullIterable().forEach(filePaths::add);
1922             for (String path : filePaths) {
1923                 try {
1924                     fromPath(path); // call to make sure cached
1925                 } catch (Throwable t) {
1926                     // ... some other exception
1927                 }
1928             }
1929             return Collections.unmodifiableSet(filePaths);
1930         }
1931 
1932         /**
1933          * Returns those regexes that were never matched.
1934          * @return
1935          */
1936         public Set<String> getUnmatchedRegexes() {
1937             Map<String, RawData> outputUnmatched = new LinkedHashMap<>();
1938             lookup.getUnmatchedPatterns(matchersFound, outputUnmatched);
1939             return outputUnmatched.keySet();
1940         }
1941 
1942         public String getRegexInfo() {
1943             return lookup.toString();
1944         }
1945     }
1946 
1947     /**
1948      * Return the territory used for the title of the Metazone page in the
1949      * Survey Tool.
1950      *
1951      * @param source
1952      * @return
1953      */
1954     public static String getMetazonePageTerritory(String source) {
1955         String result = metazoneToPageTerritory.get(source);
1956         return result == null ? "ZZ" : result;
1957     }
1958 
1959     private static final List<String> COUNTS = Arrays.asList("displayName", "zero", "one", "two", "few", "many", "other", "per");
1960 
1961     private static int alphabeticCompare(String aa, String bb) {
1962         // A frozen Collator is thread-safe.
1963         return alphabetic.compare(aa, bb);
1964     }
1965 
1966     public enum BaseUrl {
1967         //http://st.unicode.org/smoketest/survey?_=af&strid=55053dffac611328
1968         //http://st.unicode.org/cldr-apps/survey?_=en&strid=3cd31261bf6738e1
1969         SMOKE("http://st.unicode.org/smoketest/survey"), PRODUCTION("http://st.unicode.org/cldr-apps/survey");
1970         final String base;
1971 
1972         private BaseUrl(String url) {
1973             base = url;
1974         }
1975     }
1976 
1977     /**
1978      * @deprecated, use CLDRConfig.urls().forPathHeader() instead.
1979      * @param baseUrl
1980      * @param locale
1981      * @return
1982      */
1983     public String getUrl(BaseUrl baseUrl, String locale) {
1984         return getUrl(baseUrl.base, locale);
1985     }
1986 
1987     /**
1988      * @deprecated, use CLDRConfig.urls().forPathHeader() instead.
1989      * @param baseUrl
1990      * @param locale
1991      * @return
1992      */
1993     public String getUrl(String baseUrl, String locale) {
1994         return getUrl(baseUrl, locale, getOriginalPath());
1995     }
1996 
1997     /**
1998      * Map http://st.unicode.org/smoketest/survey  to http://st.unicode.org/smoketest etc
1999      * @param str
2000      * @return
2001      */
2002     public static String trimLast(String str) {
2003         int n = str.lastIndexOf('/');
2004         if (n == -1) return "";
2005         return str.substring(0, n + 1);
2006     }
2007 
2008     public static String getUrlForLocalePath(String locale, String path) {
2009         return getUrl(SURVEY_URL, locale, path);
2010     }
2011 
2012     public String getUrlForLocalePath(String locale) {
2013         return getUrl(SURVEY_URL, locale, originalPath);
2014     }
2015 
2016     public static String getUrl(String baseUrl, String locale, String path) {
2017         return trimLast(baseUrl) + "v#/" + locale + "//" + StringId.getHexId(path);
2018     }
2019 
2020     // eg http://st.unicode.org/cldr-apps/survey?_=fr&x=Locale%20Name%20Patterns
2021     /**
2022      * @deprecated use CLDRConfig.urls()
2023      * @param baseUrl
2024      * @param locale
2025      * @param subsection
2026      * @return
2027      */
2028     @Deprecated
2029     public static String getPageUrl(String baseUrl, String locale, PageId subsection) {
2030         return trimLast(baseUrl) + "v#/" + locale + "/" + subsection + "/";
2031     }
2032 
2033     private static String SURVEY_URL = CLDRConfig.getInstance().getProperty("CLDR_SURVEY_URL", "http://st.unicode.org/cldr-apps/survey");
2034 
2035     public static String getLinkedView(String baseUrl, CLDRFile file, String path) {
2036         return getLinkedView(baseUrl, file.getLocaleID(), path);
2037     }
2038     /**
2039      * @deprecated use CLDRConfig.urls()
2040      * @param baseUrl
2041      * @param file
2042      * @param path
2043      * @return
2044      */
2045     @Deprecated
2046     public static String getLinkedView(String baseUrl, String localeId, String path) {
2047 //        String value = file.getStringValue(path);
2048 //        if (value == null) {
2049 //            return null;
2050 //        }
2051         return SECTION_LINK + PathHeader.getUrl(baseUrl, localeId, path) + "'><em>view</em></a>";
2052     }
2053 
2054     /**
2055      * If a subdivision, return the (uppercased) territory and if suffix != null, the suffix. Otherwise return the input as is.
2056      * @param input
2057      * @param suffix
2058      * @return
2059      */
2060     private static String getSubdivisionsTerritory(String input, Output<String> suffix) {
2061         String theTerritory;
2062         if (StandardCodes.LstrType.subdivision.isWellFormed(input)) {
2063             int territoryEnd = input.charAt(0) < 'A' ? 3 : 2;
2064             theTerritory = input.substring(0, territoryEnd).toUpperCase(Locale.ROOT);
2065             if (suffix != null) {
2066                 suffix.value = input.substring(territoryEnd);
2067             }
2068         } else {
2069             theTerritory = input;
2070             if (suffix != null) {
2071                 suffix.value = "";
2072             }
2073         }
2074         return theTerritory;
2075     }
2076 }
2077