• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.unicode.cldr.unittest;
2 
3 import com.google.common.base.Objects;
4 import com.google.common.base.Splitter;
5 import com.google.common.collect.ImmutableSet;
6 import com.google.common.collect.Multimap;
7 import com.google.common.collect.TreeMultimap;
8 import com.ibm.icu.impl.Row.R2;
9 import com.ibm.icu.text.UnicodeSet;
10 import com.ibm.icu.util.ULocale;
11 import java.io.File;
12 import java.util.ArrayList;
13 import java.util.Arrays;
14 import java.util.Collection;
15 import java.util.Collections;
16 import java.util.EnumSet;
17 import java.util.HashSet;
18 import java.util.LinkedHashMap;
19 import java.util.LinkedHashSet;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Map.Entry;
23 import java.util.Set;
24 import java.util.TreeMap;
25 import java.util.TreeSet;
26 import java.util.stream.Collectors;
27 import org.unicode.cldr.util.CLDRConfig;
28 import org.unicode.cldr.util.CLDRPaths;
29 import org.unicode.cldr.util.CldrUtility;
30 import org.unicode.cldr.util.LanguageTagCanonicalizer;
31 import org.unicode.cldr.util.LanguageTagParser;
32 import org.unicode.cldr.util.LocaleNames;
33 import org.unicode.cldr.util.StandardCodes;
34 import org.unicode.cldr.util.StandardCodes.LstrField;
35 import org.unicode.cldr.util.StandardCodes.LstrType;
36 import org.unicode.cldr.util.TestCLDRPaths;
37 import org.unicode.cldr.util.TransliteratorUtilities;
38 import org.unicode.cldr.util.Units;
39 import org.unicode.cldr.util.Validity;
40 import org.unicode.cldr.util.Validity.Status;
41 
42 public class TestValidity extends TestFmwkPlus {
43 
44     private boolean DEBUG = false;
45 
main(String[] args)46     public static void main(String[] args) {
47         new TestValidity().run(args);
48     }
49 
50     Validity validity = Validity.getInstance();
51 
TestBasicValidity()52     public void TestBasicValidity() {
53         Object[][] tests = {
54             {LstrType.language, Validity.Status.regular, true, "aa", "en"},
55             {LstrType.language, null, false, "eng"}, // null means never found under any status
56             {LstrType.language, null, false, LocaleNames.ROOT},
57             {LstrType.language, Validity.Status.special, true, LocaleNames.MUL},
58             {LstrType.language, Validity.Status.deprecated, true, "aju"},
59             {LstrType.language, Validity.Status.reserved, true, "qaa", "qfy"},
60             {LstrType.language, Validity.Status.private_use, true, "qfz"},
61             {LstrType.language, Validity.Status.unknown, true, LocaleNames.UND},
62             {LstrType.script, Validity.Status.reserved, true, "Qaaa", "Qaap"},
63             {LstrType.script, Validity.Status.private_use, true, "Qaaq", "Qabx"},
64             {LstrType.script, Validity.Status.special, true, "Zinh"},
65             {LstrType.script, Validity.Status.special, true, "Zmth"},
66             {LstrType.script, Validity.Status.special, true, "Zsye"},
67             {LstrType.script, Validity.Status.special, true, "Zsym"},
68             {LstrType.script, Validity.Status.special, true, "Zxxx"},
69             {LstrType.script, Validity.Status.special, true, "Zyyy"},
70             {LstrType.script, Validity.Status.unknown, true, "Zzzz"},
71             {LstrType.region, Validity.Status.deprecated, true, "QU"},
72             {LstrType.region, Validity.Status.macroregion, true, "EU"},
73             {LstrType.region, Validity.Status.regular, true, "XK"},
74             {LstrType.region, Validity.Status.macroregion, true, "001"},
75             {LstrType.region, Validity.Status.reserved, true, "AA", "QM", "QZ"},
76             {LstrType.region, Validity.Status.private_use, true, "XC", "XZ"},
77             {LstrType.region, Validity.Status.unknown, true, "ZZ"},
78             {LstrType.subdivision, Validity.Status.unknown, true, "kzzzzz"},
79             {LstrType.subdivision, Validity.Status.regular, true, "usca"},
80             {LstrType.subdivision, Validity.Status.deprecated, true, "albr"},
81             {LstrType.currency, Validity.Status.regular, true, "USD"},
82             {LstrType.currency, Validity.Status.unknown, true, "XXX"},
83             {LstrType.currency, Validity.Status.deprecated, true, "ADP"},
84             {LstrType.unit, Validity.Status.regular, true, "area-acre"},
85         };
86         for (Object[] test : tests) {
87             LstrType lstr = (LstrType) test[0];
88             Validity.Status subtypeRaw = (Validity.Status) test[1];
89             Boolean desired = (Boolean) test[2];
90             for (int i = 3; i < test.length; ++i) {
91                 String code = (String) test[i];
92                 List<Status> subtypes =
93                         subtypeRaw == null
94                                 ? Arrays.asList(Status.values())
95                                 : Collections.singletonList(subtypeRaw);
96                 for (Status subtype : subtypes) {
97                     Set<String> actual = validity.getStatusToCodes(lstr).get(subtype);
98                     if (!assertRelation(
99                             "Validity",
100                             desired,
101                             CldrUtility.ifNull(actual, Collections.EMPTY_SET),
102                             TestFmwkPlus.CONTAINS,
103                             code)) {
104                         int debug = 0;
105                     }
106                 }
107             }
108         }
109         if (isVerbose()) {
110 
111             for (LstrType lstrType : LstrType.values()) {
112                 logln(lstrType.toString());
113                 final Map<Status, Set<String>> statusToCodes = validity.getStatusToCodes(lstrType);
114                 for (Entry<Validity.Status, Set<String>> entry2 : statusToCodes.entrySet()) {
115                     logln("\t" + entry2.getKey());
116                     logln("\t\t" + entry2.getValue());
117                 }
118             }
119         }
120     }
121 
122     static final Set<String> ALLOWED_UNDELETIONS =
123             ImmutableSet.of(
124                     "ug331",
125                     "nlbq1",
126                     "nlbq2",
127                     "nlbq3",
128                     "no21",
129                     "no22",
130                     // 2022
131                     "no",
132                     "escn",
133                     "gbeng",
134                     "gbnir",
135                     "gbsct",
136                     "gbwls",
137                     "itgo",
138                     "itpn",
139                     "itts",
140                     "itud",
141                     "SLE",
142                     // 2024
143                     "dzd",
144                     "knn");
145     static final Set<String> ALLOWED_MISSING =
146             ImmutableSet.of(LocaleNames.ROOT, "POSIX", "REVISED", "SAAHO");
147     static final Set<String> ALLOWED_REGULAR_TO_SPECIAL = ImmutableSet.of("Zanb", "Zinh", "Zyyy");
148 
TestCompatibility()149     public void TestCompatibility() {
150         if (!TestCLDRPaths.canUseArchiveDirectory()) {
151             return; // skipped
152         }
153 
154         Set<String> messages = new HashSet<>();
155         File archive = new File(CLDRPaths.ARCHIVE_DIRECTORY);
156         for (File cldrArchive : archive.listFiles()) {
157             if (!cldrArchive.getName().startsWith("cldr-")) {
158                 continue;
159             }
160             File oldValidityLocation =
161                     new File(
162                             cldrArchive,
163                             File.separator
164                                     + "common"
165                                     + File.separator
166                                     + "validity"
167                                     + File.separator);
168             if (!oldValidityLocation.exists()) {
169                 logln("Skipping " + oldValidityLocation);
170                 continue;
171             }
172             logln("Checking " + oldValidityLocation.toString());
173             //            final String oldValidityLocation = CLDRPaths.ARCHIVE_DIRECTORY + "cldr-" +
174             // ToolConstants.PREVIOUS_CHART_VERSION +
175             //                File.separator + "common" + File.separator + "validity" +
176             // File.separator;
177             Validity oldValidity =
178                     Validity.getInstance(oldValidityLocation.toString() + File.separator);
179 
180             for (LstrType type : LstrType.values()) {
181                 final Map<Status, Set<String>> statusToCodes = oldValidity.getStatusToCodes(type);
182                 if (statusToCodes == null) {
183                     logln("validity data unavailable: " + type);
184                     continue;
185                 }
186                 for (Entry<Status, Set<String>> e2 : statusToCodes.entrySet()) {
187                     Status oldStatus = e2.getKey();
188                     for (String code : e2.getValue()) {
189                         Status newStatus = getNewStatus(type, code);
190                         if (oldStatus == newStatus) {
191                             continue;
192                         }
193 
194                         if (newStatus == null) {
195                             if (ALLOWED_MISSING.contains(code)) {
196                                 continue;
197                             }
198                             errln(
199                                     messages,
200                                     type
201                                             + ":"
202                                             + code
203                                             + ":"
204                                             + oldStatus
205                                             + " => "
206                                             + newStatus
207                                             + " — missing in new data vs. "
208                                             + cldrArchive.getName());
209                         }
210 
211                         if (oldStatus == Status.private_use && newStatus == Status.special) {
212                             logln(
213                                     messages,
214                                     "OK: " + type + ":" + code + " was " + oldStatus + " => "
215                                             + newStatus);
216                             continue;
217                         }
218                         if (oldStatus == Status.special && newStatus == Status.unknown) {
219                             if (type == LstrType.subdivision && code.endsWith("zzzz")) {
220                                 continue;
221                             }
222                             logln(
223                                     messages,
224                                     "OK: " + type + ":" + code + " was " + oldStatus + " => "
225                                             + newStatus);
226                             continue;
227                         }
228                         if (oldStatus == Status.regular) {
229                             if (newStatus == Status.deprecated) {
230                                 //                                logln(messages, "OK: " + type +
231                                 // ":" + code + " was " + oldStatus + " => " + newStatus);
232                                 continue;
233                             } else if (newStatus == Status.special
234                                     && ALLOWED_REGULAR_TO_SPECIAL.contains(code)) {
235                                 //                              logln(messages, "OK: " + type + ":"
236                                 // + code + " was " + oldStatus + " => " + newStatus);
237                                 continue;
238                             }
239                             errln(
240                                     messages,
241                                     type
242                                             + ":"
243                                             + code
244                                             + ":"
245                                             + oldStatus
246                                             + " => "
247                                             + newStatus
248                                             + " — regular item changed, and didn't become deprecated");
249                         }
250                         if (oldStatus == Status.deprecated) {
251                             if (ALLOWED_UNDELETIONS.contains(code)) {
252                                 continue;
253                             }
254                             errln(
255                                     messages,
256                                     type
257                                             + ":"
258                                             + code
259                                             + ":"
260                                             + oldStatus
261                                             + " => "
262                                             + newStatus
263                                             + " // add to exception list (ALLOWED_UNDELETIONS) if really un-deprecated");
264                         } else if (oldStatus == Status.private_use && newStatus == Status.regular) {
265                             //                          logln(messages, "OK: " + type + ":" + code +
266                             // " was " + oldStatus + " => " + newStatus);
267                         } else if (oldStatus == Status.deprecated) {
268                             errln(
269                                     messages,
270                                     type + ":" + code + " was " + oldStatus + " => " + newStatus);
271                         }
272                     }
273                 }
274             }
275         }
276     }
277 
logln(Set<String> messages, String string)278     private void logln(Set<String> messages, String string) {
279         if (!messages.contains(string)) {
280             logln(string);
281             messages.add(string);
282         }
283     }
284 
errln(Set<String> messages, String string)285     private void errln(Set<String> messages, String string) {
286         if (!messages.contains(string)) {
287             errln(string);
288             messages.add(string);
289         }
290     }
291 
getNewStatus(LstrType type, String code)292     private Status getNewStatus(LstrType type, String code) {
293         Map<Status, Set<String>> info = validity.getStatusToCodes(type);
294         for (Entry<Status, Set<String>> e : info.entrySet()) {
295             if (e.getValue().contains(code)) {
296                 return e.getKey();
297             }
298         }
299         return null;
300     }
301 
TestBothDirections()302     public void TestBothDirections() {
303         for (LstrType type : LstrType.values()) {
304             Map<Status, Set<String>> statusToCodes = validity.getStatusToCodes(type);
305             Map<String, Status> codeToStatus = validity.getCodeToStatus(type);
306             assertEquals("null at same time", statusToCodes == null, codeToStatus == null);
307             if (statusToCodes == null) {
308                 logln("validity data unavailable: " + type);
309                 continue;
310             }
311             for (Entry<Status, Set<String>> entry : statusToCodes.entrySet()) {
312                 Status status = entry.getKey();
313                 for (String code : entry.getValue()) {
314                     assertEquals("Forward works", status, codeToStatus.get(code));
315                 }
316             }
317             for (Entry<String, Status> entry : codeToStatus.entrySet()) {
318                 final String code = entry.getKey();
319                 final Status status = entry.getValue();
320                 assertTrue("Reverse works: " + status, statusToCodes.get(status).contains(code));
321             }
322         }
323     }
324 
TestValidityUniqueness()325     public void TestValidityUniqueness() {
326         Splitter HYPHEN_SPLITTER = Splitter.on('-');
327         UnicodeSet allowed = new UnicodeSet("[a-z0-9A-Z]").freeze();
328         Validity validity = Validity.getInstance();
329         for (Entry<LstrType, Map<Status, Set<String>>> e1 : validity.getData().entrySet()) {
330             final LstrType lstrType = e1.getKey();
331             final boolean lstrTypeUnit = lstrType == LstrType.unit;
332 
333             // Try truncating every key to 8 letters, to ensure that it is unique
334 
335             Multimap<String, String> truncatedToFull = TreeMultimap.create();
336             for (Entry<Status, Set<String>> e2 : e1.getValue().entrySet()) {
337                 final Status status = e2.getKey();
338                 final boolean statusDeprecated = status != Status.deprecated;
339                 for (String codeLong : e2.getValue()) {
340                     String code = codeLong;
341                     String shortCode = code;
342                     if (lstrTypeUnit) {
343                         shortCode = Units.getShort(codeLong);
344                         if (shortCode == null) {
345                             if (statusDeprecated) {
346                                 errln("No short form of " + codeLong);
347                             }
348                             continue;
349                         }
350                     }
351                     for (String subcode : HYPHEN_SPLITTER.split(code)) {
352                         truncatedToFull.put(
353                                 subcode.length() <= 8 ? subcode : subcode.substring(0, 8), subcode);
354                         if (!allowed.containsAll(subcode)) {
355                             errln("subcode has illegal character: " + subcode + ", in " + code);
356                         }
357                     }
358                 }
359             }
360 
361             checkTruncationStatus(truncatedToFull);
362         }
363     }
364 
checkTruncationStatus(Multimap<String, String> truncatedToFull)365     public void checkTruncationStatus(Multimap<String, String> truncatedToFull) {
366         for (Entry<String, Collection<String>> entry : truncatedToFull.asMap().entrySet()) {
367             final String truncated = entry.getKey();
368             final Collection<String> longForms = entry.getValue();
369             if (longForms.size() > 1) {
370                 errln("Ambiguous subkey: " + entry);
371             } else if (isVerbose()) {
372                 if (!longForms.contains(truncated)) {
373                     logln(entry.toString());
374                 }
375             }
376         }
377     }
378 
TestLanguageTagParser()379     public void TestLanguageTagParser() {
380         String[][] tests = {
381             {
382                 "en-cyrl_ru_variant2_variant1",
383                 "en_Cyrl_RU_VARIANT1_VARIANT2",
384                 "en-Cyrl-RU-variant1-variant2"
385             },
386             // Hold off, since ICU doesn't canonicalize: doesn't correctly interpret
387             // en@co=PHONEBK;em=EMOJI;t=RU
388             // { "EN-U-CO-PHONEBK-EM-EMOJI-T_RU", "en@T=RU;CO=PHONEBK;EM=EMOJI",
389             // "en-t-ru-u-co-phonebk-em-emoji" },
390         };
391         LanguageTagParser ltp = new LanguageTagParser();
392         for (String[] test : tests) {
393             String source = test[0];
394             String expectedLanguageSubtagParserIcu = test[1];
395             String expectedLanguageSubtagParserBCP = test[2];
396 
397             // check that field 2 is the same as ICU
398             ULocale icuFromICU = new ULocale(expectedLanguageSubtagParserIcu);
399             ULocale icuFromBCP = ULocale.forLanguageTag(expectedLanguageSubtagParserBCP);
400             assertEquals("ICU from BCP47 => ICU from legacy:\t" + source, icuFromBCP, icuFromICU);
401 
402             ltp.set(source);
403             String actualLanguageSubtagParserIcu = ltp.toString();
404             assertEquals(
405                     "Language subtag (ICU) for " + source,
406                     expectedLanguageSubtagParserIcu,
407                     actualLanguageSubtagParserIcu);
408             String actualLanguageSubtagParserBCP =
409                     ltp.toString(LanguageTagParser.OutputOption.BCP47);
410             assertEquals(
411                     "Language subtag (BCP47) for " + source,
412                     expectedLanguageSubtagParserBCP,
413                     actualLanguageSubtagParserBCP);
414         }
415     }
416 
TestLanguageTagCanonicalizer()417     public void TestLanguageTagCanonicalizer() {
418         String[][] tests = {
419             {"dE-foniPa", "de_fonipa"},
420             //            { "el-1901-polytoni-aaland", "el_AX_1901_polyton" }, // doesn't yet handle
421             // polyton
422             //            { "en-POLYTONI-WHATEVER-ANYTHING-AALAND",
423             // "en_AX_anything_polyton_whatever" }, // doesn't yet handle polyton
424             {"eng-840", "en"},
425             {"sh_ba", "sr_Latn_BA"},
426             {"iw-arab-010", "he_Arab_AQ"},
427             {LocaleNames.UND, LocaleNames.UND},
428             {"und_us", "und_US"},
429             {"und_su", "und_RU"},
430         };
431         LanguageTagCanonicalizer canon = new LanguageTagCanonicalizer();
432         for (String[] inputExpected : tests) {
433             assertEquals("Canonicalize", inputExpected[1], canon.transform(inputExpected[0]));
434         }
435     }
436 
437     final Map<LstrType, Map<String, Map<LstrField, String>>> lstr = StandardCodes.getEnumLstreg();
438     final Map<String, Map<String, R2<List<String>, String>>> typeToCodeToReplacement =
439             CLDRConfig.getInstance().getSupplementalDataInfo().getLocaleAliasInfo();
440 
TestLstrConsistency()441     public void TestLstrConsistency() {
442         // get the alias info, and process
443         // eg "language" -> "sh" -> <{"sr_Latn"}, reason>
444 
445         // quick consistency check of lstr
446         // hack for extlang.
447         Map<String, Map<LstrField, String>> extlangItems = lstr.get(LstrType.extlang);
448         Map<String, Map<LstrField, String>> languageItems = lstr.get(LstrType.language);
449         if (!languageItems.keySet().containsAll(extlangItems.keySet())) {
450             errln(
451                     "extlang not subset of language: "
452                             + setDifference(extlangItems.keySet(), languageItems.keySet()));
453         }
454 
455         ImmutableSet<LstrType> LstrTypesToSkip =
456                 ImmutableSet.of(LstrType.extlang, LstrType.legacy, LstrType.redundant);
457         Set<LstrType> lstrTypesToTest = EnumSet.allOf(LstrType.class);
458         lstrTypesToTest.removeAll(LstrTypesToSkip);
459         Set<String> missingAliases = new LinkedHashSet<>();
460         Map<String, String> changedAliases = new LinkedHashMap<>();
461 
462         for (LstrType lstrType : lstrTypesToTest) {
463             Map<String, Map<LstrField, String>> lstrValue = lstr.get(lstrType);
464             if (lstrValue == null) {
465                 continue;
466             }
467             Map<String, R2<List<String>, String>> codeToReplacement =
468                     typeToCodeToReplacement.get(lstrType.toCompatString());
469 
470             Set<String> lstrDeprecated = new TreeSet<>();
471             Set<String> aliased = new TreeSet<>();
472             Map<String, String> lstrPreferred = new TreeMap<>();
473             Map<String, String> aliasPreferred = new TreeMap<>();
474 
475             for (Entry<String, Map<LstrField, String>> codeToData : lstrValue.entrySet()) {
476                 String code = codeToData.getKey();
477                 Map<LstrField, String> data = codeToData.getValue();
478                 boolean deprecated = data.get(LstrField.Deprecated) != null;
479                 if (deprecated) {
480                     lstrDeprecated.add(code);
481                 }
482                 String preferred = data.get(LstrField.Preferred_Value);
483                 if (preferred != null) {
484                     lstrPreferred.put(code, preferred);
485                 }
486                 if (codeToReplacement != null) {
487                     R2<List<String>, String> replacement = codeToReplacement.get(code);
488                     if (replacement != null) {
489                         aliased.add(code);
490                         List<String> replacementList = replacement.get0();
491                         aliasPreferred.put(code, CldrUtility.join(replacementList, " "));
492                     }
493                 }
494             }
495             Set<String> diff = setDifference(aliased, lstrDeprecated);
496             logln(lstrType + ": aliased and not deprecated in lstr: " + diff);
497             lstrDeprecated.addAll(diff);
498 
499             //            // special exceptions
500             //            switch(lstrType) {
501             //            case script: lstrDeprecated.add("Qaai"); break;
502             //            case region: lstrDeprecated.add("QU"); break;
503             //            default: break;
504             //            }
505 
506             Map<Status, Set<String>> statusToCodes = validity.getStatusToCodes(lstrType);
507             Set<String> validityDeprecated =
508                     statusToCodes == null ? null : statusToCodes.get(Status.deprecated);
509 
510             if (!Objects.equal(lstrDeprecated, validityDeprecated)) {
511                 showMinus(
512                         "Deprecated lstr - validity", lstrType, lstrDeprecated, validityDeprecated);
513                 showMinus(
514                         "Deprecated validity - lstr", lstrType, validityDeprecated, lstrDeprecated);
515             }
516 
517             if (!Objects.equal(lstrPreferred, aliasPreferred)) {
518                 // showMinus("Preferred lstr - alias", lstrType, lstrPreferred.entrySet(),
519                 // aliasPreferred.entrySet());
520                 for (Entry<String, String> entry : lstrPreferred.entrySet()) {
521                     String code = entry.getKey();
522                     String lstrReplacement = entry.getValue();
523                     String aliasValue = aliasPreferred.get(code);
524                     if (lstrReplacement.equals(aliasValue)) {
525                         continue;
526                     }
527                     String newAlias = makeAliasXml(lstrType, code, lstrReplacement, "deprecated");
528                     if (aliasValue == null) {
529                         missingAliases.add(newAlias);
530                     } else {
531                         changedAliases.put(
532                                 newAlias, makeAliasXml(lstrType, code, aliasValue, "deprecated"));
533                     }
534                 }
535             }
536         }
537         if (!missingAliases.isEmpty()) {
538             errln("Missing aliases for supplementalMetadata: " + missingAliases.size());
539             for (String s : missingAliases) {
540                 System.out.println(s);
541             }
542         }
543         if (!changedAliases.isEmpty()) {
544             warnln("Changed aliases from LSTR, just double-check: " + changedAliases.size());
545             for (Entry<String, String> entry : changedAliases.entrySet()) {
546                 getLogger().fine("\tcurrent=" + entry.getValue() + "\n\tlstr=" + entry.getKey());
547             }
548         }
549     }
550 
551     // <languageAlias type="art_lojban" replacement="jbo" reason="deprecated"/> <!-- Lojban -->
552     // <scriptAlias type="Qaai" replacement="Zinh" reason="deprecated"/>
553     // <territoryAlias type="AAA" replacement="AA" reason="overlong"/> <!-- null -->
554     // <variantAlias type="AALAND" replacement="AX" reason="deprecated"/>
makeAliasXml( LstrType lstrType, String code, String lstrReplacement, String reason)555     private String makeAliasXml(
556             LstrType lstrType, String code, String lstrReplacement, String reason) {
557         return "<"
558                 + lstrType.toCompatString()
559                 + "Alias"
560                 + " type=\""
561                 + code
562                 + "\""
563                 + " replacement=\""
564                 + lstrReplacement
565                 + "\""
566                 + " reason=\""
567                 + reason
568                 + "\"/>"
569                 + " <!-- "
570                 + TransliteratorUtilities.toXML.transform(
571                         CLDRConfig.getInstance().getEnglish().getName(code)
572                                 + " ⇒ "
573                                 + CLDRConfig.getInstance().getEnglish().getName(lstrReplacement))
574                 + " -->";
575     }
576 
setDifference(U a, U b)577     private <T, U extends Collection<T>> Set<T> setDifference(U a, U b) {
578         Set<T> diff = new LinkedHashSet<>(a);
579         diff.removeAll(b);
580         return diff;
581     }
582 
showMinus(String title, LstrType lstrType, Set<T> a, Set<T> b)583     private <T> Set<T> showMinus(String title, LstrType lstrType, Set<T> a, Set<T> b) {
584         if (a == null) {
585             a = Collections.emptySet();
586         }
587         if (b == null) {
588             b = Collections.emptySet();
589         }
590 
591         Set<T> diff = setDifference(a, b);
592         if (!diff.isEmpty()) {
593             T first = diff.iterator().next();
594             if (first instanceof String) {
595                 List<String> names =
596                         diff.stream()
597                                 .map(
598                                         code ->
599                                                 "\n\t\t"
600                                                         + code
601                                                         + " ⇒\t"
602                                                         + lstr.get(lstrType).get(code)
603                                                         + "\n\t\tvalid.⇒\t"
604                                                         + CldrUtility.ifNull(
605                                                                         validity.getCodeToStatus(
606                                                                                 lstrType),
607                                                                         Collections.emptyMap())
608                                                                 .get(code)
609                                                         + "\n\t\talias⇒\t"
610                                                         + CldrUtility.ifNull(
611                                                                         typeToCodeToReplacement.get(
612                                                                                 lstrType
613                                                                                         .toCompatString()),
614                                                                         Collections.emptyMap())
615                                                                 .get(code))
616                                 .collect(Collectors.toList());
617                 errln(
618                         title
619                                 + "\n\tLstrType=\t"
620                                 + lstrType
621                                 + "\n\tsize=\t"
622                                 + diff.size()
623                                 + "\n\tcodes=\t"
624                                 + diff
625                                 + "\n\tnames=\t"
626                                 + names);
627             } else {
628                 errln(
629                         title
630                                 + "\n\tLstrType=\t"
631                                 + lstrType
632                                 + "\n\tsize=\t"
633                                 + diff.size()
634                                 + "\n\tcodes=\t"
635                                 + diff);
636             }
637         }
638         return diff;
639     }
640 
641     final Set<LstrType> EXPECTED_NULL =
642             ImmutableSet.of(
643                     LstrType.extlang,
644                     LstrType.legacy,
645                     LstrType.redundant,
646                     LstrType.usage,
647                     LstrType.zone,
648                     LstrType.extension);
649     final Set<LstrType> EXPECTED_UNSORTED = ImmutableSet.of(LstrType.unit);
650 
testOrder()651     public void testOrder() {
652         for (LstrType type : LstrType.values()) {
653             Map<Status, Set<String>> statusToCodes = validity.getStatusToCodes(type);
654             assertEquals(type + " is null", EXPECTED_NULL.contains(type), statusToCodes == null);
655             if (statusToCodes == null) {
656                 continue;
657             }
658             for (Entry<Status, Set<String>> entry : statusToCodes.entrySet()) {
659                 Status status = entry.getKey();
660                 Set<String> codes = entry.getValue();
661                 List<String> list = new ArrayList<>(codes);
662                 List<String> sorted = new ArrayList<>(new TreeSet<>(codes));
663                 boolean isSorted = list.equals(sorted);
664                 assertEquals(
665                         type + ":" + status + " is sorted",
666                         EXPECTED_UNSORTED.contains(type),
667                         !isSorted);
668             }
669         }
670     }
671 }
672