xref: /aosp_15_r20/external/cldr/tools/cldr-code/src/main/java/org/unicode/cldr/json/LdmlConvertRules.java (revision 912701f9769bb47905792267661f0baf2b85bed5)
1 package org.unicode.cldr.json;
2 
3 import com.google.common.collect.ImmutableSet;
4 import java.util.ArrayList;
5 import java.util.HashSet;
6 import java.util.Iterator;
7 import java.util.List;
8 import java.util.Set;
9 import java.util.concurrent.atomic.AtomicInteger;
10 import java.util.regex.Matcher;
11 import java.util.regex.Pattern;
12 import org.unicode.cldr.util.Builder;
13 import org.unicode.cldr.util.CLDRFile;
14 import org.unicode.cldr.util.FileProcessor;
15 import org.unicode.cldr.util.PatternCache;
16 
17 class LdmlConvertRules {
18 
19     /** File sets that will not be processed in JSON transformation. */
20     public static final ImmutableSet<String> IGNORE_FILE_SET =
21             ImmutableSet.of(
22                     "attributeValueValidity", "coverageLevels", "postalCodeData", "subdivisions");
23 
24     /**
25      * The attribute list that should become part of the name in form of name-(attribute)-(value).
26      * [parent_element]:[element]:[attribute]
27      */
28     // common/main
29     static final ImmutableSet<String> NAME_PART_DISTINGUISHING_ATTR_SET =
30             ImmutableSet.of(
31                     "monthWidth:month:yeartype",
32                     "characters:parseLenients:scope",
33                     "dateFormat:pattern:numbers",
34                     "characterLabelPatterns:characterLabelPattern:count", // originally under
35                     // characterLabels
36                     "currencyFormats:unitPattern:count",
37                     "currency:displayName:count",
38                     "numbers:symbols:numberSystem",
39                     "numbers:decimalFormats:numberSystem",
40                     "numbers:currencyFormats:numberSystem",
41                     "numbers:percentFormats:numberSystem",
42                     "numbers:scientificFormats:numberSystem",
43                     "numbers:miscPatterns:numberSystem",
44                     "minimalPairs:pluralMinimalPairs:count",
45                     "territoryContainment:group:status",
46                     "decimalFormat:pattern:count",
47                     "currencyFormat:pattern:count",
48                     "unit:unitPattern:count",
49                     // compound units
50                     "compoundUnit:compoundUnitPattern1:count",
51                     "compoundUnit:compoundUnitPattern1:gender",
52                     "compoundUnit:compoundUnitPattern1:case",
53                     "field:relative:type",
54                     "field:relativeTime:type",
55                     "relativeTime:relativeTimePattern:count",
56                     "availableFormats:dateFormatItem:count",
57                     "listPatterns:listPattern:type",
58                     "timeZoneNames:regionFormat:type",
59                     "units:durationUnit:type",
60                     "weekData:minDays:territories",
61                     "weekData:firstDay:territories",
62                     "weekData:weekendStart:territories",
63                     "weekData:weekendEnd:territories",
64                     "supplemental:dayPeriodRuleSet:type",
65                     // units
66                     "unitPreferenceDataData:unitPreferences:category",
67                     // grammatical features
68                     // in common/supplemental/grammaticalFeatures.xml
69                     "grammaticalData:grammaticalFeatures:targets",
70                     "grammaticalGenderData:grammaticalFeatures:targets",
71                     "grammaticalFeatures:grammaticalCase:scope",
72                     "grammaticalFeatures:grammaticalGender:scope",
73                     "grammaticalDerivations:deriveCompound:structure",
74                     "grammaticalDerivations:deriveCompound:feature",
75                     "grammaticalDerivations:deriveComponent:feature",
76                     "grammaticalDerivations:deriveComponent:structure",
77                     // measurement
78                     "measurementData:measurementSystem:category",
79                     "supplemental:plurals:type",
80                     "pluralRanges:pluralRange:start",
81                     "pluralRanges:pluralRange:end",
82                     "pluralRules:pluralRule:count",
83                     "languageMatches:languageMatch:desired",
84                     "styleNames:styleName:subtype",
85                     "styleNames:styleName:alt");
86 
87     /**
88      * The set of attributes that should become part of the name in form of
89      * name-(attribute)-(value).
90      */
91 
92     /**
93      * Following is a list of element:attribute pair. These attributes should be treated as values.
94      * For example, <type type="arab" key="numbers">Arabic-Indic Digits</type> should be really
95      * converted as, "arab": { "_value": "Arabic-Indic Digits", "_key": "numbers" }
96      */
97     static final ImmutableSet<String> ATTR_AS_VALUE_SET =
98             ImmutableSet.of(
99 
100                     // in common/supplemental/dayPeriods.xml
101                     "dayPeriodRules:dayPeriodRule:from",
102 
103                     // in common/supplemental/likelySubtags.xml
104                     "likelySubtags:likelySubtag:to",
105 
106                     // in common/supplemental/metaZones.xml
107                     "timezone:usesMetazone:mzone",
108                     // Only the current usesMetazone will be kept, it is not necessary to keep
109                     // "to" and "from" attributes to make key unique. This is needed as their
110                     // value is not good if used as key.
111                     "timezone:usesMetazone:to",
112                     "timezone:usesMetazone:from",
113                     "mapTimezones:mapZone:other",
114                     "mapTimezones:mapZone:type",
115                     "mapTimezones:mapZone:territory",
116 
117                     // in common/supplemental/numberingSystems.xml
118                     "numberingSystems:numberingSystem:type",
119 
120                     // in common/supplemental/supplementalData.xml
121                     "region:currency:from",
122                     "region:currency:to",
123                     "region:currency:tender",
124                     "calendar:calendarSystem:type",
125                     "codeMappings:territoryCodes:numeric",
126                     "codeMappings:territoryCodes:alpha3",
127                     "codeMappings:currencyCodes:numeric",
128                     "timeData:hours:allowed",
129                     "timeData:hours:preferred",
130                     // common/supplemental/supplementalMetaData.xml
131                     "validity:variable:type",
132                     "deprecated:deprecatedItems:elements",
133                     "deprecated:deprecatedItems:attributes",
134                     "deprecated:deprecatedItems:type",
135 
136                     // in common/supplemental/telephoneCodeData.xml
137                     "codesByTerritory:telephoneCountryCode:code",
138 
139                     // in common/supplemental/windowsZones.xml
140                     "mapTimezones:mapZone:other",
141 
142                     // in common/supplemental/units.xml
143                     "*:unitPreference:geq",
144                     "*:unitPreference:skeleton",
145 
146                     // in common/supplemental/grammaticalFeatures.xml
147                     "grammaticalDerivations:deriveComponent:value0",
148                     "grammaticalDerivations:deriveComponent:value1",
149 
150                     // identity elements
151                     "identity:language:type",
152                     "identity:script:type",
153                     "identity:territory:type",
154                     "identity:variant:type",
155 
156                     // in common/bcp47/*.xml
157                     "keyword:key:name");
158 
159     /**
160      * The set of element:attribute pair in which the attribute should be treated as value. All the
161      * attribute here are non-distinguishing attributes.
162      */
163 
164     /**
165      * For those attributes that are treated as values, they taken the form of element_name: { ...,
166      * attribute: value, ...} This is desirable as an element may have several attributes that are
167      * treated as values. But in some cases, there is one such attribute only, and it is more
168      * desirable to convert element_name: { attribute: value} to element_name: value With a solid
169      * example, (likelySubtags:likelySubtag:to) <likelySubtag from="zh" to="zh_Hans_CN" />
170      * distinguishing attr "from" will become the key, its better to omit "to" and have this simple
171      * mapping: "zh" : "zh_Hans_CN",
172      */
173     static final ImmutableSet<String> COMPACTABLE_ATTR_AS_VALUE_SET =
174             ImmutableSet.of(
175                     // parent:element:attribute
176                     // common/main
177                     "calendars:default:choice",
178                     "dateFormats:default:choice",
179                     "months:default:choice",
180                     "monthContext:default:choice",
181                     "days:default:choice",
182                     "dayContext:default:choice",
183                     "timeFormats:default:choice",
184                     "dateTimeFormats:default:choice",
185                     "timeZoneNames:singleCountries:list",
186 
187                     // rbnf
188                     "ruleset:rbnfrule:value",
189                     // common/supplemental
190                     "likelySubtags:likelySubtag:to",
191                     // "territoryContainment:group:type",
192                     "calendar:calendarSystem:type",
193                     "calendarPreferenceData:calendarPreference:ordering",
194                     "codesByTerritory:telephoneCountryCode:code",
195 
196                     // common/collation
197                     "collations:default:choice",
198 
199                     // common/supplemental/pluralRanges.xml
200                     "pluralRanges:pluralRange:result",
201 
202                     // identity elements
203                     "identity:language:type",
204                     "identity:script:type",
205                     "identity:territory:type",
206                     "identity:variant:type",
207                     "grammaticalFeatures:grammaticalGender:values",
208                     "grammaticalFeatures:grammaticalDefiniteness:values",
209                     "grammaticalFeatures:grammaticalCase:values",
210                     "grammaticalDerivations:deriveCompound:value");
211 
212     /**
213      * The set of attributes that should be treated as value, and reduce to simple value only form.
214      */
215 
216     /** Anonymous key name. */
217     public static final String ANONYMOUS_KEY = "_";
218 
219     /**
220      * Check if the attribute should be suppressed.
221      *
222      * <p>Right now only "_q" is suppressed. In most cases array is used and there is no need for
223      * this information. In other cases, order is irrelevant.
224      *
225      * @return True if the attribute should be suppressed.
226      */
IsSuppresedAttr(String attr)227     public static boolean IsSuppresedAttr(String attr) {
228         return attr.endsWith("_q") || attr.endsWith("-q");
229     }
230 
231     /** The set of attributes that should be ignored in the conversion process. */
232     public static final ImmutableSet<String> IGNORABLE_NONDISTINGUISHING_ATTR_SET =
233             ImmutableSet.of("draft", "references", "origin");
234 
235     /**
236      * List of attributes that should be suppressed. This list comes from
237      * cldr/common/supplemental/supplementalMetadata. Each three of them is a group, they are for
238      * element, value and attribute. If the specified attribute appears in specified element with
239      * specified = value, it should be suppressed.
240      */
241     public static final String[] ATTR_SUPPRESS_LIST = {
242         // common/main
243         "dateFormat", "standard", "type",
244         "dateTimeFormat", "standard", "type",
245         "timeFormat", "standard", "type",
246         "decimalFormat", "standard", "type",
247         "percentFormat", "standard", "type",
248         "scientificFormat", "standard", "type",
249         "pattern", "standard", "type"
250     };
251 
252     /** This is a simple class to hold the splittable attribute specification. */
253     public static class SplittableAttributeSpec {
254         public String element;
255         public String attribute;
256         public String attrAsValueAfterSplit;
257 
SplittableAttributeSpec(String el, String attr, String av)258         SplittableAttributeSpec(String el, String attr, String av) {
259             element = el;
260             attribute = attr;
261             attrAsValueAfterSplit = av;
262         }
263     }
264 
265     /**
266      * List of attributes that has value that can be split. Each two of them is a group, and
267      * represent element and value. Occurrences of such match should lead to creation of multiple
268      * node. Example: <weekendStart day="thu" territories="DZ KW OM SA SD YE AF IR"/> should be
269      * treated as if following node is encountered. <weekendStart day="thu" territories="DZ"/>
270      * <weekendStart day="thu" territories="KW"/> <weekendStart day="thu" territories="OM"/>
271      * <weekendStart day="thu" territories="SA"/> <weekendStart day="thu" territories="SD"/>
272      * <weekendStart day="thu" territories="YE"/> <weekendStart day="thu" territories="AF"/>
273      * <weekendStart day="thu" territories="IR"/>
274      */
275     private static final SplittableAttributeSpec[] SPLITTABLE_ATTRS = {
276         new SplittableAttributeSpec("calendarPreference", "territories", null),
277         new SplittableAttributeSpec("pluralRanges", "locales", null),
278         new SplittableAttributeSpec("pluralRules", "locales", null),
279         new SplittableAttributeSpec("minDays", "territories", "count"),
280         new SplittableAttributeSpec("firstDay", "territories", "day"),
281         new SplittableAttributeSpec("weekendStart", "territories", "day"),
282         new SplittableAttributeSpec("weekendEnd", "territories", "day"),
283         new SplittableAttributeSpec("weekOfPreference", "locales", "ordering"),
284         new SplittableAttributeSpec("measurementSystem", "territories", "type"),
285         // this is deprecated, so no need to generalize this exception.
286         new SplittableAttributeSpec(
287                 "measurementSystem-category-temperature", "territories", "type"),
288         new SplittableAttributeSpec("paperSize", "territories", "type"),
289         new SplittableAttributeSpec("parentLocale", "locales", "parent"),
290         new SplittableAttributeSpec(
291                 "collations", "locales", "parent"), // parentLocale component=collations
292         new SplittableAttributeSpec(
293                 "plurals", "locales", "parent"), // parentLocale component=plurals
294         new SplittableAttributeSpec(
295                 "segmentations", "locales", "parent"), // parentLocale component=segmentations
296         new SplittableAttributeSpec("hours", "regions", null),
297         new SplittableAttributeSpec("dayPeriodRules", "locales", null),
298         // new SplittableAttributeSpec("group", "contains", "group"),
299         new SplittableAttributeSpec("personList", "locales", "type"),
300         new SplittableAttributeSpec("unitPreference", "regions", null),
301         new SplittableAttributeSpec("grammaticalFeatures", "locales", null),
302         new SplittableAttributeSpec("grammaticalDerivations", "locales", null),
303         // this will cause EMPTY parentLocales elements to work properly
304         new SplittableAttributeSpec("parentLocales", "component", "" /* Not null */),
305     };
306 
307     /** The set that contains all timezone type of elements. */
308     public static final Set<String> TIMEZONE_ELEMENT_NAME_SET =
309             Builder.with(new HashSet<String>())
310                     .add("zone")
311                     .add("timezone")
312                     .add("zoneItem")
313                     .add("typeMap")
314                     .freeze();
315 
316     /**
317      * There are a handful of attribute values that are more properly represented as an array of
318      * strings rather than as a single string. These are not locked to a specific element, may need
319      * to change the matching algorithm if there are conflicts.
320      */
321     public static final Set<String> ATTRVALUE_AS_ARRAY_SET =
322             Builder.with(new HashSet<String>())
323                     .add("territories")
324                     .add("scripts")
325                     .add("contains")
326                     .add("systems")
327                     .add("origin")
328                     .add("component") // for parentLocales - may need to be more specific here
329                     .add("localeRules") // for parentLocales
330                     .add("values") // for unitIdComponents - may need to be more specific here
331                     .freeze();
332 
333     /**
334      * Following is the list of elements that need to be sorted before output.
335      *
336      * <p>Time zone item is split to multiple level, and each level should be grouped together. The
337      * locale list in "dayPeriodRule" could be split to multiple items, and items for each locale
338      * should be grouped together.
339      */
340     public static final String[] ELEMENT_NEED_SORT = {
341         "zone",
342         "timezone",
343         "zoneItem",
344         "typeMap",
345         "dayPeriodRule",
346         "pluralRanges",
347         "pluralRules",
348         "personList",
349         "calendarPreferenceData",
350         "character-fallback",
351         "types",
352         "timeData",
353         "minDays",
354         "firstDay",
355         "weekendStart",
356         "weekendEnd",
357         "measurementData",
358         "measurementSystem"
359     };
360 
361     /**
362      * Some elements in CLDR has multiple children of the same type of element. We would like to
363      * treat them as array.
364      */
365     public static final Pattern ARRAY_ITEM_PATTERN =
366             PatternCache.get(
367                     "(.*/collation[^/]*/rules[^/]*/"
368                             + "|.*/character-fallback[^/]*/character[^/]*/"
369                             + "|.*/rbnfrule[^/]*/"
370                             + "|.*/ruleset[^/]*/"
371                             + "|.*/languageMatching[^/]*/languageMatches[^/]*/"
372                             + "|.*/unitPreferences/[^/]*/[^/]*/"
373                             + "|.*/windowsZones[^/]*/mapTimezones[^/]*/"
374                             + "|.*/metaZones[^/]*/mapTimezones[^/]*/"
375                             + "|.*/segmentation[^/]*/variables[^/]*/"
376                             + "|.*/segmentation[^/]*/suppressions[^/]*/"
377                             + "|.*/transform[^/]*/tRules[^/]*/"
378                             + "|.*/region/region[^/]*/"
379                             + "|.*/keyword[^/]*/key[^/]*/"
380                             + "|.*/telephoneCodeData[^/]*/codesByTerritory[^/]*/"
381                             + "|.*/metazoneInfo[^/]*/timezone\\[[^\\]]*\\]/"
382                             + "|.*/metadata[^/]*/validity[^/]*/"
383                             + "|.*/metadata[^/]*/suppress[^/]*/"
384                             + "|.*/metadata[^/]*/deprecated[^/]*/"
385                             + ")(.*)");
386 
387     /** These objects values should be output as arrays. */
388     public static final Pattern VALUE_IS_SPACESEP_ARRAY =
389             PatternCache.get(
390                     "(grammaticalCase|grammaticalGender|grammaticalDefiniteness|nameOrderLocales|component)");
391 
392     /**
393      * Indicates that the child value of this element needs to be separated into array items. For
394      * example: {@code <weekOfPreference ordering="weekOfDate weekOfMonth" locales="en bn ja ka"/>}
395      * becomes {@code {"en":["weekOfDate","weekOfMonth"],"bn":["weekOfDate","weekOfMonth"]} }
396      */
397     public static final Set<String> CHILD_VALUE_IS_SPACESEP_ARRAY =
398             ImmutableSet.of("weekOfPreference", "calendarPreferenceData");
399 
400     /**
401      * Number elements without a numbering system are there only for compatibility purposes. We
402      * automatically suppress generation of JSON objects for them.
403      */
404     public static final Pattern NO_NUMBERING_SYSTEM_PATTERN =
405             Pattern.compile(
406                     "//ldml/numbers/(symbols|(decimal|percent|scientific|currency)Formats)/.*");
407 
408     public static final Pattern NUMBERING_SYSTEM_PATTERN =
409             Pattern.compile(
410                     "//ldml/numbers/(symbols|miscPatterns|(decimal|percent|scientific|currency)Formats)\\[@numberSystem=\"([^\"]++)\"\\]/.*");
411     public static final String[] ACTIVE_NUMBERING_SYSTEM_XPATHS = {
412         "//ldml/numbers/defaultNumberingSystem",
413         "//ldml/numbers/otherNumberingSystems/native",
414         "//ldml/numbers/otherNumberingSystems/traditional",
415         "//ldml/numbers/otherNumberingSystems/finance"
416     };
417 
418     /**
419      * Root language id pattern should be discarded in all locales except root, even though the path
420      * will exist in a resolved CLDRFile.
421      */
422     public static final Pattern ROOT_IDENTITY_PATTERN =
423             Pattern.compile("//ldml/identity/language\\[@type=\"root\"\\]");
424 
425     /** A simple class to hold the specification of a path transformation. */
426     public static class PathTransformSpec {
427 
428         private final boolean DEBUG_TRANSFORMS = false;
429         public Pattern pattern;
430         public String replacement;
431         public String patternStr;
432         public String comment = "";
433         private AtomicInteger use = new AtomicInteger();
434 
PathTransformSpec(String patternStr, String replacement, String comment)435         PathTransformSpec(String patternStr, String replacement, String comment) {
436             this.patternStr = patternStr;
437             pattern = PatternCache.get(patternStr);
438             this.replacement = replacement;
439             this.comment = comment;
440             if (this.comment == null) this.comment = "";
441         }
442 
443         @Override
toString()444         public String toString() {
445             StringBuilder sb = new StringBuilder();
446             sb.append('\n')
447                     .append("# ")
448                     .append(comment.replace('\n', ' '))
449                     .append('\n')
450                     .append("< ")
451                     .append(patternStr)
452                     .append('\n')
453                     .append("> ")
454                     .append(replacement)
455                     .append('\n');
456             return sb.toString();
457         }
458 
459         /**
460          * Apply this rule to a string
461          *
462          * @param result input string
463          * @return result, or null if unchanged
464          */
apply(String result)465         public String apply(String result) {
466             Matcher m = pattern.matcher(result);
467             if (m.matches()) {
468                 final String newResult = m.replaceFirst(replacement);
469                 final int count = this.use.incrementAndGet();
470                 if (DEBUG_TRANSFORMS) {
471                     System.err.println(
472                             result
473                                     + " => "
474                                     + newResult
475                                     + " count "
476                                     + count
477                                     + " << "
478                                     + this.toString());
479                 }
480                 return newResult;
481             }
482             return null;
483         }
484 
dumpAll()485         public static void dumpAll() {
486             System.out.println("# Path Transformations");
487             for (final PathTransformSpec ts : getPathTransformations()) {
488                 System.out.append(ts.toString());
489             }
490             System.out.println();
491         }
492 
applyAll(String result)493         public static final String applyAll(String result) {
494             for (final PathTransformSpec ts : getPathTransformations()) {
495                 final String changed = ts.apply(result);
496                 if (changed != null) {
497                     result = changed;
498                     break;
499                 }
500             }
501             return result;
502         }
503     }
504 
getPathTransformations()505     public static final Iterable<PathTransformSpec> getPathTransformations() {
506         return PathTransformSpecHelper.INSTANCE;
507     }
508 
509     /**
510      * Add a path transform for the //ldml/identity/version element to the specific number
511      *
512      * @param version
513      */
addVersionHandler(String version)514     public static final void addVersionHandler(String version) {
515         if (!CLDRFile.GEN_VERSION.equals(version)) {
516             PathTransformSpecHelper.INSTANCE.prependVersionTransforms(version);
517         }
518     }
519 
520     public static final class PathTransformSpecHelper extends FileProcessor
521             implements Iterable<PathTransformSpec> {
522         static final PathTransformSpecHelper INSTANCE = make();
523 
make()524         static final PathTransformSpecHelper make() {
525             final PathTransformSpecHelper helper = new PathTransformSpecHelper();
526             helper.process(PathTransformSpecHelper.class, "pathTransforms.txt");
527             return helper;
528         }
529 
PathTransformSpecHelper()530         private PathTransformSpecHelper() {}
531 
532         private List<PathTransformSpec> data = new ArrayList<>();
533         private String lastComment = "";
534         private String lastPattern = null;
535         private String lastReplacement = null;
536 
537         @Override
handleStart()538         protected void handleStart() {
539             // Add these to the beginning because of the dynamic version
540             String version = CLDRFile.GEN_VERSION;
541             prependVersionTransforms(version);
542         }
543 
544         /**
545          * Prepend version transform. If called twice, the LAST caller will be used.
546          *
547          * @param version
548          */
prependVersionTransforms(String version)549         public void prependVersionTransforms(String version) {
550             data.add(
551                     0,
552                     new PathTransformSpec(
553                             "(.+)/identity/version\\[@number=\"([^\"]*)\"\\]",
554                             "$1" + "/identity/version\\[@cldrVersion=\"" + version + "\"\\]",
555                             "added by code"));
556             // Add cldrVersion attribute to supplemental data
557             data.add(
558                     0,
559                     new PathTransformSpec(
560                             "(.+)/version\\[@number=\"([^\"]*)\"\\]\\[@unicodeVersion=\"([^\"]*\")(\\])",
561                             "$1"
562                                     + "/version\\[@cldrVersion=\""
563                                     + version
564                                     + "\"\\]"
565                                     + "\\[@unicodeVersion=\""
566                                     + "$3"
567                                     + "\\]",
568                             "added by code"));
569         }
570 
571         @Override
handleLine(int lineCount, String line)572         protected boolean handleLine(int lineCount, String line) {
573             if (line.isEmpty()) return true;
574             if (line.startsWith("<")) {
575                 lastReplacement = null;
576                 if (lastPattern != null) {
577                     throw new IllegalArgumentException("line " + lineCount + ": two <'s in a row");
578                 }
579                 lastPattern = line.substring(1).trim();
580                 if (lastPattern.isEmpty()) {
581                     throw new IllegalArgumentException("line " + lineCount + ": empty < pattern");
582                 }
583             } else if (line.startsWith(">")) {
584                 if (lastPattern == null) {
585                     throw new IllegalArgumentException(
586                             "line " + lineCount + ": need < line before > line");
587                 }
588                 lastReplacement = line.substring(1).trim();
589                 data.add(new PathTransformSpec(lastPattern, lastReplacement, lastComment));
590                 reset();
591             }
592             return true;
593         }
594 
595         @Override
handleEnd()596         protected void handleEnd() {
597             if (lastPattern != null) {
598                 throw new IllegalArgumentException("ended with a < but no >");
599             }
600         }
601 
reset()602         private void reset() {
603             this.lastComment = "";
604             this.lastPattern = null;
605             this.lastReplacement = null;
606         }
607 
608         @Override
handleComment(String line, int commentCharPosition)609         public void handleComment(String line, int commentCharPosition) {
610             lastComment = line.substring(commentCharPosition + 1).trim();
611         }
612 
613         @Override
iterator()614         public Iterator<PathTransformSpec> iterator() {
615             return data.iterator();
616         }
617     }
618 
main(String args[])619     public static void main(String args[]) {
620         // for debugging / verification
621         PathTransformSpec.dumpAll();
622     }
623 
getKeyStr(String name, String key)624     public static final String getKeyStr(String name, String key) {
625         String keyStr2 = "*:" + name + ":" + key;
626         return keyStr2;
627     }
628 
getKeyStr(String parent, String name, String key)629     public static final String getKeyStr(String parent, String name, String key) {
630         String keyStr = parent + ":" + name + ":" + key;
631         return keyStr;
632     }
633 
getSplittableAttrs()634     public static SplittableAttributeSpec[] getSplittableAttrs() {
635         return SPLITTABLE_ATTRS;
636     }
637 
valueIsSpacesepArray(final String nodeName, String parent)638     public static final boolean valueIsSpacesepArray(final String nodeName, String parent) {
639         return VALUE_IS_SPACESEP_ARRAY.matcher(nodeName).matches()
640                 || (parent != null && CHILD_VALUE_IS_SPACESEP_ARRAY.contains(parent));
641     }
642 
643     static final Set<String> BOOLEAN_OMIT_FALSE =
644             ImmutableSet.of(
645                     // attribute names within bcp47 that are booleans, but omitted if false.
646                     "deprecated");
647 
648     // These attributes are booleans, and should be omitted if false
attrIsBooleanOmitFalse( final String fullPath, final String nodeName, final String parent, final String key)649     public static final boolean attrIsBooleanOmitFalse(
650             final String fullPath, final String nodeName, final String parent, final String key) {
651         return (fullPath != null
652                 && (fullPath.startsWith("//supplementalData/metaZones/metazoneIds")
653                         || fullPath.startsWith("//ldmlBCP47/keyword/key"))
654                 && BOOLEAN_OMIT_FALSE.contains(key));
655     }
656 }
657