xref: /aosp_15_r20/external/cldr/tools/cldr-code/src/main/java/org/unicode/cldr/util/LogicalGrouping.java (revision 912701f9769bb47905792267661f0baf2b85bed5)
1 package org.unicode.cldr.util;
2 
3 import com.google.common.collect.ImmutableList;
4 import com.google.common.collect.ImmutableSet;
5 import com.ibm.icu.text.PluralRules;
6 import com.ibm.icu.util.Output;
7 import java.util.Collection;
8 import java.util.Collections;
9 import java.util.HashSet;
10 import java.util.List;
11 import java.util.Set;
12 import java.util.TreeSet;
13 import java.util.concurrent.ConcurrentHashMap;
14 import org.unicode.cldr.util.DayPeriodInfo.DayPeriod;
15 import org.unicode.cldr.util.GrammarInfo.GrammaticalFeature;
16 import org.unicode.cldr.util.GrammarInfo.GrammaticalScope;
17 import org.unicode.cldr.util.GrammarInfo.GrammaticalTarget;
18 import org.unicode.cldr.util.PluralRulesUtil.KeywordStatus;
19 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo;
20 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo.Count;
21 import org.unicode.cldr.util.SupplementalDataInfo.PluralType;
22 
23 public class LogicalGrouping {
24 
25     static final SupplementalDataInfo supplementalData =
26             CLDRConfig.getInstance().getSupplementalDataInfo();
27 
28     public static final ImmutableSet<String> metazonesDSTSet =
29             ImmutableSet.of(
30                     "Acre",
31                     "Africa_Western",
32                     "Alaska",
33                     "Almaty",
34                     "Amazon",
35                     "America_Central",
36                     "America_Eastern",
37                     "America_Mountain",
38                     "America_Pacific",
39                     "Anadyr",
40                     "Apia",
41                     "Aqtau",
42                     "Aqtobe",
43                     "Arabian",
44                     "Argentina",
45                     "Argentina_Western",
46                     "Armenia",
47                     "Atlantic",
48                     "Australia_Central",
49                     "Australia_CentralWestern",
50                     "Australia_Eastern",
51                     "Australia_Western",
52                     "Azerbaijan",
53                     "Azores",
54                     "Bangladesh",
55                     "Brasilia",
56                     "Cape_Verde",
57                     "Chatham",
58                     "Chile",
59                     "China",
60                     "Choibalsan",
61                     "Colombia",
62                     "Cook",
63                     "Cuba",
64                     "Easter",
65                     "Europe_Central",
66                     "Europe_Eastern",
67                     "Europe_Western",
68                     "Falkland",
69                     "Fiji",
70                     "Georgia",
71                     "Greenland",
72                     "Greenland_Eastern",
73                     "Greenland_Western",
74                     "Hawaii_Aleutian",
75                     "Hong_Kong",
76                     "Hovd",
77                     "Iran",
78                     "Irkutsk",
79                     "Israel",
80                     "Japan",
81                     "Kamchatka",
82                     "Korea",
83                     "Krasnoyarsk",
84                     "Lord_Howe",
85                     "Macau",
86                     "Magadan",
87                     "Mauritius",
88                     "Mexico_Northwest",
89                     "Mexico_Pacific",
90                     "Mongolia",
91                     "Moscow",
92                     "New_Caledonia",
93                     "New_Zealand",
94                     "Newfoundland",
95                     "Norfolk",
96                     "Noronha",
97                     "Novosibirsk",
98                     "Omsk",
99                     "Pakistan",
100                     "Paraguay",
101                     "Peru",
102                     "Philippines",
103                     "Pierre_Miquelon",
104                     "Qyzylorda",
105                     "Sakhalin",
106                     "Samara",
107                     "Samoa",
108                     "Taipei",
109                     "Tonga",
110                     "Turkmenistan",
111                     "Uruguay",
112                     "Uzbekistan",
113                     "Vanuatu",
114                     "Vladivostok",
115                     "Volgograd",
116                     "Yakutsk",
117                     "Yekaterinburg");
118 
119     public static final ImmutableList<String> days =
120             ImmutableList.of("sun", "mon", "tue", "wed", "thu", "fri", "sat");
121 
122     public static final ImmutableSet<String> calendarsWith13Months =
123             ImmutableSet.of("coptic", "ethiopic", "hebrew");
124     public static final ImmutableSet<String> compactDecimalFormatLengths =
125             ImmutableSet.of("short", "long");
126     private static final ImmutableSet<String> ampm = ImmutableSet.of("am", "pm");
127     private static final ImmutableSet<String> nowUnits =
128             ImmutableSet.of(
129                     "second",
130                     "second-short",
131                     "second-narrow",
132                     "minute",
133                     "minute-short",
134                     "minute-narrow",
135                     "hour",
136                     "hour-short",
137                     "hour-narrow");
138 
139     /** Cache from path (String) to logical group (Set<String>) */
140     private static final ConcurrentHashMap<String, Set<String>> cachePathToLogicalGroup =
141             new ConcurrentHashMap<>();
142 
143     /** Cache from locale and path (<Pair<String, String>), to logical group (Set<String>) */
144     private static ConcurrentHashMap<Pair<String, String>, Set<String>>
145             cacheLocaleAndPathToLogicalGroup = new ConcurrentHashMap<>();
146 
147     /**
148      * Statistics on occurrences of types of logical groups, for performance testing, debugging.
149      * GET_TYPE_COUNTS should be false for production to maximize performance.
150      */
151     public static final boolean GET_TYPE_COUNTS = false;
152 
153     public static final ConcurrentHashMap<String, Long> typeCount =
154             GET_TYPE_COUNTS ? new ConcurrentHashMap<>() : null;
155 
156     /**
157      * GET_TYPE_FROM_PARTS is more elegant when true, but performance is a little faster when it's
158      * false. This might change if XPathParts.getInstance and/or XPathParts.set are made faster.
159      */
160     private static final boolean GET_TYPE_FROM_PARTS = false;
161 
162     /**
163      * Return a sorted set of paths that are in the same logical set as the given path
164      *
165      * @param cldrFile the CLDRFile
166      * @param path the distinguishing xpath
167      * @param pathTypeOut if not null, gets filled in with the PathType
168      * @return the set of paths, or null (to be treated as equivalent to empty set)
169      *     <p>For example, given the path
170      *     <p>//ldml/dates/calendars/calendar[@type="gregorian"]/quarters/quarterContext[@type="format"]/quarterWidth[@type="abbreviated"]/quarter[@type="1"]
171      *     <p>return the set of four paths
172      *     <p>//ldml/dates/calendars/calendar[@type="gregorian"]/quarters/quarterContext[@type="format"]/quarterWidth[@type="abbreviated"]/quarter[@type="1"]
173      *     //ldml/dates/calendars/calendar[@type="gregorian"]/quarters/quarterContext[@type="format"]/quarterWidth[@type="abbreviated"]/quarter[@type="2"]
174      *     //ldml/dates/calendars/calendar[@type="gregorian"]/quarters/quarterContext[@type="format"]/quarterWidth[@type="abbreviated"]/quarter[@type="3"]
175      *     //ldml/dates/calendars/calendar[@type="gregorian"]/quarters/quarterContext[@type="format"]/quarterWidth[@type="abbreviated"]/quarter[@type="4"]
176      *     <p>Caches: Most of the calculations are independent of the locale, and can be cached on a
177      *     static basis. The paths that are locale-dependent are /dayPeriods and @count. Those can
178      *     be computed on a per-locale basis; and cached (they are shared across a number of
179      *     locales).
180      */
getPaths( CLDRFile cldrFile, String path, Output<PathType> pathTypeOut)181     public static Set<String> getPaths(
182             CLDRFile cldrFile, String path, Output<PathType> pathTypeOut) {
183         if (path == null) {
184             return null; // return null for null path
185         }
186         XPathParts parts = null;
187         PathType pathType = null;
188         if (GET_TYPE_FROM_PARTS) {
189             parts = XPathParts.getFrozenInstance(path);
190             pathType = PathType.getPathTypeFromParts(parts);
191         } else {
192             /*
193              * XPathParts.set is expensive, so avoid it (not needed for singletons) if !GET_TYPE_FROM_PARTS
194              */
195             pathType = PathType.getPathTypeFromPath(path);
196         }
197         if (pathTypeOut != null) {
198             pathTypeOut.value = pathType;
199         }
200 
201         if (GET_TYPE_COUNTS) {
202             typeCount.compute(pathType.toString(), (k, v) -> (v == null) ? 1 : v + 1);
203         }
204 
205         if (pathType == PathType.SINGLETON) {
206             /*
207              * Skip cache for PathType.SINGLETON and simply return a set of one.
208              */
209             Set<String> set = new TreeSet<>();
210             set.add(path);
211             return set;
212         }
213 
214         if (!GET_TYPE_FROM_PARTS) {
215             parts = XPathParts.getFrozenInstance(path).cloneAsThawed();
216         } else {
217             parts = parts.cloneAsThawed();
218         }
219 
220         if (PathType.isLocaleDependent(pathType)) {
221             String locale = cldrFile.getLocaleID();
222             Pair<String, String> key = new Pair<>(locale, path);
223             if (cacheLocaleAndPathToLogicalGroup.containsKey(key)) {
224                 return new TreeSet<>(cacheLocaleAndPathToLogicalGroup.get(key));
225             }
226             Set<String> set = new TreeSet<>();
227             pathType.addPaths(set, cldrFile, path, parts);
228             cacheLocaleAndPathToLogicalGroup.put(key, set);
229             return set;
230         } else {
231             /*
232              * All other paths are locale-independent.
233              */
234             if (cachePathToLogicalGroup.containsKey(path)) {
235                 return new TreeSet<>(cachePathToLogicalGroup.get(path));
236             }
237             Set<String> set = new TreeSet<>();
238             pathType.addPaths(set, cldrFile, path, parts);
239             cachePathToLogicalGroup.compute(
240                     path,
241                     (pathKey, cachedPaths) -> {
242                         if (cachedPaths == null) {
243                             return Collections.synchronizedSet(new HashSet<>(set));
244                         } else {
245                             cachedPaths.addAll(set);
246                             return cachedPaths;
247                         }
248                     });
249             return set;
250         }
251     }
252 
getPaths(CLDRFile cldrFile, String path)253     public static Set<String> getPaths(CLDRFile cldrFile, String path) {
254         return getPaths(cldrFile, path, null);
255     }
256 
257     /** Returns the plural info for a given locale. */
getPluralInfo(CLDRFile cldrFile)258     private static PluralInfo getPluralInfo(CLDRFile cldrFile) {
259         return supplementalData.getPlurals(PluralType.cardinal, cldrFile.getLocaleID());
260     }
261 
262     /**
263      * @param cldrFile
264      * @param path
265      * @return true if the specified path is optional in the logical grouping that it belongs to.
266      */
isOptional(CLDRFile cldrFile, String path)267     public static boolean isOptional(CLDRFile cldrFile, String path) {
268         XPathParts parts = XPathParts.getFrozenInstance(path);
269 
270         if (parts.containsElement("relative")) {
271             String fieldType = parts.findAttributeValue("field", "type");
272             String relativeType = parts.findAttributeValue("relative", "type");
273             Integer relativeValue = relativeType == null ? 999 : Integer.parseInt(relativeType);
274             if (fieldType != null
275                     && fieldType.startsWith("day")
276                     && Math.abs(relativeValue.intValue()) >= 2) {
277                 return true; // relative days +2 +3 -2 -3 are optional in a logical group.
278             }
279         }
280         // Paths with count="(zero|one)" are optional if their usage is covered
281         // fully by paths with count="(0|1)", which are always optional themselves.
282         if (!path.contains("[@count=")) return false;
283         String pluralType = parts.getAttributeValue(-1, "count");
284         switch (pluralType) {
285             case "0":
286             case "1":
287                 return true;
288             case "zero":
289             case "one":
290                 break; // continue
291             default:
292                 return false;
293         }
294 
295         parts = parts.cloneAsThawed();
296         PluralRules pluralRules = getPluralInfo(cldrFile).getPluralRules();
297         parts.setAttribute(-1, "count", "0");
298         Set<Double> explicits = new HashSet<>();
299         if (cldrFile.isHere(parts.toString())) {
300             explicits.add(0.0);
301         }
302         parts.setAttribute(-1, "count", "1");
303         if (cldrFile.isHere(parts.toString())) {
304             explicits.add(1.0);
305         }
306         if (!explicits.isEmpty()) {
307             // HACK: The com.ibm.icu.text prefix is needed so that ST can find it
308             // (no idea why).
309             KeywordStatus status =
310                     org.unicode.cldr.util.PluralRulesUtil.getKeywordStatus(
311                             pluralRules, pluralType, 0, explicits, true);
312             if (status == KeywordStatus.SUPPRESSED) {
313                 return true;
314             }
315         }
316         return false;
317     }
318 
removeOptionalPaths(Set<String> grouping, CLDRFile cldrFile)319     public static void removeOptionalPaths(Set<String> grouping, CLDRFile cldrFile) {
320         Set<String> grouping2 = new HashSet<>(grouping);
321         for (String p : grouping2) {
322             if (LogicalGrouping.isOptional(cldrFile, p)) {
323                 grouping.remove(p);
324             }
325         }
326     }
327 
328     /** Path types for logical groupings */
329     public enum PathType {
330         SINGLETON { // no logical groups for singleton paths
331             @Override
332             @SuppressWarnings("unused")
addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts)333             void addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts) {
334                 // Do nothing. This function won't be called.
335             }
336         },
337         METAZONE {
338             @Override
339             @SuppressWarnings("unused")
addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts)340             void addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts) {
341                 String metazoneName = parts.getAttributeValue(3, "type");
342                 if (metazonesDSTSet.contains(metazoneName)) {
343                     for (String str : ImmutableSet.of("generic", "standard", "daylight")) {
344                         set.add(path.substring(0, path.lastIndexOf('/') + 1) + str);
345                     }
346                 }
347             }
348         },
349         DAYS {
350             @Override
351             @SuppressWarnings("unused")
addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts)352             void addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts) {
353                 String dayName = parts.size() > 7 ? parts.getAttributeValue(7, "type") : null;
354                 // This is just a quick check to make sure the path is good.
355                 if (dayName != null && days.contains(dayName)) {
356                     for (String str : days) {
357                         parts.setAttribute("day", "type", str);
358                         set.add(parts.toString());
359                     }
360                 }
361             }
362         },
363         DAY_PERIODS {
364             @Override
addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts)365             void addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts) {
366                 if (parts.containsElement("alias")) {
367                     set.add(path);
368                 } else {
369                     String dayPeriodType = parts.findAttributeValue("dayPeriod", "type");
370                     if (ampm.contains(dayPeriodType)) {
371                         for (String s : ampm) {
372                             parts.setAttribute("dayPeriod", "type", s);
373                             set.add(parts.toString());
374                         }
375                     } else {
376                         DayPeriodInfo.Type dayPeriodContext =
377                                 DayPeriodInfo.Type.fromString(
378                                         parts.findAttributeValue("dayPeriodContext", "type"));
379                         DayPeriodInfo dpi =
380                                 supplementalData.getDayPeriods(
381                                         dayPeriodContext, cldrFile.getLocaleID());
382                         List<DayPeriod> dayPeriods = dpi.getPeriods();
383                         DayPeriod thisDayPeriod = DayPeriod.fromString(dayPeriodType);
384                         if (dayPeriods.contains(thisDayPeriod)) {
385                             for (DayPeriod d : dayPeriods) {
386                                 parts.setAttribute("dayPeriod", "type", d.name());
387                                 set.add(parts.toString());
388                             }
389                         }
390                     }
391                 }
392             }
393         },
394         QUARTERS {
395             @Override
396             @SuppressWarnings("unused")
addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts)397             void addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts) {
398                 String quarterName = parts.size() > 7 ? parts.getAttributeValue(7, "type") : null;
399                 Integer quarter = quarterName == null ? 0 : Integer.parseInt(quarterName);
400                 if (quarter > 0
401                         && quarter
402                                 <= 4) { // This is just a quick check to make sure the path is good.
403                     for (Integer i = 1; i <= 4; i++) {
404                         parts.setAttribute("quarter", "type", i.toString());
405                         set.add(parts.toString());
406                     }
407                 }
408             }
409         },
410         MONTHS {
411             @Override
412             @SuppressWarnings("unused")
addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts)413             void addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts) {
414                 String calType = parts.size() > 3 ? parts.getAttributeValue(3, "type") : null;
415                 String monthName = parts.size() > 7 ? parts.getAttributeValue(7, "type") : null;
416                 Integer month = monthName == null ? 0 : Integer.parseInt(monthName);
417                 int calendarMonthMax = calendarsWith13Months.contains(calType) ? 13 : 12;
418                 if (month > 0
419                         && month <= calendarMonthMax) { // This is just a quick check to make sure
420                     // the path is good.
421                     for (Integer i = 1; i <= calendarMonthMax; i++) {
422                         parts.setAttribute("month", "type", i.toString());
423                         if ("hebrew".equals(calType)) {
424                             parts.removeAttribute("month", "yeartype");
425                         }
426                         set.add(parts.toString());
427                     }
428                     if ("hebrew".equals(calType)) { // Add extra hebrew calendar leap month
429                         parts.setAttribute("month", "type", Integer.toString(7));
430                         parts.setAttribute("month", "yeartype", "leap");
431                         set.add(parts.toString());
432                     }
433                 }
434             }
435         },
436         RELATIVE {
437             @Override
438             @SuppressWarnings("unused")
addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts)439             void addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts) {
440                 String fieldType = parts.findAttributeValue("field", "type");
441                 String relativeType = parts.findAttributeValue("relative", "type");
442                 Integer relativeValue = relativeType == null ? 999 : Integer.parseInt(relativeType);
443                 if (relativeValue >= -3
444                         && relativeValue
445                                 <= 3) { // This is just a quick check to make sure the path is good.
446                     if (!(nowUnits.contains(fieldType)
447                             && relativeValue
448                                     == 0)) { // Workaround for "now", "this hour", "this minute"
449                         int limit = 1;
450                         if (fieldType != null && fieldType.startsWith("day")) {
451                             limit = 3;
452                         }
453                         for (Integer i = -1 * limit; i <= limit; i++) {
454                             parts.setAttribute("relative", "type", i.toString());
455                             set.add(parts.toString());
456                         }
457                     }
458                 }
459             }
460         },
461         DECIMAL_FORMAT_LENGTH {
462             @Override
463             @SuppressWarnings("unused")
addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts)464             void addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts) {
465                 PluralInfo pluralInfo = getPluralInfo(cldrFile);
466                 Set<Count> pluralTypes = pluralInfo.getCounts();
467                 String decimalFormatLengthType =
468                         parts.size() > 3 ? parts.getAttributeValue(3, "type") : null;
469                 String decimalFormatPatternType =
470                         parts.size() > 5 ? parts.getAttributeValue(5, "type") : null;
471                 if (decimalFormatLengthType != null
472                         && decimalFormatPatternType != null
473                         && compactDecimalFormatLengths.contains(decimalFormatLengthType)) {
474                     int numZeroes = decimalFormatPatternType.length() - 1;
475                     int baseZeroes = (numZeroes / 3) * 3;
476                     for (int i = 0; i < 3; i++) {
477                         // This gives us "baseZeroes+i" zeroes at the end.
478                         String patType =
479                                 "1" + String.format(String.format("%%0%dd", baseZeroes + i), 0);
480                         parts.setAttribute(5, "type", patType);
481                         for (Count count : pluralTypes) {
482                             parts.setAttribute(5, "count", count.toString());
483                             set.add(parts.toString());
484                         }
485                     }
486                 }
487             }
488         },
489         COUNT {
490             @Override
491             @SuppressWarnings("unused")
addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts)492             void addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts) {
493                 addCaseOnly(set, cldrFile, parts);
494             }
495         },
496         COUNT_CASE {
497             @Override
498             @SuppressWarnings("unused")
addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts)499             void addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts) {
500                 if (!GrammarInfo.getGrammarLocales().contains(cldrFile.getLocaleID())) {
501                     addCaseOnly(set, cldrFile, parts);
502                     return;
503                 }
504                 GrammarInfo grammarInfo = supplementalData.getGrammarInfo(cldrFile.getLocaleID());
505                 if (grammarInfo == null
506                         || (parts.getElement(3).equals("unitLength")
507                                 && GrammarInfo.getUnitsToAddGrammar()
508                                         .contains(parts.getAttributeValue(3, "type")))) {
509                     addCaseOnly(set, cldrFile, parts);
510                     return;
511                 }
512                 Set<Count> pluralTypes = getPluralInfo(cldrFile).getCounts();
513                 Collection<String> rawCases =
514                         grammarInfo.get(
515                                 GrammaticalTarget.nominal,
516                                 GrammaticalFeature.grammaticalCase,
517                                 GrammaticalScope.units);
518                 setGrammarAttributes(set, parts, pluralTypes, rawCases, null);
519             }
520         },
521         COUNT_CASE_GENDER {
522             @Override
523             @SuppressWarnings("unused")
addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts)524             void addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts) {
525                 if (!GrammarInfo.getGrammarLocales().contains(cldrFile.getLocaleID())) {
526                     addCaseOnly(set, cldrFile, parts);
527                     return;
528                 }
529                 GrammarInfo grammarInfo = supplementalData.getGrammarInfo(cldrFile.getLocaleID());
530                 if (grammarInfo == null) {
531                     addCaseOnly(set, cldrFile, parts);
532                     return;
533                 }
534                 Set<Count> pluralTypes = getPluralInfo(cldrFile).getCounts();
535                 Collection<String> rawCases =
536                         grammarInfo.get(
537                                 GrammaticalTarget.nominal,
538                                 GrammaticalFeature.grammaticalCase,
539                                 GrammaticalScope.units);
540                 Collection<String> rawGenders =
541                         grammarInfo.get(
542                                 GrammaticalTarget.nominal,
543                                 GrammaticalFeature.grammaticalGender,
544                                 GrammaticalScope.units);
545                 setGrammarAttributes(set, parts, pluralTypes, rawCases, rawGenders);
546             }
547         };
548 
addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts)549         abstract void addPaths(Set<String> set, CLDRFile cldrFile, String path, XPathParts parts);
550 
addCaseOnly(Set<String> set, CLDRFile cldrFile, XPathParts parts)551         public void addCaseOnly(Set<String> set, CLDRFile cldrFile, XPathParts parts) {
552             Set<Count> pluralTypes = getPluralInfo(cldrFile).getCounts();
553             for (Count count : pluralTypes) {
554                 parts.setAttribute(-1, "count", count.toString());
555                 set.add(parts.toString());
556             }
557         }
558 
setGrammarAttributes( Set<String> set, XPathParts parts, Set<Count> pluralTypes, Collection<String> rawCases, Collection<String> rawGenders)559         public void setGrammarAttributes(
560                 Set<String> set,
561                 XPathParts parts,
562                 Set<Count> pluralTypes,
563                 Collection<String> rawCases,
564                 Collection<String> rawGenders) {
565             final String defaultGender =
566                     GrammaticalFeature.grammaticalGender.getDefault(rawGenders);
567             final String defaultCase = GrammaticalFeature.grammaticalCase.getDefault(rawCases);
568 
569             if (rawCases == null || rawCases.isEmpty()) {
570                 rawCases = Collections.singleton(defaultCase);
571             }
572             if (rawGenders == null || rawGenders.isEmpty()) {
573                 rawGenders = Collections.singleton(defaultGender);
574             }
575             for (String gender : rawGenders) {
576                 if (gender.equals(defaultGender)) {
577                     gender = null;
578                 }
579                 for (String case1 : rawCases) {
580                     if (case1.equals(defaultCase)) {
581                         case1 = null;
582                     }
583                     for (Count count : pluralTypes) {
584                         parts.setAttribute(-1, "gender", gender);
585                         parts.setAttribute(-1, "count", count.toString());
586                         parts.setAttribute(-1, "case", case1);
587                         set.add(parts.toString());
588                     }
589                 }
590             }
591         }
592 
593         /**
594          * Is the given PathType locale-dependent (for caching)?
595          *
596          * @param pathType the PathType
597          * @return the boolean
598          */
isLocaleDependent(PathType pathType)599         private static boolean isLocaleDependent(PathType pathType) {
600             /*
601              * The paths that are locale-dependent are @count and /dayPeriods.
602              */
603             return (pathType == COUNT
604                     || pathType == DAY_PERIODS
605                     || pathType.equals(COUNT_CASE)
606                     || pathType.equals(COUNT_CASE_GENDER));
607         }
608 
609         /**
610          * Get the PathType from the given path
611          *
612          * @param path the path
613          * @return the PathType
614          *     <p>Note: it would be more elegant and cleaner, but slower, if we used XPathParts to
615          *     determine the PathType. We avoid that since XPathParts.set is a performance hot spot.
616          *     (NOTE: don't know if the preceding is true anymore.)
617          */
getPathTypeFromPath(String path)618         public static PathType getPathTypeFromPath(String path) {
619             /*
620              * Would changing the order of these tests ever change the return value?
621              * Assume it could if in doubt.
622              */
623             if (path.indexOf("/metazone") > 0) {
624                 return PathType.METAZONE;
625             }
626             if (path.indexOf("/days") > 0) {
627                 return PathType.DAYS;
628             }
629             if (path.indexOf("/dayPeriods") > 0) {
630                 return PathType.DAY_PERIODS;
631             }
632             if (path.indexOf("/quarters") > 0) {
633                 return PathType.QUARTERS;
634             }
635             if (path.indexOf("/months") > 0) {
636                 return PathType.MONTHS;
637             }
638             if (path.indexOf("/relative[") > 0) {
639                 /*
640                  * include "[" in "/relative[" to avoid matching "/relativeTime" or "/relativeTimePattern".
641                  */
642                 return PathType.RELATIVE;
643             }
644             if (path.indexOf("/decimalFormatLength") > 0) {
645                 return PathType.DECIMAL_FORMAT_LENGTH;
646             }
647             if (path.indexOf("/unitLength[@type=\"long\"]") > 0) {
648                 if (path.indexOf("compoundUnitPattern1") > 0) {
649                     return PathType.COUNT_CASE_GENDER;
650                 }
651                 if (path.indexOf("/unitPattern[") > 0) {
652                     return PathType.COUNT_CASE;
653                 }
654             }
655             if (path.indexOf("[@count=") > 0) {
656                 return PathType.COUNT;
657             }
658             return PathType.SINGLETON;
659         }
660 
661         /**
662          * Get the PathType from the given XPathParts
663          *
664          * @param parts the XPathParts
665          * @return the PathType
666          * @deprecated
667          */
668         @Deprecated
getPathTypeFromParts(XPathParts parts)669         private static PathType getPathTypeFromParts(XPathParts parts) {
670             if (true) {
671                 throw new UnsupportedOperationException(
672                         "Code not updated. We may want to try using XPathParts in a future optimization, so leaving for now.");
673             }
674             /*
675              * Would changing the order of these tests ever change the return value?
676              * Assume it could if in doubt.
677              */
678             if (parts.containsElement("metazone")) {
679                 return PathType.METAZONE;
680             }
681             if (parts.containsElement("days")) {
682                 return PathType.DAYS;
683             }
684             if (parts.containsElement("dayPeriods")) {
685                 return PathType.DAY_PERIODS;
686             }
687             if (parts.containsElement("quarters")) {
688                 return PathType.QUARTERS;
689             }
690             if (parts.containsElement("months")) {
691                 return PathType.MONTHS;
692             }
693             if (parts.containsElement("relative")) {
694                 return PathType.RELATIVE;
695             }
696             if (parts.containsElement("decimalFormatLength")) {
697                 return PathType.DECIMAL_FORMAT_LENGTH;
698             }
699             if (parts.containsAttribute("count")) { // containsAttribute not containsElement
700                 return PathType.COUNT;
701             }
702             return PathType.SINGLETON;
703         }
704     }
705 }
706