• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.unicode.cldr.unittest;
2 
3 import com.google.common.base.Joiner;
4 import com.google.common.base.Splitter;
5 import com.google.common.collect.BiMap;
6 import com.google.common.collect.Comparators;
7 import com.google.common.collect.ComparisonChain;
8 import com.google.common.collect.HashMultimap;
9 import com.google.common.collect.ImmutableList;
10 import com.google.common.collect.ImmutableMap;
11 import com.google.common.collect.ImmutableMultimap;
12 import com.google.common.collect.ImmutableSet;
13 import com.google.common.collect.ImmutableSortedSet;
14 import com.google.common.collect.LinkedHashMultimap;
15 import com.google.common.collect.Multimap;
16 import com.google.common.collect.Multimaps;
17 import com.google.common.collect.Ordering;
18 import com.google.common.collect.Sets;
19 import com.google.common.collect.Sets.SetView;
20 import com.google.common.collect.TreeMultimap;
21 import com.ibm.icu.dev.test.TestFmwk;
22 import com.ibm.icu.impl.Row;
23 import com.ibm.icu.impl.Row.R2;
24 import com.ibm.icu.impl.Row.R3;
25 import com.ibm.icu.number.FormattedNumber;
26 import com.ibm.icu.number.LocalizedNumberFormatter;
27 import com.ibm.icu.number.Notation;
28 import com.ibm.icu.number.NumberFormatter;
29 import com.ibm.icu.number.NumberFormatter.UnitWidth;
30 import com.ibm.icu.number.Precision;
31 import com.ibm.icu.number.UnlocalizedNumberFormatter;
32 import com.ibm.icu.text.MessageFormat;
33 import com.ibm.icu.text.PluralRules;
34 import com.ibm.icu.text.UnicodeSet;
35 import com.ibm.icu.util.ICUUncheckedIOException;
36 import com.ibm.icu.util.Measure;
37 import com.ibm.icu.util.MeasureUnit;
38 import com.ibm.icu.util.Output;
39 import com.ibm.icu.util.ULocale;
40 import java.io.File;
41 import java.io.IOException;
42 import java.io.OutputStreamWriter;
43 import java.io.PrintWriter;
44 import java.math.BigDecimal;
45 import java.math.BigInteger;
46 import java.math.MathContext;
47 import java.nio.file.Files;
48 import java.nio.file.Path;
49 import java.util.ArrayList;
50 import java.util.Arrays;
51 import java.util.Collection;
52 import java.util.Collections;
53 import java.util.Comparator;
54 import java.util.HashSet;
55 import java.util.LinkedHashMap;
56 import java.util.LinkedHashSet;
57 import java.util.List;
58 import java.util.Locale;
59 import java.util.Map;
60 import java.util.Map.Entry;
61 import java.util.Objects;
62 import java.util.Set;
63 import java.util.TreeMap;
64 import java.util.TreeSet;
65 import java.util.logging.Logger;
66 import java.util.regex.Matcher;
67 import java.util.regex.Pattern;
68 import java.util.stream.Collectors;
69 import java.util.stream.Stream;
70 import java.util.stream.StreamSupport;
71 import org.unicode.cldr.draft.FileUtilities;
72 import org.unicode.cldr.test.CheckCLDR.CheckStatus;
73 import org.unicode.cldr.test.CheckCLDR.Options;
74 import org.unicode.cldr.test.CheckUnits;
75 import org.unicode.cldr.test.ExampleGenerator;
76 import org.unicode.cldr.util.CLDRConfig;
77 import org.unicode.cldr.util.CLDRFile;
78 import org.unicode.cldr.util.CLDRPaths;
79 import org.unicode.cldr.util.ChainedMap;
80 import org.unicode.cldr.util.ChainedMap.M3;
81 import org.unicode.cldr.util.ChainedMap.M4;
82 import org.unicode.cldr.util.CldrUtility;
83 import org.unicode.cldr.util.Counter;
84 import org.unicode.cldr.util.DtdData;
85 import org.unicode.cldr.util.DtdType;
86 import org.unicode.cldr.util.Factory;
87 import org.unicode.cldr.util.GrammarDerivation;
88 import org.unicode.cldr.util.GrammarInfo;
89 import org.unicode.cldr.util.GrammarInfo.GrammaticalFeature;
90 import org.unicode.cldr.util.GrammarInfo.GrammaticalScope;
91 import org.unicode.cldr.util.GrammarInfo.GrammaticalTarget;
92 import org.unicode.cldr.util.Level;
93 import org.unicode.cldr.util.LocaleStringProvider;
94 import org.unicode.cldr.util.MapComparator;
95 import org.unicode.cldr.util.Organization;
96 import org.unicode.cldr.util.Pair;
97 import org.unicode.cldr.util.PathHeader;
98 import org.unicode.cldr.util.Rational;
99 import org.unicode.cldr.util.Rational.ContinuedFraction;
100 import org.unicode.cldr.util.Rational.FormatStyle;
101 import org.unicode.cldr.util.Rational.RationalParser;
102 import org.unicode.cldr.util.SimpleXMLSource;
103 import org.unicode.cldr.util.StandardCodes;
104 import org.unicode.cldr.util.StandardCodes.LstrType;
105 import org.unicode.cldr.util.SupplementalDataInfo;
106 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo;
107 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo.Count;
108 import org.unicode.cldr.util.SupplementalDataInfo.PluralType;
109 import org.unicode.cldr.util.SupplementalDataInfo.UnitIdComponentType;
110 import org.unicode.cldr.util.TempPrintWriter;
111 import org.unicode.cldr.util.UnitConverter;
112 import org.unicode.cldr.util.UnitConverter.ConversionInfo;
113 import org.unicode.cldr.util.UnitConverter.TargetInfo;
114 import org.unicode.cldr.util.UnitConverter.UnitComplexity;
115 import org.unicode.cldr.util.UnitConverter.UnitId;
116 import org.unicode.cldr.util.UnitConverter.UnitSystem;
117 import org.unicode.cldr.util.UnitParser;
118 import org.unicode.cldr.util.UnitPathType;
119 import org.unicode.cldr.util.UnitPreferences;
120 import org.unicode.cldr.util.UnitPreferences.UnitPreference;
121 import org.unicode.cldr.util.Units;
122 import org.unicode.cldr.util.Validity;
123 import org.unicode.cldr.util.Validity.Status;
124 import org.unicode.cldr.util.With;
125 import org.unicode.cldr.util.XMLSource;
126 import org.unicode.cldr.util.XPathParts;
127 
128 public class TestUnits extends TestFmwk {
129     private static final Joiner JOIN_TAB = Joiner.on('\t').useForNull("∅");
130     private static final StandardCodes STANDARD_CODES = StandardCodes.make();
131     private static final boolean DEBUG = System.getProperty("TestUnits:DEBUG") != null;
132     private static final boolean TEST_ICU = System.getProperty("TestUnits:TEST_ICU") != null;
133 
134     private static final Joiner JOIN_COMMA = Joiner.on(", ");
135 
136     /** Flags to emit debugging information */
137     private static final boolean SHOW_UNIT_ORDER = getFlag("TestUnits:SHOW_UNIT_ORDER");
138 
139     private static final boolean SHOW_UNIT_CATEGORY = getFlag("TestUnits:SHOW_UNIT_CATEGORY");
140     private static final boolean SHOW_COMPOSE = getFlag("TestUnits:SHOW_COMPOSE");
141     private static final boolean SHOW_DATA = getFlag("TestUnits:SHOW_DATA");
142     private static final boolean SHOW_MISSING_TEST_DATA =
143             getFlag("TestUnits:SHOW_MISSING_TEST_DATA");
144     private static final boolean SHOW_SYSTEMS = getFlag("TestUnits:SHOW_SYSTEMS");
145 
146     /** Flags for reformatting data file */
147     private static final boolean SHOW_PREFS = getFlag("TestUnits:SHOW_PREFS");
148 
149     /** Flag for generating test: TODO move to separate file */
150     private static final boolean GENERATE_TESTS = getFlag("TestUnits:GENERATE_TESTS");
151 
152     private static final Set<String> VALID_REGULAR_UNITS =
153             Validity.getInstance().getStatusToCodes(LstrType.unit).get(Validity.Status.regular);
154     private static final Set<String> DEPRECATED_REGULAR_UNITS =
155             Validity.getInstance().getStatusToCodes(LstrType.unit).get(Validity.Status.deprecated);
156     public static final CLDRConfig CLDR_CONFIG = CLDRConfig.getInstance();
157     private static final Integer INTEGER_ONE = 1;
158 
getFlag(String flag)159     public static boolean getFlag(String flag) {
160         return CldrUtility.getProperty(flag, false);
161     }
162 
163     private static final String TEST_SEP = ";\t";
164 
165     private static final ImmutableSet<String> WORLD_SET = ImmutableSet.of("001");
166     private static final CLDRConfig info = CLDR_CONFIG;
167     private static final SupplementalDataInfo SDI = info.getSupplementalDataInfo();
168 
169     static final UnitConverter converter = SDI.getUnitConverter();
170     static final Set<String> VALID_SHORT_UNITS = converter.getShortIds(VALID_REGULAR_UNITS);
171     static final Set<String> DEPRECATED_SHORT_UNITS =
172             converter.getShortIds(DEPRECATED_REGULAR_UNITS);
173 
174     static final Splitter SPLIT_SEMI = Splitter.on(Pattern.compile("\\s*;\\s*")).trimResults();
175     static final Splitter SPLIT_SPACE = Splitter.on(' ').trimResults().omitEmptyStrings();
176     static final Splitter SPLIT_AND = Splitter.on("-and-").trimResults().omitEmptyStrings();
177     static final Splitter SPLIT_DASH = Splitter.on('-').trimResults().omitEmptyStrings();
178 
179     static final Rational R1000 = Rational.of(1000);
180 
181     static Map<String, String> normalizationCache = new TreeMap<>();
182 
main(String[] args)183     public static void main(String[] args) {
184         new TestUnits().run(args);
185     }
186 
187     private Map<String, String> BASE_UNIT_TO_QUANTITY = converter.getBaseUnitToQuantity();
188 
TestSpaceInNarrowUnits()189     public void TestSpaceInNarrowUnits() {
190         final CLDRFile english = CLDR_CONFIG.getEnglish();
191         final Matcher m = Pattern.compile("narrow.*unitPattern").matcher("");
192         for (String path : english) {
193             if (m.reset(path).find()) {
194                 String value = english.getStringValue(path);
195                 if (value.contains("} ")) {
196                     errln(path + " fails, «" + value + "» contains } + space");
197                 }
198             }
199         }
200     }
201 
202     static final String[][] COMPOUND_TESTS = {
203         {"area-square-centimeter", "square", "length-centimeter"},
204         {"area-square-foot", "square", "length-foot"},
205         {"area-square-inch", "square", "length-inch"},
206         {"area-square-kilometer", "square", "length-kilometer"},
207         {"area-square-meter", "square", "length-meter"},
208         {"area-square-mile", "square", "length-mile"},
209         {"area-square-yard", "square", "length-yard"},
210         {"digital-gigabit", "giga", "digital-bit"},
211         {"digital-gigabyte", "giga", "digital-byte"},
212         {"digital-kilobit", "kilo", "digital-bit"},
213         {"digital-kilobyte", "kilo", "digital-byte"},
214         {"digital-megabit", "mega", "digital-bit"},
215         {"digital-megabyte", "mega", "digital-byte"},
216         {"digital-petabyte", "peta", "digital-byte"},
217         {"digital-terabit", "tera", "digital-bit"},
218         {"digital-terabyte", "tera", "digital-byte"},
219         {"duration-microsecond", "micro", "duration-second"},
220         {"duration-millisecond", "milli", "duration-second"},
221         {"duration-nanosecond", "nano", "duration-second"},
222         {"electric-milliampere", "milli", "electric-ampere"},
223         {"energy-kilocalorie", "kilo", "energy-calorie"},
224         {"energy-kilojoule", "kilo", "energy-joule"},
225         {"frequency-gigahertz", "giga", "frequency-hertz"},
226         {"frequency-kilohertz", "kilo", "frequency-hertz"},
227         {"frequency-megahertz", "mega", "frequency-hertz"},
228         {"graphics-megapixel", "mega", "graphics-pixel"},
229         {"length-centimeter", "centi", "length-meter"},
230         {"length-decimeter", "deci", "length-meter"},
231         {"length-kilometer", "kilo", "length-meter"},
232         {"length-micrometer", "micro", "length-meter"},
233         {"length-millimeter", "milli", "length-meter"},
234         {"length-nanometer", "nano", "length-meter"},
235         {"length-picometer", "pico", "length-meter"},
236         {"mass-kilogram", "kilo", "mass-gram"},
237         {"mass-microgram", "micro", "mass-gram"},
238         {"mass-milligram", "milli", "mass-gram"},
239         {"power-gigawatt", "giga", "power-watt"},
240         {"power-kilowatt", "kilo", "power-watt"},
241         {"power-megawatt", "mega", "power-watt"},
242         {"power-milliwatt", "milli", "power-watt"},
243         {"pressure-hectopascal", "hecto", "pressure-pascal"},
244         {"pressure-millibar", "milli", "pressure-bar"},
245         {"pressure-kilopascal", "kilo", "pressure-pascal"},
246         {"pressure-megapascal", "mega", "pressure-pascal"},
247         {"volume-centiliter", "centi", "volume-liter"},
248         {"volume-cubic-centimeter", "cubic", "length-centimeter"},
249         {"volume-cubic-foot", "cubic", "length-foot"},
250         {"volume-cubic-inch", "cubic", "length-inch"},
251         {"volume-cubic-kilometer", "cubic", "length-kilometer"},
252         {"volume-cubic-meter", "cubic", "length-meter"},
253         {"volume-cubic-mile", "cubic", "length-mile"},
254         {"volume-cubic-yard", "cubic", "length-yard"},
255         {"volume-deciliter", "deci", "volume-liter"},
256         {"volume-hectoliter", "hecto", "volume-liter"},
257         {"volume-megaliter", "mega", "volume-liter"},
258         {"volume-milliliter", "milli", "volume-liter"},
259     };
260 
261     static final String[][] PREFIX_NAME_TYPE = {
262         {"deci", "10p-1"},
263         {"centi", "10p-2"},
264         {"milli", "10p-3"},
265         {"micro", "10p-6"},
266         {"nano", "10p-9"},
267         {"pico", "10p-12"},
268         {"femto", "10p-15"},
269         {"atto", "10p-18"},
270         {"zepto", "10p-21"},
271         {"yocto", "10p-24"},
272         {"deka", "10p1"},
273         {"hecto", "10p2"},
274         {"kilo", "10p3"},
275         {"mega", "10p6"},
276         {"giga", "10p9"},
277         {"tera", "10p12"},
278         {"peta", "10p15"},
279         {"exa", "10p18"},
280         {"zetta", "10p21"},
281         {"yotta", "10p24"},
282         {"square", "power2"},
283         {"cubic", "power3"},
284     };
285 
286     static final String PATH_UNIT_PATTERN =
287             "//ldml/units/unitLength[@type=\"{0}\"]/unit[@type=\"{1}\"]/unitPattern[@count=\"{2}\"]";
288 
289     static final String PATH_PREFIX_PATTERN =
290             "//ldml/units/unitLength[@type=\"{0}\"]/compoundUnit[@type=\"{1}\"]/unitPrefixPattern";
291     static final String PATH_SUFFIX_PATTERN =
292             "//ldml/units/unitLength[@type=\"{0}\"]/compoundUnit[@type=\"{1}\"]/compoundUnitPattern1";
293 
294     static final String PATH_MILLI_PATTERN =
295             "//ldml/units/unitLength[@type=\"{0}\"]/compoundUnit[@type=\"10p-3\"]/unitPrefixPattern";
296     static final String PATH_SQUARE_PATTERN =
297             "//ldml/units/unitLength[@type=\"{0}\"]/compoundUnit[@type=\"power2\"]/compoundUnitPattern1";
298 
299     static final String PATH_METER_PATTERN =
300             "//ldml/units/unitLength[@type=\"{0}\"]/unit[@type=\"length-meter\"]/unitPattern[@count=\"{1}\"]";
301     static final String PATH_MILLIMETER_PATTERN =
302             "//ldml/units/unitLength[@type=\"{0}\"]/unit[@type=\"length-millimeter\"]/unitPattern[@count=\"{1}\"]";
303     static final String PATH_SQUARE_METER_PATTERN =
304             "//ldml/units/unitLength[@type=\"{0}\"]/unit[@type=\"area-square-meter\"]/unitPattern[@count=\"{1}\"]";
305 
TestAUnits()306     public void TestAUnits() {
307         if (isVerbose()) {
308             System.out.println();
309             Output<String> baseUnit = new Output<>();
310             int count = 0;
311             for (String simpleUnit : converter.getSimpleUnits()) {
312                 ConversionInfo conversion = converter.parseUnitId(simpleUnit, baseUnit, false);
313                 if (simpleUnit.equals(baseUnit)) {
314                     continue;
315                 }
316                 System.out.println(
317                         ++count
318                                 + ")\t"
319                                 + simpleUnit
320                                 + " → "
321                                 + baseUnit
322                                 + "; factor = "
323                                 + conversion.factor
324                                 + " = "
325                                 + conversion.factor.toString(FormatStyle.repeatingAll)
326                                 + (conversion.offset.equals(Rational.ZERO)
327                                         ? ""
328                                         : "; offset = " + conversion.offset));
329             }
330         }
331     }
332 
TestCompoundUnit3()333     public void TestCompoundUnit3() {
334         Factory factory = CLDR_CONFIG.getCldrFactory();
335 
336         Map<String, String> prefixToType = new LinkedHashMap<>();
337         for (String[] prefixRow : PREFIX_NAME_TYPE) {
338             prefixToType.put(prefixRow[0], prefixRow[1]);
339         }
340         prefixToType = ImmutableMap.copyOf(prefixToType);
341 
342         Set<String> localesToTest = ImmutableSet.of("en"); // factory.getAvailableLanguages();
343         int testCount = 0;
344         for (String locale : localesToTest) {
345             CLDRFile file = factory.make(locale, true);
346             // ExampleGenerator exampleGenerator = getExampleGenerator(locale);
347             PluralInfo pluralInfo = SDI.getPlurals(PluralType.cardinal, locale);
348             final boolean isEnglish = locale.contentEquals("en");
349             int errMsg = isEnglish ? ERR : WARN;
350 
351             for (String[] compoundTest : COMPOUND_TESTS) {
352                 String targetUnit = compoundTest[0];
353                 String prefix = compoundTest[1];
354                 String baseUnit = compoundTest[2];
355                 String prefixType = prefixToType.get(prefix); // will be null for square, cubic
356                 final boolean isPrefix = prefixType.startsWith("1");
357 
358                 for (String len : Arrays.asList("long", "short", "narrow")) {
359                     String prefixPath =
360                             ExampleGenerator.format(
361                                     isPrefix ? PATH_PREFIX_PATTERN : PATH_SUFFIX_PATTERN,
362                                     len,
363                                     prefixType);
364                     String prefixValue = file.getStringValue(prefixPath);
365                     boolean lowercaseIfSpaced = len.equals("long");
366 
367                     for (Count count : pluralInfo.getCounts()) {
368                         final String countString = count.toString();
369                         String targetUnitPath =
370                                 ExampleGenerator.format(
371                                         PATH_UNIT_PATTERN, len, targetUnit, countString);
372                         String targetUnitPattern = file.getStringValue(targetUnitPath);
373 
374                         String baseUnitPath =
375                                 ExampleGenerator.format(
376                                         PATH_UNIT_PATTERN, len, baseUnit, countString);
377                         String baseUnitPattern = file.getStringValue(baseUnitPath);
378 
379                         String composedTargetUnitPattern =
380                                 Units.combinePattern(
381                                         baseUnitPattern, prefixValue, lowercaseIfSpaced);
382                         if (isEnglish && !targetUnitPattern.equals(composedTargetUnitPattern)) {
383                             if (allowEnglishException(
384                                     targetUnitPattern, composedTargetUnitPattern)) {
385                                 continue;
386                             }
387                         }
388                         if (!assertEquals2(
389                                 errMsg,
390                                 testCount++
391                                         + ") "
392                                         + locale
393                                         + "/"
394                                         + len
395                                         + "/"
396                                         + count
397                                         + "/"
398                                         + prefix
399                                         + "+"
400                                         + baseUnit
401                                         + ": constructed pattern",
402                                 targetUnitPattern,
403                                 composedTargetUnitPattern)) {
404                             Units.combinePattern(baseUnitPattern, prefixValue, lowercaseIfSpaced);
405                             int debug = 0;
406                         }
407                     }
408                 }
409             }
410         }
411     }
412 
413     /**
414      * Curated list of known exceptions. Usually because the short form of a unit is shorter when
415      * combined with a prefix or suffix
416      */
417     static final Map<String, String> ALLOW_ENGLISH_EXCEPTION =
418             ImmutableMap.<String, String>builder()
419                     .put("sq ft", "ft²")
420                     .put("sq mi", "mi²")
421                     .put("ft", "′")
422                     .put("in", "″")
423                     .put("MP", "Mpx")
424                     .put("b", "bit")
425                     .put("mb", "mbar")
426                     .put("B", "byte")
427                     .put("s", "sec")
428                     .build();
429 
allowEnglishException( String targetUnitPattern, String composedTargetUnitPattern)430     private boolean allowEnglishException(
431             String targetUnitPattern, String composedTargetUnitPattern) {
432         for (Entry<String, String> entry : ALLOW_ENGLISH_EXCEPTION.entrySet()) {
433             String mod = targetUnitPattern.replace(entry.getKey(), entry.getValue());
434             if (mod.contentEquals(composedTargetUnitPattern)) {
435                 return true;
436             }
437         }
438         return false;
439     }
440 
441     // TODO Work this into a generating and then maintaining a data table for the units
442     /*
443     CLDRFile english = factory.make("en", false);
444     Set<String> prefixes = new TreeSet<>();
445     for (String path : english) {
446         XPathParts parts = XPathParts.getFrozenInstance(path);
447         String lastElement = parts.getElement(-1);
448         if (lastElement.equals("unitPrefixPattern") || lastElement.equals("compoundUnitPattern1")) {
449             if (!parts.getAttributeValue(2, "type").equals("long")) {
450                 continue;
451             }
452             String value = english.getStringValue(path);
453             prefixes.add(value.replace("{0}", "").trim());
454         }
455     }
456     Map<Status, Set<String>> unitValidity = Validity.getInstance().getStatusToCodes(LstrType.unit);
457     Multimap<String, String> from = LinkedHashMultimap.create();
458     for (String unit : unitValidity.get(Status.regular)) {
459         String[] parts = unit.split("[-]");
460         String main = parts[1];
461         for (String prefix : prefixes) {
462             if (main.startsWith(prefix)) {
463                 if (main.length() == prefix.length()) { // square,...
464                     from.put(unit, main);
465                 } else { // milli
466                     from.put(unit, main.substring(0,prefix.length()));
467                     from.put(unit, main.substring(prefix.length()));
468                 }
469                 for (int i = 2; i < parts.length; ++i) {
470                     from.put(unit, parts[i]);
471                 }
472             }
473         }
474     }
475     for (Entry<String, Collection<String>> set : from.asMap().entrySet()) {
476         System.out.println(set.getKey() + "\t" + CollectionUtilities.join(set.getValue(), "\t"));
477     }
478     */
assertEquals2( int TestERR, String title, String sqmeterPattern, String conSqmeterPattern)479     private boolean assertEquals2(
480             int TestERR, String title, String sqmeterPattern, String conSqmeterPattern) {
481         if (!Objects.equals(sqmeterPattern, conSqmeterPattern)) {
482             msg(
483                     title + ", expected «" + sqmeterPattern + "», got «" + conSqmeterPattern + "»",
484                     TestERR,
485                     true,
486                     true);
487             return false;
488         } else if (isVerbose()) {
489             msg(
490                     title + ", expected «" + sqmeterPattern + "», got «" + conSqmeterPattern + "»",
491                     LOG,
492                     true,
493                     true);
494         }
495         return true;
496     }
497 
TestConversion()498     public void TestConversion() {
499         String[][] tests = {
500             {"foot", "12", "inch"},
501             {"gallon", "4", "quart"},
502             {"gallon", "16", "cup"},
503         };
504         for (String[] test : tests) {
505             String sourceUnit = test[0];
506             Rational factor = Rational.of(test[1]);
507             String targetUnit = test[2];
508             final Rational convert = converter.convertDirect(Rational.ONE, sourceUnit, targetUnit);
509             assertEquals(sourceUnit + " to " + targetUnit, factor, convert);
510         }
511 
512         // test conversions are disjoint
513         Set<String> gotAlready = new HashSet<>();
514         List<Set<String>> equivClasses = new ArrayList<>();
515         Map<String, String> classToId = new TreeMap<>();
516         for (String unit : converter.canConvert()) {
517             if (gotAlready.contains(unit)) {
518                 continue;
519             }
520             Set<String> set = converter.canConvertBetween(unit);
521             final String id = "ID" + equivClasses.size();
522             equivClasses.add(set);
523             gotAlready.addAll(set);
524             for (String s : set) {
525                 classToId.put(s, id);
526             }
527         }
528 
529         // check not overlapping
530         // now handled by TestParseUnit, but we might revive a modified version of this.
531         //        for (int i = 0; i < equivClasses.size(); ++i) {
532         //            Set<String> eclass1 = equivClasses.get(i);
533         //            for (int j = i+1; j < equivClasses.size(); ++j) {
534         //                Set<String> eclass2 = equivClasses.get(j);
535         //                if (!Collections.disjoint(eclass1, eclass2)) {
536         //                    errln("Overlapping equivalence classes: " + eclass1 + " ~ " + eclass2
537         // + "\n\tProbably bad chain requiring 3 steps.");
538         //                }
539         //            }
540         //
541         //            // check that all elements of an equivalence class have the same type
542         //            Multimap<String,String> breakdown = TreeMultimap.create();
543         //            for (String item : eclass1) {
544         //                String type = CORE_TO_TYPE.get(item);
545         //                if (type == null) {
546         //                    type = "?";
547         //                }
548         //                breakdown.put(type, item);
549         //            }
550         //            if (DEBUG) System.out.println("type to item: " + breakdown);
551         //            if (breakdown.keySet().size() != 1) {
552         //                errln("mixed categories: " + breakdown);
553         //            }
554         //
555         //        }
556         //
557         //        // check that all units with the same type have the same equivalence class
558         //        for (Entry<String, Collection<String>> entry : TYPE_TO_CORE.asMap().entrySet()) {
559         //            Multimap<String,String> breakdown = TreeMultimap.create();
560         //            for (String item : entry.getValue()) {
561         //                String id = classToId.get(item);
562         //                if (id == null) {
563         //                    continue;
564         //                }
565         //                breakdown.put(id, item);
566         //            }
567         //            if (DEBUG) System.out.println(entry.getKey() + " id to item: " + breakdown);
568         //            if (breakdown.keySet().size() != 1) {
569         //                errln(entry.getKey() + " mixed categories: " + breakdown);
570         //            }
571         //        }
572     }
573 
TestBaseUnits()574     public void TestBaseUnits() {
575         Splitter barSplitter = Splitter.on('-');
576         for (String unit : converter.baseUnits()) {
577             for (String piece : barSplitter.split(unit)) {
578                 assertTrue(
579                         unit + ": " + piece + " in " + UnitConverter.BASE_UNIT_PARTS,
580                         UnitConverter.BASE_UNIT_PARTS.contains(piece));
581             }
582         }
583     }
584 
TestUnitId()585     public void TestUnitId() {
586 
587         for (String simple : converter.getSimpleUnits()) {
588             String canonicalUnit = converter.getBaseUnit(simple);
589             UnitId unitId = converter.createUnitId(canonicalUnit);
590             String output = unitId.toString();
591             if (!assertEquals(
592                     simple + ": targets should be in canonical form", output, canonicalUnit)) {
593                 // for debugging
594                 converter.createUnitId(canonicalUnit);
595                 unitId.toString();
596             }
597         }
598         for (Entry<String, String> baseUnitToQuantity : BASE_UNIT_TO_QUANTITY.entrySet()) {
599             String baseUnit = baseUnitToQuantity.getKey();
600             String quantity = baseUnitToQuantity.getValue();
601             try {
602                 UnitId unitId = converter.createUnitId(baseUnit);
603                 String output = unitId.toString();
604                 if (!assertEquals(
605                         quantity + ": targets should be in canonical form", output, baseUnit)) {
606                     // for debugging
607                     converter.createUnitId(baseUnit);
608                     unitId.toString();
609                 }
610             } catch (Exception e) {
611                 errln("Can't convert baseUnit: " + baseUnit);
612             }
613         }
614 
615         for (String baseUnit : CORE_TO_TYPE.keySet()) {
616             try {
617                 UnitId unitId = converter.createUnitId(baseUnit);
618                 assertNotNull("Can't parse baseUnit: " + baseUnit, unitId);
619             } catch (Exception e) {
620                 converter.createUnitId(baseUnit); // for debugging
621                 errln("Can't parse baseUnit: " + baseUnit);
622             }
623         }
624     }
625 
TestParseUnit()626     public void TestParseUnit() {
627         Output<String> compoundBaseUnit = new Output<>();
628         String[][] tests = {
629             {"kilometer-pound-per-hour", "kilogram-meter-per-second", "45359237/360000000"},
630             {"kilometer-per-hour", "meter-per-second", "5/18"},
631         };
632         for (String[] test : tests) {
633             String source = test[0];
634             String expectedUnit = test[1];
635             Rational expectedRational = new Rational.RationalParser().parse(test[2]);
636             ConversionInfo unitInfo = converter.parseUnitId(source, compoundBaseUnit, false);
637             assertEquals(source, expectedUnit, compoundBaseUnit.value);
638             assertEquals(source, expectedRational, unitInfo.factor);
639         }
640 
641         // check all
642         if (GENERATE_TESTS) System.out.println();
643         Set<String> badUnits = new LinkedHashSet<>();
644         Set<String> noQuantity = new LinkedHashSet<>();
645         Multimap<Pair<String, Double>, String> testPrintout = TreeMultimap.create();
646 
647         // checkUnitConvertability(converter, compoundBaseUnit, badUnits, "pint-metric-per-second");
648 
649         for (Entry<String, String> entry : TYPE_TO_CORE.entries()) {
650             String type = entry.getKey();
651             String unit = entry.getValue();
652             if (NOT_CONVERTABLE.contains(unit)) {
653                 continue;
654             }
655             checkUnitConvertability(
656                     converter, compoundBaseUnit, badUnits, noQuantity, type, unit, testPrintout);
657         }
658         if (GENERATE_TESTS) { // test data
659             try (TempPrintWriter pw =
660                     TempPrintWriter.openUTF8Writer(
661                             CLDRPaths.TEST_DATA + "units", "unitsTest.txt")) {
662 
663                 pw.println(
664                         "# Test data for unit conversions\n"
665                                 + CldrUtility.getCopyrightString("#  ")
666                                 + "\n"
667                                 + "#\n"
668                                 + "# Format:\n"
669                                 + "#\tQuantity\t;\tx\t;\ty\t;\tconversion to y (rational)\t;\ttest: 1000 x ⟹ y\n"
670                                 + "#\n"
671                                 + "# Use: convert 1000 x units to the y unit; the result should match the final column,\n"
672                                 + "#   at the given precision. For example, when the last column is 159.1549,\n"
673                                 + "#   round to 4 decimal digits before comparing.\n"
674                                 + "# Note that certain conversions are approximate, such as degrees to radians\n"
675                                 + "#\n"
676                                 + "# Generation: Set GENERATE_TESTS in TestUnits.java to regenerate unitsTest.txt.\n");
677                 for (Entry<Pair<String, Double>, String> entry : testPrintout.entries()) {
678                     pw.println(entry.getValue());
679                 }
680             }
681         }
682         assertEquals("Unconvertable units", Collections.emptySet(), badUnits);
683         assertEquals("Units without Quantity", Collections.emptySet(), noQuantity);
684     }
685 
686     static final Set<String> NOT_CONVERTABLE = ImmutableSet.of("generic");
687 
checkUnitConvertability( UnitConverter converter, Output<String> compoundBaseUnit, Set<String> badUnits, Set<String> noQuantity, String type, String unit, Multimap<Pair<String, Double>, String> testPrintout)688     private void checkUnitConvertability(
689             UnitConverter converter,
690             Output<String> compoundBaseUnit,
691             Set<String> badUnits,
692             Set<String> noQuantity,
693             String type,
694             String unit,
695             Multimap<Pair<String, Double>, String> testPrintout) {
696 
697         if (converter.isBaseUnit(unit)) {
698             String quantity = converter.getQuantityFromBaseUnit(unit);
699             if (quantity == null) {
700                 noQuantity.add(unit);
701             }
702             if (GENERATE_TESTS) {
703                 testPrintout.put(
704                         new Pair<>(quantity, 1000d),
705                         quantity + "\t;\t" + unit + "\t;\t" + unit + "\t;\t1 * x\t;\t1,000.00");
706             }
707         } else {
708             ConversionInfo unitInfo = converter.getUnitInfo(unit, compoundBaseUnit);
709             if (unitInfo == null) {
710                 unitInfo = converter.parseUnitId(unit, compoundBaseUnit, false);
711             }
712             if (unitInfo == null) {
713                 badUnits.add(unit);
714             } else if (GENERATE_TESTS) {
715                 String quantity = converter.getQuantityFromBaseUnit(compoundBaseUnit.value);
716                 if (quantity == null) {
717                     noQuantity.add(compoundBaseUnit.value);
718                 }
719                 final double testValue =
720                         unitInfo.convert(R1000).toBigDecimal(MathContext.DECIMAL32).doubleValue();
721                 testPrintout.put(
722                         new Pair<>(quantity, testValue),
723                         quantity
724                                 + "\t;\t"
725                                 + unit
726                                 + "\t;\t"
727                                 + compoundBaseUnit
728                                 + "\t;\t"
729                                 + unitInfo
730                                 + "\t;\t"
731                                 + testValue
732                         //                    + "\t" +
733                         // unitInfo.factor.toBigDecimal(MathContext.DECIMAL32)
734                         //                    + "\t" +
735                         // unitInfo.factor.reciprocal().toBigDecimal(MathContext.DECIMAL32)
736                         );
737             }
738         }
739     }
740 
TestRational()741     public void TestRational() {
742         Rational a3_5 = Rational.of(3, 5);
743 
744         Rational a6_10 = Rational.of(6, 10);
745         assertEquals("", a3_5, a6_10);
746 
747         Rational a5_3 = Rational.of(5, 3);
748         assertEquals("", a3_5, a5_3.reciprocal());
749 
750         assertEquals("", Rational.ONE, a3_5.multiply(a3_5.reciprocal()));
751         assertEquals("", Rational.ZERO, a3_5.add(a3_5.negate()));
752 
753         assertEquals("", Rational.NEGATIVE_ONE, Rational.ONE.negate());
754 
755         assertEquals("", BigDecimal.valueOf(2), Rational.of(2, 1).toBigDecimal());
756         assertEquals("", BigDecimal.valueOf(0.5), Rational.of(1, 2).toBigDecimal());
757 
758         assertEquals("", BigDecimal.valueOf(100), Rational.of(100, 1).toBigDecimal());
759         assertEquals("", BigDecimal.valueOf(0.01), Rational.of(1, 100).toBigDecimal());
760 
761         assertEquals("", Rational.of(12370, 1), Rational.of(BigDecimal.valueOf(12370)));
762         assertEquals("", Rational.of(1237, 10), Rational.of(BigDecimal.valueOf(1237.0 / 10)));
763         assertEquals("", Rational.of(1237, 10000), Rational.of(BigDecimal.valueOf(1237.0 / 10000)));
764 
765         ConversionInfo uinfo = new ConversionInfo(Rational.of(2), Rational.of(3));
766         assertEquals("", Rational.of(3), uinfo.convert(Rational.ZERO));
767         assertEquals("", Rational.of(7), uinfo.convert(Rational.of(2)));
768 
769         assertEquals("", Rational.INFINITY, Rational.ZERO.reciprocal());
770         assertEquals("", Rational.NEGATIVE_INFINITY, Rational.INFINITY.negate());
771 
772         Set<Rational> anything =
773                 ImmutableSet.of(
774                         Rational.NaN,
775                         Rational.NEGATIVE_INFINITY,
776                         Rational.NEGATIVE_ONE,
777                         Rational.ZERO,
778                         Rational.ONE,
779                         Rational.INFINITY);
780         for (Rational something : anything) {
781             assertEquals("0/0", Rational.NaN, Rational.NaN.add(something));
782             assertEquals("0/0", Rational.NaN, Rational.NaN.subtract(something));
783             assertEquals("0/0", Rational.NaN, Rational.NaN.divide(something));
784             assertEquals("0/0", Rational.NaN, Rational.NaN.add(something));
785             assertEquals("0/0", Rational.NaN, Rational.NaN.negate());
786 
787             assertEquals("0/0", Rational.NaN, something.add(Rational.NaN));
788             assertEquals("0/0", Rational.NaN, something.subtract(Rational.NaN));
789             assertEquals("0/0", Rational.NaN, something.divide(Rational.NaN));
790             assertEquals("0/0", Rational.NaN, something.add(Rational.NaN));
791         }
792         assertEquals("0/0", Rational.NaN, Rational.ZERO.divide(Rational.ZERO));
793         assertEquals("INF-INF", Rational.NaN, Rational.INFINITY.subtract(Rational.INFINITY));
794         assertEquals("INF+-INF", Rational.NaN, Rational.INFINITY.add(Rational.NEGATIVE_INFINITY));
795         assertEquals("-INF+INF", Rational.NaN, Rational.NEGATIVE_INFINITY.add(Rational.INFINITY));
796         assertEquals("INF/INF", Rational.NaN, Rational.INFINITY.divide(Rational.INFINITY));
797 
798         assertEquals("INF+1", Rational.INFINITY, Rational.INFINITY.add(Rational.ONE));
799         assertEquals("INF-1", Rational.INFINITY, Rational.INFINITY.subtract(Rational.ONE));
800     }
801 
TestRationalParse()802     public void TestRationalParse() {
803         Rational.RationalParser parser = SDI.getRationalParser();
804 
805         Rational a3_5 = Rational.of(3, 5);
806 
807         assertEquals("", a3_5, parser.parse("6/10"));
808 
809         assertEquals("", a3_5, parser.parse("0.06/0.10"));
810 
811         assertEquals("", Rational.of(381, 1250), parser.parse("ft_to_m"));
812         assertEquals(
813                 "", 6.02214076E+23d, parser.parse("6.02214076E+23").toBigDecimal().doubleValue());
814         Rational temp = parser.parse("gal_to_m3");
815         // System.out.println(" " + temp);
816         assertEquals(
817                 "", 0.003785411784, temp.numerator.doubleValue() / temp.denominator.doubleValue());
818     }
819 
820     static final Map<String, String> CORE_TO_TYPE;
821     static final Multimap<String, String> TYPE_TO_CORE;
822 
823     static {
824         Set<String> VALID_UNITS =
825                 Validity.getInstance().getStatusToCodes(LstrType.unit).get(Status.regular);
826 
827         Map<String, String> coreToType = new TreeMap<>();
828         TreeMultimap<String, String> typeToCore = TreeMultimap.create();
829         for (String s : VALID_UNITS) {
830             int dashPos = s.indexOf('-');
831             String unitType = s.substring(0, dashPos);
832             String coreUnit = s.substring(dashPos + 1);
833             coreUnit = converter.fixDenormalized(coreUnit);
coreToType.put(coreUnit, unitType)834             coreToType.put(coreUnit, unitType);
typeToCore.put(unitType, coreUnit)835             typeToCore.put(unitType, coreUnit);
836         }
837         CORE_TO_TYPE = ImmutableMap.copyOf(coreToType);
838         TYPE_TO_CORE = ImmutableMultimap.copyOf(typeToCore);
839     }
840 
841     static final Map<String, String> quantityToCategory =
842             ImmutableMap.<String, String>builder()
843                     .put("acceleration", "acceleration")
844                     .put("angle", "angle")
845                     .put("area", "area")
846                     .put("catalytic-activity", "concentr")
847                     .put("concentration", "concentr")
848                     .put("concentration-mass", "concentr")
849                     .put("consumption", "consumption")
850                     .put("consumption-inverse", "consumption")
851                     .put("digital", "digital")
852                     .put("duration", "duration")
853                     .put("electric-capacitance", "electric")
854                     .put("electric-charge", "electric")
855                     .put("electric-conductance", "electric")
856                     .put("electric-current", "electric")
857                     .put("electric-inductance", "electric")
858                     .put("electric-resistance", "electric")
859                     .put("energy", "energy")
860                     .put("force", "force")
861                     .put("frequency", "frequency")
862                     .put("graphics", "graphics")
863                     .put("illuminance", "light")
864                     .put("ionizing-radiation", "energy")
865                     .put("length", "length")
866                     .put("luminous-flux", "light")
867                     .put("luminous-intensity", "light")
868                     .put("magnetic-flux", "magnetic")
869                     .put("magnetic-induction", "magnetic")
870                     .put("mass", "mass")
871                     .put("portion", "concentr")
872                     .put("power", "power")
873                     .put("pressure", "pressure")
874                     .put("pressure-per-length", "pressure")
875                     .put("radioactivity", "energy")
876                     .put("resolution", "graphics")
877                     .put("solid-angle", "angle")
878                     .put("speed", "speed")
879                     .put("substance-amount", "concentr")
880                     .put("temperature", "temperature")
881                     .put("typewidth", "graphics")
882                     .put("voltage", "electric")
883                     .put("volume", "volume")
884                     .put("year-duration", "duration")
885                     .build();
886 
887     // TODO Get rid of these exceptions.
888     // Some of the qualities are 'split' over categories, which ideally shouldn't happen.
889     static final Map<String, String> CATEGORY_EXCEPTIONS =
890             ImmutableMap.<String, String>builder()
891                     .put("dalton", "mass")
892                     .put("newton-meter", "torque")
893                     .put("pound-force-foot", "torque")
894                     .put("solar-luminosity", "light")
895                     .put("night", "duration")
896                     .build();
897 
TestUnitCategory()898     public void TestUnitCategory() {
899         Map<String, Multimap<String, String>> bad = new TreeMap<>();
900         for (Entry<String, String> entry : TYPE_TO_CORE.entries()) {
901             final String coreUnit = entry.getValue();
902             final String unitType = entry.getKey();
903             if (NOT_CONVERTABLE.contains(coreUnit)) {
904                 continue;
905             }
906             String quantity = converter.getQuantityFromUnit(coreUnit, false);
907             if (quantity == null) {
908                 converter.getQuantityFromUnit(coreUnit, true);
909                 errln("Null quantity " + coreUnit);
910             } else {
911                 String exception = CATEGORY_EXCEPTIONS.get(coreUnit);
912                 if (unitType.equals(exception)) {
913                     continue;
914                 }
915                 assertEquals(
916                         "Category for «" + coreUnit + "» with quality «" + quantity + "»",
917                         unitType,
918                         quantityToCategory.get(quantity));
919             }
920         }
921     }
922 
TestQuantities()923     public void TestQuantities() {
924         // put quantities in order
925         Multimap<String, String> quantityToBaseUnits = LinkedHashMultimap.create();
926 
927         Multimaps.invertFrom(Multimaps.forMap(BASE_UNIT_TO_QUANTITY), quantityToBaseUnits);
928 
929         for (Entry<String, Collection<String>> entry : quantityToBaseUnits.asMap().entrySet()) {
930             assertEquals(entry.toString(), 1, entry.getValue().size());
931         }
932 
933         TreeMultimap<String, String> quantityToConvertible = TreeMultimap.create();
934         Set<String> missing = new TreeSet<>(CORE_TO_TYPE.keySet());
935         missing.removeAll(NOT_CONVERTABLE);
936 
937         for (Entry<String, String> entry : BASE_UNIT_TO_QUANTITY.entrySet()) {
938             String baseUnit = entry.getKey();
939             String quantity = entry.getValue();
940             Set<String> convertible = converter.canConvertBetween(baseUnit);
941             missing.removeAll(convertible);
942             quantityToConvertible.putAll(quantity, convertible);
943         }
944 
945         // handle missing
946         for (String missingUnit : ImmutableSet.copyOf(missing)) {
947             if (missingUnit.equals("mile-per-gallon")) {
948                 int debug = 0;
949             }
950             String quantity = converter.getQuantityFromUnit(missingUnit, false);
951             if (quantity != null) {
952                 quantityToConvertible.put(quantity, missingUnit);
953                 missing.remove(missingUnit);
954             } else {
955                 quantity = converter.getQuantityFromUnit(missingUnit, true); // for debugging
956             }
957         }
958         assertEquals("all units have quantity", Collections.emptySet(), missing);
959 
960         if (SHOW_UNIT_CATEGORY) {
961             System.out.println();
962             for (Entry<String, String> entry : BASE_UNIT_TO_QUANTITY.entrySet()) {
963                 String baseUnit = entry.getKey();
964                 String quantity = entry.getValue();
965                 System.out.println(
966                         "        <unitQuantity"
967                                 + " baseUnit='"
968                                 + baseUnit
969                                 + "'"
970                                 + " quantity='"
971                                 + quantity
972                                 + "'"
973                                 + "/>");
974             }
975             System.out.println();
976             System.out.println("Quantities");
977             for (Entry<String, Collection<String>> entry :
978                     quantityToConvertible.asMap().entrySet()) {
979                 String quantity = entry.getKey();
980                 Collection<String> convertible = entry.getValue();
981                 System.out.println(quantity + "\t" + convertible);
982             }
983         }
984     }
985 
986     static final UnicodeSet ALLOWED_IN_COMPONENT = new UnicodeSet("[a-z0-9]").freeze();
987     static final Set<String> STILL_RECOGNIZED_SIMPLES =
988             ImmutableSet.of(
989                     "em",
990                     "g-force",
991                     "therm-us",
992                     "british-thermal-unit-it",
993                     "calorie-it",
994                     "bu-jp",
995                     "jo-jp",
996                     "ri-jp",
997                     "se-jp",
998                     "to-jp",
999                     "cup-jp");
1000 
TestOrder()1001     public void TestOrder() {
1002         if (SHOW_UNIT_ORDER) System.out.println();
1003         for (String s : UnitConverter.BASE_UNITS) {
1004             String quantity = converter.getQuantityFromBaseUnit(s);
1005             if (SHOW_UNIT_ORDER) {
1006                 System.out.println("\"" + quantity + "\",");
1007             }
1008         }
1009         for (String unit : CORE_TO_TYPE.keySet()) {
1010             if (!STILL_RECOGNIZED_SIMPLES.contains(unit)) {
1011                 for (String part : unit.split("-")) {
1012                     assertTrue(unit + " has no parts < 2 in length", part.length() > 2);
1013                     assertTrue(
1014                             unit + " has only allowed characters",
1015                             ALLOWED_IN_COMPONENT.containsAll(part));
1016                 }
1017             }
1018             if (unit.equals("generic")) {
1019                 continue;
1020             }
1021             String quantity = converter.getQuantityFromUnit(unit, false); // make sure doesn't crash
1022         }
1023     }
1024 
TestConversionLineOrder()1025     public void TestConversionLineOrder() {
1026         Map<String, TargetInfo> data = converter.getInternalConversionData();
1027         Multimap<TargetInfo, String> sorted =
1028                 TreeMultimap.create(converter.targetInfoComparator, Comparator.naturalOrder());
1029         Multimaps.invertFrom(Multimaps.forMap(data), sorted);
1030 
1031         String lastBase = "";
1032 
1033         // Test that sorted is in same order as the file.
1034         MapComparator<String> conversionOrder = new MapComparator<>(data.keySet());
1035         String lastUnit = null;
1036         Set<String> warnings = new LinkedHashSet<>();
1037         for (Entry<TargetInfo, String> entry : sorted.entries()) {
1038             final TargetInfo tInfo = entry.getKey();
1039             final String unit = entry.getValue();
1040             if (lastUnit != null) {
1041                 if (!(conversionOrder.compare(lastUnit, unit) < 0)) {
1042                     Output<String> metricUnit = new Output<>();
1043                     ConversionInfo lastInfo = converter.parseUnitId(lastUnit, metricUnit, false);
1044                     String lastMetric = metricUnit.value;
1045                     ConversionInfo info = converter.parseUnitId(unit, metricUnit, false);
1046                     String metric = metricUnit.value;
1047                     if (metric.equals(lastMetric)) {
1048                         warnings.add(
1049                                 "Expected "
1050                                         + lastUnit
1051                                         + " < "
1052                                         + unit
1053                                         + "\t"
1054                                         + lastMetric
1055                                         + " "
1056                                         + lastInfo
1057                                         + " < "
1058                                         + metric
1059                                         + " "
1060                                         + info);
1061                     }
1062                 }
1063             }
1064             lastUnit = unit;
1065             if (SHOW_UNIT_ORDER) {
1066                 if (!lastBase.equals(tInfo.target)) {
1067                     lastBase = tInfo.target;
1068                     System.out.println(
1069                             "\n      <!-- " + converter.getQuantityFromBaseUnit(lastBase) + " -->");
1070                 }
1071                 //  <convertUnit source='week-person' target='second' factor='604800'/>
1072                 System.out.println("        " + tInfo.formatOriginalSource(entry.getValue()));
1073             }
1074         }
1075         if (!warnings.isEmpty()) {
1076             warnln("Some units are not ordered by size, count=" + warnings.size());
1077         }
1078     }
1079 
TestSimplify()1080     public final void TestSimplify() {
1081         Set<Rational> seen = new HashSet<>();
1082         checkSimplify("ZERO", Rational.ZERO, seen);
1083         checkSimplify("ONE", Rational.ONE, seen);
1084         checkSimplify("NEGATIVE_ONE", Rational.NEGATIVE_ONE, seen);
1085         checkSimplify("INFINITY", Rational.INFINITY, seen);
1086         checkSimplify("NEGATIVE_INFINITY", Rational.NEGATIVE_INFINITY, seen);
1087         checkSimplify("NaN", Rational.NaN, seen);
1088 
1089         checkSimplify("Simplify", Rational.of(25, 300), seen);
1090         checkSimplify("Simplify", Rational.of(100, 1), seen);
1091         checkSimplify("Simplify", Rational.of(2, 5), seen);
1092         checkSimplify("Simplify", Rational.of(4, 25), seen);
1093         checkSimplify("Simplify", Rational.of(5, 2), seen);
1094         checkSimplify("Simplify", Rational.of(25, 4), seen);
1095 
1096         for (Entry<String, TargetInfo> entry : converter.getInternalConversionData().entrySet()) {
1097             final Rational factor = entry.getValue().unitInfo.factor;
1098             checkSimplify(entry.getKey(), factor, seen);
1099             if (!factor.equals(Rational.ONE)) {
1100                 checkSimplify(entry.getKey(), factor, seen);
1101             }
1102             final Rational offset = entry.getValue().unitInfo.offset;
1103             if (!offset.equals(Rational.ZERO)) {
1104                 checkSimplify(entry.getKey(), offset, seen);
1105             }
1106         }
1107     }
1108 
checkSimplify(String title, Rational expected, Set<Rational> seen)1109     private void checkSimplify(String title, Rational expected, Set<Rational> seen) {
1110         if (!seen.contains(expected)) {
1111             seen.add(expected);
1112             String simpleStr = expected.toString(FormatStyle.formatted);
1113             if (SHOW_DATA) System.out.println(title + ": " + expected + " => " + simpleStr);
1114             Rational actual = RationalParser.BASIC.parse(simpleStr);
1115             assertEquals("simplify", expected, actual);
1116         }
1117     }
1118 
1119     private static final Pattern usSystemPattern =
1120             Pattern.compile(
1121                     "\\b(lb_to_kg|ft_to_m|ft2_to_m2|ft3_to_m3|in3_to_m3|gal_to_m3|cup_to_m3)\\b");
1122     private static final Pattern ukSystemPattern =
1123             Pattern.compile("\\b(lb_to_kg|ft_to_m|ft2_to_m2|ft3_to_m3|in3_to_m3|gal_imp_to_m3)\\b");
1124 
1125     static final Set<String> OK_BOTH =
1126             ImmutableSet.of(
1127                     "ounce-troy",
1128                     "nautical-mile",
1129                     "fahrenheit",
1130                     "inch-ofhg",
1131                     "british-thermal-unit",
1132                     "foodcalorie",
1133                     "knot");
1134 
1135     static final Set<String> OK_US = ImmutableSet.of("therm-us", "bushel");
1136     static final Set<String> NOT_US = ImmutableSet.of("stone");
1137 
1138     static final Set<String> OK_UK = ImmutableSet.of();
1139     static final Set<String> NOT_UK = ImmutableSet.of("therm-us", "bushel", "barrel");
1140 
1141     public static final Set<String> OTHER_SYSTEM =
1142             ImmutableSet.of(
1143                     "g-force",
1144                     "dalton",
1145                     "calorie",
1146                     "earth-radius",
1147                     "solar-radius",
1148                     "solar-radius",
1149                     "astronomical-unit",
1150                     "light-year",
1151                     "parsec",
1152                     "earth-mass",
1153                     "solar-mass",
1154                     "bit",
1155                     "byte",
1156                     "karat",
1157                     "solar-luminosity",
1158                     "ofhg",
1159                     "atmosphere",
1160                     "pixel",
1161                     "dot",
1162                     "permillion",
1163                     "permyriad",
1164                     "permille",
1165                     "percent",
1166                     "karat",
1167                     "portion",
1168                     "minute",
1169                     "hour",
1170                     "day",
1171                     "day-person",
1172                     "week",
1173                     "week-person",
1174                     "year",
1175                     "year-person",
1176                     "decade",
1177                     "month",
1178                     "month-person",
1179                     "century",
1180                     "quarter",
1181                     "arc-second",
1182                     "arc-minute",
1183                     "degree",
1184                     "radian",
1185                     "revolution",
1186                     "electronvolt",
1187                     "beaufort",
1188                     // quasi-metric
1189                     "dunam",
1190                     "mile-scandinavian",
1191                     "carat",
1192                     "cup-metric",
1193                     "pint-metric");
1194 
TestSystems()1195     public void TestSystems() {
1196         final Logger logger = getLogger();
1197         //        Map<String, TargetInfo> data = converter.getInternalConversionData();
1198         Output<String> metricUnit = new Output<>();
1199         Multimap<Set<UnitSystem>, R3<String, ConversionInfo, String>> systemsToUnits =
1200                 TreeMultimap.create(
1201                         Comparators.lexicographical(Ordering.natural()), Ordering.natural());
1202         for (String longUnit : VALID_REGULAR_UNITS) {
1203             String unit = Units.getShort(longUnit);
1204             if (NOT_CONVERTABLE.contains(unit)) {
1205                 continue;
1206             }
1207             if (unit.contentEquals("centiliter")) {
1208                 int debug = 0;
1209             }
1210             Set<UnitSystem> systems = converter.getSystemsEnum(unit);
1211             ConversionInfo parseInfo = converter.parseUnitId(unit, metricUnit, false);
1212             String mUnit = metricUnit.value;
1213             final R3<String, ConversionInfo, String> row = Row.of(mUnit, parseInfo, unit);
1214             systemsToUnits.put(systems, row);
1215             //            if (systems.isEmpty()) {
1216             //                Rational factor = parseInfo.factor;
1217             //                if (factor.isPowerOfTen()) {
1218             //                    log("System should be 'metric': " + unit);
1219             //                } else {
1220             //                    log("System should be ???: " + unit);
1221             //                }
1222             //            }
1223         }
1224         String std = converter.getStandardUnit("kilogram-meter-per-square-meter-square-second");
1225         logger.fine("");
1226         Output<Rational> outFactor = new Output<>();
1227         for (Entry<Set<UnitSystem>, Collection<R3<String, ConversionInfo, String>>>
1228                 systemsAndUnits : systemsToUnits.asMap().entrySet()) {
1229             Set<UnitSystem> systems = systemsAndUnits.getKey();
1230             for (R3<String, ConversionInfo, String> unitInfo : systemsAndUnits.getValue()) {
1231                 String unit = unitInfo.get2();
1232                 switch (unit) {
1233                     case "gram":
1234                         continue;
1235                     case "kilogram":
1236                         break;
1237                     default:
1238                         String paredUnit = UnitConverter.stripPrefix(unit, outFactor);
1239                         if (!paredUnit.equals(unit)) {
1240                             continue;
1241                         }
1242                 }
1243                 final String metric = unitInfo.get0();
1244                 String standard = converter.getStandardUnit(metric);
1245                 final String quantity = converter.getQuantityFromUnit(unit, false);
1246                 final Rational factor = unitInfo.get1().factor;
1247                 // show non-metric relations
1248                 String specialRef = "";
1249                 String specialUnit = converter.getSpecialBaseUnit(quantity, systems);
1250                 if (specialUnit != null) {
1251                     Rational specialFactor =
1252                             converter.convert(Rational.ONE, unit, specialUnit, false);
1253                     specialRef = "\t" + specialFactor + "\t" + specialUnit;
1254                 }
1255                 logger.fine(
1256                         systems
1257                                 + "\t"
1258                                 + quantity
1259                                 + "\t"
1260                                 + unit
1261                                 + "\t"
1262                                 + factor
1263                                 + "\t"
1264                                 + standard
1265                                 + specialRef);
1266             }
1267         }
1268     }
1269 
TestTestFile()1270     public void TestTestFile() {
1271         File base = info.getCldrBaseDirectory();
1272         File testFile = new File(base, "common/testData/units/unitsTest.txt");
1273         Output<String> metricUnit = new Output<>();
1274         Stream<String> lines;
1275         try {
1276             lines = Files.lines(testFile.toPath());
1277         } catch (IOException e) {
1278             throw new ICUUncheckedIOException("Couldn't process " + testFile);
1279         }
1280         lines.forEach(
1281                 line -> {
1282                     // angle   ;   arc-second  ;   revolution  ;   1 / 1296000 * x ;   7.716049E-4
1283                     line = line.trim();
1284                     if (line.isEmpty() || line.charAt(0) == '#') {
1285                         return;
1286                     }
1287                     List<String> fields = SPLIT_SEMI.splitToList(line);
1288                     ConversionInfo unitInfo;
1289                     try {
1290                         unitInfo = converter.parseUnitId(fields.get(1), metricUnit, false);
1291                     } catch (Exception e1) {
1292                         throw new IllegalArgumentException("Couldn't access fields on " + line);
1293                     }
1294                     if (unitInfo == null) {
1295                         throw new IllegalArgumentException("Couldn't get unitInfo on " + line);
1296                     }
1297                     double expected;
1298                     try {
1299                         expected = Double.parseDouble(fields.get(4).replace(",", ""));
1300                     } catch (NumberFormatException e) {
1301                         errln("Can't parse double in: " + line);
1302                         return;
1303                     }
1304                     double actual =
1305                             unitInfo.convert(R1000)
1306                                     .toBigDecimal(MathContext.DECIMAL32)
1307                                     .doubleValue();
1308                     assertEquals(Joiner.on(" ; ").join(fields), expected, actual);
1309                 });
1310         lines.close();
1311     }
1312 
TestSpecialCases()1313     public void TestSpecialCases() {
1314         String[][] tests = {
1315             {"1", "millimole-per-liter", "milligram-ofglucose-per-deciliter", "18.01557"},
1316             {"1", "millimole-per-liter", "item-per-cubic-meter", "602214076000000000000000"},
1317             {"50", "foot", "x-foo", "0/0"},
1318             {"50", "x-foo", "mile", "0/0"},
1319             {"50", "foot", "second", "0/0"},
1320             {"50", "foot-per-x-foo", "mile-per-hour", "0/0"},
1321             {"50", "foot-per-minute", "mile", "0/0"},
1322             {"50", "foot-per-ampere", "mile-per-hour", "0/0"},
1323             {"50", "foot", "mile", "5 / 528"},
1324             {"50", "foot-per-minute", "mile-per-hour", "25 / 44"},
1325             {"50", "foot-per-minute", "hour-per-mile", "44 / 25"},
1326             {"50", "mile-per-gallon", "liter-per-100-kilometer", "112903 / 24000"},
1327             {"50", "celsius-per-second", "kelvin-per-second", "50"},
1328             {"50", "celsius-per-second", "fahrenheit-per-second", "90"},
1329             {
1330                 "50",
1331                 "pound-force",
1332                 "kilogram-meter-per-square-second",
1333                 "8896443230521 / 40000000000"
1334             },
1335             // Note: pound-foot-per-square-second is a pound-force divided by gravity
1336             {
1337                 "50",
1338                 "pound-foot-per-square-second",
1339                 "kilogram-meter-per-square-second",
1340                 "17281869297 / 2500000000"
1341             },
1342             {"1", "beaufort", "meter-per-second", "0.95"}, // 19/20
1343             {"4", "beaufort", "meter-per-second", "6.75"}, // 27/4
1344             {"7", "beaufort", "meter-per-second", "15.55"}, // 311/20
1345             {"10", "beaufort", "meter-per-second", "26.5"}, // 53/2
1346             {"13", "beaufort", "meter-per-second", "39.15"}, // 783/20
1347             {"1", "beaufort", "mile-per-hour", "11875 / 5588"}, // 2.125089...
1348             {"4", "beaufort", "mile-per-hour", "84375 / 5588"}, // 15.099319971367215
1349             {"7", "beaufort", "mile-per-hour", "194375 / 5588"}, // 34.784359341445956
1350             {"10", "beaufort", "mile-per-hour", "165625 / 2794"}, // 59.27881...
1351             {"13", "beaufort", "mile-per-hour", "489375 / 5588"}, // 87.576056...
1352             {"1", "meter-per-second", "beaufort", "1"},
1353             {"7", "meter-per-second", "beaufort", "4"},
1354             {"16", "meter-per-second", "beaufort", "7"},
1355             {"27", "meter-per-second", "beaufort", "10"},
1356             {"39", "meter-per-second", "beaufort", "13"},
1357         };
1358         int count = 0;
1359         for (String[] test : tests) {
1360             final Rational sourceValue = Rational.of(test[0]);
1361             final String sourceUnit = test[1];
1362             final String targetUnit = test[2];
1363             final Rational expectedValue = Rational.of(test[3]);
1364             final Rational conversion =
1365                     converter.convert(sourceValue, sourceUnit, targetUnit, SHOW_DATA);
1366             if (!assertEquals(
1367                     count++ + ") " + sourceValue + " " + sourceUnit + " ⟹ " + targetUnit,
1368                     expectedValue,
1369                     conversion)) {
1370                 converter.convert(sourceValue, sourceUnit, targetUnit, SHOW_DATA);
1371             }
1372         }
1373     }
1374 
1375     static Multimap<String, String> EXTRA_UNITS =
1376             ImmutableMultimap.<String, String>builder()
1377                     .putAll("area", "square-foot", "square-yard", "square-mile")
1378                     .putAll("volume", "cubic-inch", "cubic-foot", "cubic-yard")
1379                     .build();
1380 
TestEnglishSystems()1381     public void TestEnglishSystems() {
1382         Multimap<String, String> systemToUnits = TreeMultimap.create();
1383         for (String unit : converter.canConvert()) {
1384             Set<String> systems = converter.getSystems(unit);
1385             if (systems.isEmpty()) {
1386                 systemToUnits.put("other", unit);
1387             } else
1388                 for (String s : systems) {
1389                     systemToUnits.put(s, unit);
1390                 }
1391         }
1392         for (Entry<String, Collection<String>> systemAndUnits : systemToUnits.asMap().entrySet()) {
1393             String system = systemAndUnits.getKey();
1394             final Collection<String> units = systemAndUnits.getValue();
1395             printSystemUnits(system, units);
1396         }
1397     }
1398 
printSystemUnits(String system, Collection<String> units)1399     private void printSystemUnits(String system, Collection<String> units) {
1400         Multimap<String, String> quantityToUnits = TreeMultimap.create();
1401         boolean metric = system.equals("metric");
1402         for (String unit : units) {
1403             quantityToUnits.put(converter.getQuantityFromUnit(unit, false), unit);
1404         }
1405         for (Entry<String, Collection<String>> entry : quantityToUnits.asMap().entrySet()) {
1406             String quantity = entry.getKey();
1407             String baseUnit = converter.getBaseUnitToQuantity().inverse().get(quantity);
1408             Multimap<Rational, String> sorted = TreeMultimap.create();
1409             sorted.put(Rational.ONE, baseUnit);
1410             if (!metric) {
1411                 String englishBaseUnit = getEnglishBaseUnit(baseUnit);
1412                 addUnit(baseUnit, englishBaseUnit, sorted);
1413                 Collection<String> extras = EXTRA_UNITS.get(quantity);
1414                 if (extras != null) {
1415                     for (String unit2 : extras) {
1416                         addUnit(baseUnit, unit2, sorted);
1417                     }
1418                 }
1419             }
1420             for (String unit : entry.getValue()) {
1421                 addUnit(baseUnit, unit, sorted);
1422             }
1423             Set<String> comparableUnits = ImmutableSet.copyOf(sorted.values());
1424 
1425             if (SHOW_DATA) {
1426                 printUnits(system, quantity, comparableUnits);
1427             }
1428         }
1429     }
1430 
addUnit( String baseUnit, String englishBaseUnit, Multimap<Rational, String> sorted)1431     private void addUnit(
1432             String baseUnit, String englishBaseUnit, Multimap<Rational, String> sorted) {
1433         Rational value = converter.convert(Rational.ONE, englishBaseUnit, baseUnit, false);
1434         sorted.put(value, englishBaseUnit);
1435     }
1436 
printUnits(String system, String quantity, Set<String> comparableUnits)1437     private void printUnits(String system, String quantity, Set<String> comparableUnits) {
1438         System.out.print("\n" + system + "\t" + quantity);
1439         for (String targetUnit : comparableUnits) {
1440             System.out.print("\t" + targetUnit);
1441         }
1442         System.out.println();
1443         for (String sourceUnit : comparableUnits) {
1444             System.out.print("\t" + sourceUnit);
1445             for (String targetUnit : comparableUnits) {
1446                 Rational rational = converter.convert(Rational.ONE, sourceUnit, targetUnit, false);
1447                 System.out.print("\t" + rational.toBigDecimal(MathContext.DECIMAL64).doubleValue());
1448             }
1449             System.out.println();
1450         }
1451     }
1452 
getEnglishBaseUnit(String baseUnit)1453     private String getEnglishBaseUnit(String baseUnit) {
1454         return baseUnit.replace("kilogram", "pound").replace("meter", "foot");
1455     }
1456 
TestPI()1457     public void TestPI() {
1458         Rational PI = converter.getConstants().get("PI");
1459         double PID = PI.toBigDecimal(MathContext.DECIMAL128).doubleValue();
1460         final BigDecimal bigPi =
1461                 new BigDecimal("3.141592653589793238462643383279502884197169399375105820974944");
1462         double bigPiD = bigPi.doubleValue();
1463         assertEquals("pi accurate enough", bigPiD, PID);
1464 
1465         // also test continued fractions used in deriving values
1466 
1467         Object[][] tests0 = {
1468             {
1469                 new ContinuedFraction(0, 1, 5, 2, 2),
1470                 Rational.of(27, 32),
1471                 ImmutableList.of(
1472                         Rational.of(0), Rational.of(1), Rational.of(5, 6), Rational.of(11, 13))
1473             },
1474         };
1475         for (Object[] test : tests0) {
1476             ContinuedFraction source = (ContinuedFraction) test[0];
1477             Rational expected = (Rational) test[1];
1478             @SuppressWarnings("unchecked")
1479             List<Rational> expectedIntermediates = (List<Rational>) test[2];
1480             List<Rational> intermediates = new ArrayList<>();
1481             final Rational actual = source.toRational(intermediates);
1482             assertEquals("continued", expected, actual);
1483             assertEquals("continued", expectedIntermediates, intermediates);
1484         }
1485         Object[][] tests = {
1486             {Rational.of(3245, 1000), new ContinuedFraction(3, 4, 12, 4)},
1487             {Rational.of(39, 10), new ContinuedFraction(3, 1, 9)},
1488             {Rational.of(-3245, 1000), new ContinuedFraction(-4, 1, 3, 12, 4)},
1489         };
1490         for (Object[] test : tests) {
1491             Rational source = (Rational) test[0];
1492             ContinuedFraction expected = (ContinuedFraction) test[1];
1493             ContinuedFraction actual = new ContinuedFraction(source);
1494             assertEquals(source.toString(), expected, actual);
1495             assertEquals(actual.toString(), source, actual.toRational(null));
1496         }
1497 
1498         if (SHOW_DATA) {
1499             ContinuedFraction actual = new ContinuedFraction(Rational.of(bigPi));
1500             List<Rational> intermediates = new ArrayList<>();
1501             actual.toRational(intermediates);
1502             System.out.println("\nRational\tdec64\tdec128\tgood enough");
1503             System.out.println(
1504                     "Target\t"
1505                             + bigPi.round(MathContext.DECIMAL64)
1506                             + "x"
1507                             + "\t"
1508                             + bigPi.round(MathContext.DECIMAL128)
1509                             + "x"
1510                             + "\t"
1511                             + "delta");
1512             int goodCount = 0;
1513             for (Rational item : intermediates) {
1514                 final BigDecimal dec64 = item.toBigDecimal(MathContext.DECIMAL64);
1515                 final BigDecimal dec128 = item.toBigDecimal(MathContext.DECIMAL128);
1516                 final boolean goodEnough =
1517                         bigPiD == item.toBigDecimal(MathContext.DECIMAL128).doubleValue();
1518                 System.out.println(
1519                         item
1520                                 + "\t"
1521                                 + dec64
1522                                 + "x\t"
1523                                 + dec128
1524                                 + "x\t"
1525                                 + goodEnough
1526                                 + "\t"
1527                                 + item.toBigDecimal(MathContext.DECIMAL128).subtract(bigPi));
1528                 if (goodEnough && goodCount++ > 6) {
1529                     break;
1530                 }
1531             }
1532         }
1533     }
1534 
TestUnitPreferenceSource()1535     public void TestUnitPreferenceSource() {
1536         XMLSource xmlSource = new SimpleXMLSource("units");
1537         xmlSource.setNonInheriting(true);
1538         CLDRFile foo = new CLDRFile(xmlSource);
1539         foo.setDtdType(DtdType.supplementalData);
1540         UnitPreferences uprefs = new UnitPreferences();
1541         int order = 0;
1542         for (String line : FileUtilities.in(TestUnits.class, "UnitPreferenceSource.txt")) {
1543             line = line.trim();
1544             if (line.isEmpty() || line.startsWith("#")) {
1545                 continue;
1546             }
1547             List<String> items = SPLIT_SEMI.splitToList(line);
1548             try {
1549                 String quantity = items.get(0);
1550                 String usage = items.get(1);
1551                 String regionsStr = items.get(2);
1552                 List<String> regions = SPLIT_SPACE.splitToList(items.get(2));
1553                 String geqStr = items.get(3);
1554                 Rational geq = geqStr.isEmpty() ? Rational.ONE : Rational.of(geqStr);
1555                 String skeleton = items.get(4);
1556                 String unit = items.get(5);
1557                 uprefs.add(quantity, usage, regionsStr, geqStr, skeleton, unit);
1558                 String path = uprefs.getPath(order++, quantity, usage, regions, geq, skeleton);
1559                 xmlSource.putValueAtPath(path, unit);
1560             } catch (Exception e) {
1561                 errln("Failure on line: " + line + "; " + e.getMessage());
1562             }
1563         }
1564         if (SHOW_PREFS) {
1565             PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
1566             foo.write(out);
1567             out.flush();
1568         } else {
1569             warnln("Use  -DTestUnits:SHOW_PREFS to get the reformatted source");
1570         }
1571     }
1572 
1573     static final Joiner JOIN_SPACE = Joiner.on(' ');
1574 
checkUnitPreferences(UnitPreferences uprefs)1575     private void checkUnitPreferences(UnitPreferences uprefs) {
1576         Set<String> usages = new LinkedHashSet<>();
1577         for (Entry<String, Map<String, Multimap<Set<String>, UnitPreference>>> entry1 :
1578                 uprefs.getData().entrySet()) {
1579             String quantity = entry1.getKey();
1580 
1581             // Each of the quantities is valid.
1582             assertNotNull("quantity is convertible", converter.getBaseUnitFromQuantity(quantity));
1583 
1584             Map<String, Multimap<Set<String>, UnitPreference>> usageToRegionToUnitPreference =
1585                     entry1.getValue();
1586 
1587             // each of the quantities has a default usage
1588             assertTrue(
1589                     "Quantity " + quantity + " contains default usage",
1590                     usageToRegionToUnitPreference.containsKey("default"));
1591 
1592             for (Entry<String, Multimap<Set<String>, UnitPreference>> entry2 :
1593                     usageToRegionToUnitPreference.entrySet()) {
1594                 String usage = entry2.getKey();
1595                 final String quantityPlusUsage = quantity + "/" + usage;
1596                 Multimap<Set<String>, UnitPreference> regionsToUnitPreference = entry2.getValue();
1597                 usages.add(usage);
1598                 Set<Set<String>> regionSets = regionsToUnitPreference.keySet();
1599 
1600                 // all quantity + usage pairs must contain 001 (one exception)
1601                 assertTrue(
1602                         "For "
1603                                 + quantityPlusUsage
1604                                 + ", the set of sets of regions must contain 001",
1605                         regionSets.contains(WORLD_SET)
1606                                 || quantityPlusUsage.contentEquals("concentration/blood-glucose"));
1607 
1608                 // Check that regions don't overlap for same quantity/usage
1609                 Multimap<String, Set<String>> checkOverlap = LinkedHashMultimap.create();
1610                 for (Set<String> regionSet : regionsToUnitPreference.keySet()) {
1611                     for (String region : regionSet) {
1612                         checkOverlap.put(region, regionSet);
1613                     }
1614                 }
1615                 for (Entry<String, Collection<Set<String>>> entry :
1616                         checkOverlap.asMap().entrySet()) {
1617                     assertEquals(
1618                             quantityPlusUsage
1619                                     + ": regions must be in only one set: "
1620                                     + entry.getValue(),
1621                             1,
1622                             entry.getValue().size());
1623                 }
1624 
1625                 Set<String> systems = new TreeSet<>();
1626                 for (Entry<Set<String>, Collection<UnitPreference>> entry :
1627                         regionsToUnitPreference.asMap().entrySet()) {
1628                     Collection<UnitPreference> uPrefs = entry.getValue();
1629                     Set<String> regions = entry.getKey();
1630 
1631                     // reset these for every new set of regions
1632                     Rational lastSize = null;
1633                     String lastUnit = null;
1634                     Rational lastgeq = null;
1635                     systems.clear();
1636                     Set<String> lastRegions = null;
1637                     String unitQuantity = null;
1638 
1639                     preferences:
1640                     for (UnitPreference up : uPrefs) {
1641                         String topUnit = null;
1642                         if ("minute:second".equals(up.unit)) {
1643                             int debug = 0;
1644                         }
1645                         String lastQuantity = null;
1646                         Rational lastValue = null;
1647                         Rational geq = converter.parseRational(String.valueOf(up.geq));
1648 
1649                         // where we have an 'and' unit, get its information
1650                         for (String unit : SPLIT_AND.split(up.unit)) {
1651                             try {
1652                                 if (topUnit == null) {
1653                                     topUnit = unit;
1654                                 }
1655                                 unitQuantity = converter.getQuantityFromUnit(unit, false);
1656                             } catch (Exception e) {
1657                                 errln("Unit is not covertible: " + up.unit);
1658                                 continue preferences;
1659                             }
1660                             String baseUnit = converter.getBaseUnitFromQuantity(unitQuantity);
1661                             if (geq.compareTo(Rational.ZERO) < 0) {
1662                                 throw new IllegalArgumentException("geq must be > 0" + geq);
1663                             }
1664                             Rational value = converter.convert(Rational.ONE, unit, baseUnit, false);
1665                             if (lastQuantity != null) {
1666                                 int diff = value.compareTo(lastValue);
1667                                 if (diff >= 0) {
1668                                     throw new IllegalArgumentException(
1669                                             "Bad mixed unit; biggest unit must be first: "
1670                                                     + up.unit);
1671                                 }
1672                                 if (!lastQuantity.contentEquals(quantity)) {
1673                                     throw new IllegalArgumentException(
1674                                             "Inconsistent quantities for mixed unit: " + up.unit);
1675                                 }
1676                             }
1677                             lastValue = value;
1678                             lastQuantity = quantity;
1679                             systems.addAll(converter.getSystems(unit));
1680                         }
1681                         String baseUnit = converter.getBaseUnitFromQuantity(unitQuantity);
1682                         Rational size = converter.convert(up.geq, topUnit, baseUnit, false);
1683                         if (lastSize != null) { // ensure descending order
1684                             if (!assertTrue(
1685                                     "Successive items must be ≥ previous:\n\t"
1686                                             + quantityPlusUsage
1687                                             + "; unit: "
1688                                             + up.unit
1689                                             + "; size: "
1690                                             + size
1691                                             + "; regions: "
1692                                             + regions
1693                                             + "; lastUnit: "
1694                                             + lastUnit
1695                                             + "; lastSize: "
1696                                             + lastSize
1697                                             + "; lastRegions: "
1698                                             + lastRegions,
1699                                     size.compareTo(lastSize) <= 0)) {
1700                                 int debug = 0;
1701                             }
1702                         }
1703                         lastSize = size;
1704                         lastUnit = up.unit;
1705                         lastgeq = geq;
1706                         lastRegions = regions;
1707                         if (SHOW_DATA)
1708                             System.out.println(
1709                                     quantity
1710                                             + "\t"
1711                                             + usage
1712                                             + "\t"
1713                                             + regions
1714                                             + "\t"
1715                                             + up.geq
1716                                             + "\t"
1717                                             + up.unit
1718                                             + "\t"
1719                                             + up.skeleton);
1720                     }
1721                     // Check that last geq is ONE.
1722                     assertEquals(
1723                             usage
1724                                     + " + "
1725                                     + regions
1726                                     + ": the least unit must have geq=1 (or equivalently, no geq)",
1727                             Rational.ONE,
1728                             lastgeq);
1729 
1730                     // Check that each set has a consistent system.
1731                     assertTrue(
1732                             usage
1733                                     + " + "
1734                                     + regions
1735                                     + " has mixed systems: "
1736                                     + systems
1737                                     + "\n\t"
1738                                     + uPrefs,
1739                             areConsistent(systems, unitQuantity));
1740                 }
1741             }
1742         }
1743     }
1744 
areConsistent(Set<String> systems, String unitQuantity)1745     private boolean areConsistent(Set<String> systems, String unitQuantity) {
1746         return unitQuantity.equals("duration")
1747                 || !(systems.contains("metric")
1748                         && (systems.contains("ussystem") || systems.contains("uksystem")));
1749     }
1750 
TestBcp47()1751     public void TestBcp47() {
1752         checkBcp47("Quantity", converter.getQuantities(), lowercaseAZ, false);
1753         checkBcp47("Usage", SDI.getUnitPreferences().getUsages(), lowercaseAZ09, true);
1754         checkBcp47("Unit", converter.getSimpleUnits(), lowercaseAZ09, true);
1755     }
1756 
checkBcp47( String identifierType, Set<String> identifiers, UnicodeSet allowed, boolean allowHyphens)1757     private void checkBcp47(
1758             String identifierType,
1759             Set<String> identifiers,
1760             UnicodeSet allowed,
1761             boolean allowHyphens) {
1762         Output<Integer> counter = new Output<>(0);
1763         Multimap<String, String> truncatedToFullIdentifier = TreeMultimap.create();
1764         final Set<String> simpleUnits = identifiers;
1765         for (String unit : simpleUnits) {
1766             if (!allowHyphens && unit.contains("-")) {
1767                 truncatedToFullIdentifier.put(unit, "-");
1768             }
1769             checkBcp47(counter, identifierType, unit, allowed, truncatedToFullIdentifier);
1770         }
1771         for (Entry<String, Collection<String>> entry :
1772                 truncatedToFullIdentifier.asMap().entrySet()) {
1773             Set<String> identifierSet = ImmutableSet.copyOf(entry.getValue());
1774             assertEquals(
1775                     identifierType + ": truncated identifier " + entry.getKey() + " must be unique",
1776                     ImmutableSet.of(identifierSet.iterator().next()),
1777                     identifierSet);
1778         }
1779     }
1780 
1781     private static int MIN_SUBTAG_LENGTH = 3;
1782     private static int MAX_SUBTAG_LENGTH = 8;
1783 
1784     static final UnicodeSet lowercaseAZ = new UnicodeSet("[a-z]").freeze();
1785     static final UnicodeSet lowercaseAZ09 = new UnicodeSet("[a-z0-9]").freeze();
1786 
checkBcp47( Output<Integer> counter, String title, String identifier, UnicodeSet allowed, Multimap<String, String> truncatedToFullIdentifier)1787     private void checkBcp47(
1788             Output<Integer> counter,
1789             String title,
1790             String identifier,
1791             UnicodeSet allowed,
1792             Multimap<String, String> truncatedToFullIdentifier) {
1793         StringBuilder shortIdentifer = new StringBuilder();
1794         boolean fail = false;
1795         for (String subtag : identifier.split("-")) {
1796             assertTrue(
1797                     ++counter.value
1798                             + ") "
1799                             + title
1800                             + " identifier="
1801                             + identifier
1802                             + " subtag="
1803                             + subtag
1804                             + " has right characters",
1805                     allowed.containsAll(subtag));
1806             if (!(subtag.length() >= MIN_SUBTAG_LENGTH && subtag.length() <= MAX_SUBTAG_LENGTH)) {
1807                 for (Entry<String, Rational> entry : UnitConverter.PREFIXES.entrySet()) {
1808                     String prefix = entry.getKey();
1809                     if (subtag.startsWith(prefix)) {
1810                         subtag = subtag.substring(prefix.length());
1811                         break;
1812                     }
1813                 }
1814             }
1815             if (shortIdentifer.length() != 0) {
1816                 shortIdentifer.append('-');
1817             }
1818             if (subtag.length() > MAX_SUBTAG_LENGTH) {
1819                 shortIdentifer.append(subtag.substring(0, MAX_SUBTAG_LENGTH));
1820                 fail = true;
1821             } else {
1822                 shortIdentifer.append(subtag);
1823             }
1824         }
1825         if (fail) {
1826             String shortIdentiferStr = shortIdentifer.toString();
1827             truncatedToFullIdentifier.put(shortIdentiferStr, identifier);
1828         }
1829     }
1830 
TestUnitPreferences()1831     public void TestUnitPreferences() {
1832         warnln(
1833                 "If this fails, check the output of TestUnitPreferencesSource (with -DTestUnits:SHOW_DATA), fix as needed, then incorporate.");
1834         UnitPreferences prefs = SDI.getUnitPreferences();
1835         checkUnitPreferences(prefs);
1836 
1837         if (GENERATE_TESTS) {
1838             try (TempPrintWriter pw =
1839                     TempPrintWriter.openUTF8Writer(
1840                             CLDRPaths.TEST_DATA + "units", "unitPreferencesTest.txt")) {
1841 
1842                 pw.println(
1843                         "\n# Test data for unit preferences\n"
1844                                 + CldrUtility.getCopyrightString("#  ")
1845                                 + "\n"
1846                                 + "#\n"
1847                                 + "# Format:\n"
1848                                 + "#\tQuantity;\tUsage;\tRegion;\tInput (r);\tInput (d);\tInput Unit;\tOutput (r);\tOutput (d);\tOutput Unit\n"
1849                                 + "#\n"
1850                                 + "# Use: Convert the Input amount & unit according to the Usage and Region.\n"
1851                                 + "#\t The result should match the Output amount and unit.\n"
1852                                 + "#\t Both rational (r) and double64 (d) forms of the input and output amounts are supplied so that implementations\n"
1853                                 + "#\t have two options for testing based on the precision in their implementations. For example:\n"
1854                                 + "#\t   3429 / 12500; 0.27432; meter;\n"
1855                                 + "#\t The Output amount and Unit are repeated for mixed units. In such a case, only the smallest unit will have\n"
1856                                 + "#\t both a rational and decimal amount; the others will have a single integer value, such as:\n"
1857                                 + "#\t   length; person-height; CA; 3429 / 12500; 0.27432; meter; 2; foot; 54 / 5; 10.8; inch\n"
1858                                 + "#\t The input and output units are unit identifers; in particular, the output does not have further processing:\n"
1859                                 + "#\t\t • no localization\n"
1860                                 + "#\t\t • no adjustment for pluralization\n"
1861                                 + "#\t\t • no formatted with the skeleton\n"
1862                                 + "#\t\t • no suppression of zero values (for secondary -and- units such as pound in stone-and-pound)\n"
1863                                 + "#\n"
1864                                 + "# Generation: Set GENERATE_TESTS in TestUnits.java to regenerate unitPreferencesTest.txt.\n");
1865                 Rational ONE_TENTH = Rational.of(1, 10);
1866 
1867                 // Note that for production usage, precomputed data like the
1868                 // prefs.getFastMap(converter) would be used instead of the raw data.
1869 
1870                 for (Entry<String, Map<String, Multimap<Set<String>, UnitPreference>>> entry :
1871                         prefs.getData().entrySet()) {
1872                     String quantity = entry.getKey();
1873                     String baseUnit = converter.getBaseUnitFromQuantity(quantity);
1874                     for (Entry<String, Multimap<Set<String>, UnitPreference>> entry2 :
1875                             entry.getValue().entrySet()) {
1876                         String usage = entry2.getKey();
1877 
1878                         // collect samples of base units
1879                         for (Entry<Set<String>, Collection<UnitPreference>> entry3 :
1880                                 entry2.getValue().asMap().entrySet()) {
1881                             boolean first = true;
1882                             Set<Rational> samples = new TreeSet<>(Comparator.reverseOrder());
1883                             for (UnitPreference pref : entry3.getValue()) {
1884                                 final String topUnit =
1885                                         UnitPreferences.SPLIT_AND
1886                                                 .split(pref.unit)
1887                                                 .iterator()
1888                                                 .next();
1889                                 if (first) {
1890                                     samples.add(
1891                                             converter.convert(
1892                                                     pref.geq.add(ONE_TENTH),
1893                                                     topUnit,
1894                                                     baseUnit,
1895                                                     false));
1896                                     first = false;
1897                                 }
1898                                 samples.add(converter.convert(pref.geq, topUnit, baseUnit, false));
1899                                 samples.add(
1900                                         converter.convert(
1901                                                 pref.geq.subtract(ONE_TENTH),
1902                                                 topUnit,
1903                                                 baseUnit,
1904                                                 false));
1905                             }
1906                             // show samples
1907                             Set<String> regions = entry3.getKey();
1908                             String sampleRegion = regions.iterator().next();
1909                             Collection<UnitPreference> uprefs = entry3.getValue();
1910                             for (Rational sample : samples) {
1911                                 showSample(
1912                                         quantity,
1913                                         usage,
1914                                         sampleRegion,
1915                                         sample,
1916                                         baseUnit,
1917                                         uprefs,
1918                                         pw);
1919                             }
1920                             pw.println();
1921                         }
1922                     }
1923                 }
1924             }
1925         }
1926     }
1927 
showSample( String quantity, String usage, String sampleRegion, Rational sampleBaseValue, String baseUnit, Collection<UnitPreference> prefs, TempPrintWriter pw)1928     private void showSample(
1929             String quantity,
1930             String usage,
1931             String sampleRegion,
1932             Rational sampleBaseValue,
1933             String baseUnit,
1934             Collection<UnitPreference> prefs,
1935             TempPrintWriter pw) {
1936         String lastUnit = null;
1937         boolean gotOne = false;
1938         for (UnitPreference pref : prefs) {
1939             final String topUnit = UnitPreferences.SPLIT_AND.split(pref.unit).iterator().next();
1940             Rational baseGeq = converter.convert(pref.geq, topUnit, baseUnit, false);
1941             if (sampleBaseValue.compareTo(baseGeq) >= 0) {
1942                 showSample2(
1943                         quantity, usage, sampleRegion, sampleBaseValue, baseUnit, pref.unit, pw);
1944                 gotOne = true;
1945                 break;
1946             }
1947             lastUnit = pref.unit;
1948         }
1949         if (!gotOne) {
1950             showSample2(quantity, usage, sampleRegion, sampleBaseValue, baseUnit, lastUnit, pw);
1951         }
1952     }
1953 
showSample2( String quantity, String usage, String sampleRegion, Rational sampleBaseValue, String baseUnit, String lastUnit, TempPrintWriter pw)1954     private void showSample2(
1955             String quantity,
1956             String usage,
1957             String sampleRegion,
1958             Rational sampleBaseValue,
1959             String baseUnit,
1960             String lastUnit,
1961             TempPrintWriter pw) {
1962         Rational originalSampleBaseValue = sampleBaseValue;
1963         // Known slow algorithm for mixed values, but for generating tests we don't care.
1964         final List<String> units = UnitPreferences.SPLIT_AND.splitToList(lastUnit);
1965         StringBuilder formattedUnit = new StringBuilder();
1966         int remaining = units.size();
1967         for (String unit : units) {
1968             --remaining;
1969             Rational sample = converter.convert(sampleBaseValue, baseUnit, unit, false);
1970             if (formattedUnit.length() != 0) {
1971                 formattedUnit.append(TEST_SEP);
1972             }
1973             if (remaining != 0) {
1974                 BigInteger floor = sample.floor();
1975                 formattedUnit.append(floor + TEST_SEP + unit);
1976                 // convert back to base unit
1977                 sampleBaseValue =
1978                         converter.convert(
1979                                 sample.subtract(Rational.of(floor)), unit, baseUnit, false);
1980             } else {
1981                 formattedUnit.append(sample + TEST_SEP + sample.doubleValue() + TEST_SEP + unit);
1982             }
1983         }
1984         pw.println(
1985                 quantity
1986                         + TEST_SEP
1987                         + usage
1988                         + TEST_SEP
1989                         + sampleRegion
1990                         + TEST_SEP
1991                         + originalSampleBaseValue
1992                         + TEST_SEP
1993                         + originalSampleBaseValue.doubleValue()
1994                         + TEST_SEP
1995                         + baseUnit
1996                         + TEST_SEP
1997                         + formattedUnit);
1998     }
1999 
TestWithExternalData()2000     public void TestWithExternalData() throws IOException {
2001 
2002         Multimap<String, ExternalUnitConversionData> seen = HashMultimap.create();
2003         Set<ExternalUnitConversionData> cantConvert = new LinkedHashSet<>();
2004         Map<ExternalUnitConversionData, Rational> convertDiff = new LinkedHashMap<>();
2005         Set<String> remainingCldrUnits =
2006                 new LinkedHashSet<>(converter.getInternalConversionData().keySet());
2007         Set<ExternalUnitConversionData> couldAdd = new LinkedHashSet<>();
2008 
2009         if (SHOW_DATA) {
2010             System.out.println();
2011         }
2012         Set<ExternalUnitConversionData> unconvertible = new LinkedHashSet<>();
2013 
2014         for (ExternalUnitConversionData data : NistUnits.externalConversionData) {
2015             Rational externalResult = data.info.convert(Rational.ONE);
2016             Rational cldrResult;
2017             try {
2018                 cldrResult = converter.convert(Rational.ONE, data.source, data.target, false);
2019             } catch (Exception e) {
2020                 unconvertible.add(data);
2021                 continue;
2022             }
2023             seen.put(data.source + "⟹" + data.target, data);
2024 
2025             if (externalResult.isPowerOfTen()) {
2026                 couldAdd.add(data);
2027             }
2028 
2029             if (cldrResult.equals(Rational.NaN)) {
2030                 cantConvert.add(data);
2031             } else {
2032                 if (!cldrResult.approximatelyEquals(externalResult)) {
2033                     convertDiff.put(data, cldrResult);
2034                 } else {
2035                     remainingCldrUnits.remove(data.source);
2036                     remainingCldrUnits.remove(data.target);
2037                     if (SHOW_DATA)
2038                         System.out.println(
2039                                 "*Converted"
2040                                         + "\t"
2041                                         + cldrResult.doubleValue()
2042                                         + "\t"
2043                                         + externalResult.doubleValue()
2044                                         + "\t"
2045                                         + cldrResult.symmetricDiff(externalResult).doubleValue()
2046                                         + "\t"
2047                                         + data);
2048                 }
2049             }
2050         }
2051 
2052         if (!unconvertible.isEmpty()) {
2053             warnln("Unconvertible " + Joiner.on(", ").join(unconvertible));
2054         }
2055 
2056         // get additional data on derived units
2057         //        for (Entry<String, TargetInfo> e : NistUnits.derivedUnitToConversion.entrySet()) {
2058         //            String sourceUnit = e.getKey();
2059         //            TargetInfo targetInfo = e.getValue();
2060         //
2061         //            Rational conversion = converter.convert(Rational.ONE, sourceUnit,
2062         // targetInfo.target, false);
2063         //            if (conversion.equals(Rational.NaN)) {
2064         //                couldAdd.add(new ExternalUnitConversionData("", sourceUnit,
2065         // targetInfo.target, conversion, "?", null));
2066         //            }
2067         //        }
2068         if (SHOW_DATA) {
2069             for (Entry<String, Collection<String>> e :
2070                     NistUnits.unitToQuantity.asMap().entrySet()) {
2071                 System.out.println("*Quantities:" + "\t" + e.getKey() + "\t" + e.getValue());
2072             }
2073         }
2074 
2075         // check for missing external data
2076 
2077         int unitsWithoutExternalCheck = 0;
2078         if (SHOW_MISSING_TEST_DATA && !remainingCldrUnits.isEmpty()) {
2079             System.out.println("\nNot tested against external data");
2080         }
2081         for (String remainingUnit : remainingCldrUnits) {
2082             ExternalUnitConversionData external = NistUnits.unitToData.get(remainingUnit);
2083             final TargetInfo targetInfo = converter.getInternalConversionData().get(remainingUnit);
2084             if (!targetInfo.target.contentEquals(remainingUnit)) {
2085                 if (SHOW_MISSING_TEST_DATA) {
2086                     printlnIfZero(unitsWithoutExternalCheck);
2087                     System.out.println(
2088                             remainingUnit
2089                                     + "\t"
2090                                     + targetInfo.unitInfo.factor.doubleValue()
2091                                     + "\t"
2092                                     + targetInfo.target);
2093                 }
2094                 unitsWithoutExternalCheck++;
2095             }
2096         }
2097         if (unitsWithoutExternalCheck != 0 && !SHOW_MISSING_TEST_DATA) {
2098             warnln(
2099                     unitsWithoutExternalCheck
2100                             + " units without external data verification.  Use -DTestUnits:SHOW_MISSING_TEST_DATA for details.");
2101         }
2102 
2103         boolean showDiagnostics = false;
2104         for (Entry<String, Collection<ExternalUnitConversionData>> entry :
2105                 seen.asMap().entrySet()) {
2106             if (entry.getValue().size() != 1) {
2107                 Multimap<ConversionInfo, ExternalUnitConversionData> factors =
2108                         HashMultimap.create();
2109                 for (ExternalUnitConversionData s : entry.getValue()) {
2110                     factors.put(s.info, s);
2111                 }
2112                 if (factors.keySet().size() > 1) {
2113                     for (ExternalUnitConversionData s : entry.getValue()) {
2114                         errln("*DUP-" + s);
2115                         showDiagnostics = true;
2116                     }
2117                 }
2118             }
2119         }
2120 
2121         if (convertDiff.size() > 0) {
2122             for (Entry<ExternalUnitConversionData, Rational> e : convertDiff.entrySet()) {
2123                 final Rational computed = e.getValue();
2124                 final ExternalUnitConversionData external = e.getKey();
2125                 Rational externalResult = external.info.convert(Rational.ONE);
2126                 showDiagnostics = true;
2127                 // for debugging
2128                 converter.convert(Rational.ONE, external.source, external.target, true);
2129 
2130                 errln(
2131                         "*DIFF CONVERT:"
2132                                 + "\t"
2133                                 + external.source
2134                                 + "\t⟹\t"
2135                                 + external.target
2136                                 + "\texpected\t"
2137                                 + externalResult.doubleValue()
2138                                 + "\tactual:\t"
2139                                 + computed.doubleValue()
2140                                 + "\tsdiff:\t"
2141                                 + computed.symmetricDiff(externalResult).abs().doubleValue()
2142                                 + "\txdata:\t"
2143                                 + external);
2144             }
2145         }
2146 
2147         // temporary: show the items that didn't covert correctly
2148         if (showDiagnostics) {
2149             System.out.println();
2150             Rational x = showDelta("pound-fahrenheit", "gram-celsius", false);
2151             Rational y = showDelta("calorie", "joule", false);
2152             showDelta("product\t", x.multiply(y));
2153             showDelta("british-thermal-unit", "calorie", false);
2154             showDelta("inch-ofhg", "pascal", false);
2155             showDelta("millimeter-ofhg", "pascal", false);
2156             showDelta("ofhg", "kilogram-per-square-meter-square-second", false);
2157             showDelta("13595.1*gravity", Rational.of("9.80665*13595.1"));
2158 
2159             showDelta(
2160                     "fahrenheit-hour-square-foot-per-british-thermal-unit-inch",
2161                     "meter-kelvin-per-watt",
2162                     true);
2163         }
2164 
2165         if (showDiagnostics && NistUnits.skipping.size() > 0) {
2166             System.out.println();
2167             for (String s : NistUnits.skipping) {
2168                 System.out.println("*SKIPPING " + s);
2169             }
2170         }
2171         if (showDiagnostics && NistUnits.idChanges.size() > 0) {
2172             System.out.println();
2173             for (Entry<String, Collection<String>> e : NistUnits.idChanges.asMap().entrySet()) {
2174                 if (SHOW_DATA)
2175                     System.out.println(
2176                             "*CHANGES\t" + e.getKey() + "\t" + Joiner.on('\t').join(e.getValue()));
2177             }
2178         }
2179 
2180         if (showDiagnostics && cantConvert.size() > 0) {
2181             System.out.println();
2182             for (ExternalUnitConversionData e : cantConvert) {
2183                 System.out.println("*CANT CONVERT-" + e);
2184             }
2185         }
2186         Output<String> baseUnit = new Output<>();
2187         for (ExternalUnitConversionData s : couldAdd) {
2188             String target = s.target;
2189             Rational endFactor = s.info.factor;
2190             String mark = "";
2191             TargetInfo baseUnit2 = NistUnits.derivedUnitToConversion.get(s.target);
2192             if (baseUnit2 != null) {
2193                 target = baseUnit2.target;
2194                 endFactor = baseUnit2.unitInfo.factor;
2195                 mark = "¹";
2196             } else {
2197                 ConversionInfo conversionInfo = converter.getUnitInfo(s.target, baseUnit);
2198                 if (conversionInfo != null && !s.target.equals(baseUnit.value)) {
2199                     target = baseUnit.value;
2200                     endFactor = conversionInfo.convert(s.info.factor);
2201                     mark = "²";
2202                 }
2203             }
2204             //            if (SHOW_DATA)
2205             //                System.out.println(
2206             //                    "Could add 10^X conversion from a"
2207             //                        + "\t"
2208             //                        + s.source
2209             //                        + "\tto"
2210             //                        + mark
2211             //                        + "\t"
2212             //                        + endFactor.toString(FormatStyle.simple)
2213             //                        + "\t"
2214             //                        + target);
2215         }
2216         warnln("Use GenerateNewUnits.java to show units we could add from NIST.");
2217     }
2218 
showDelta(String firstUnit, String secondUnit, boolean showYourWork)2219     private Rational showDelta(String firstUnit, String secondUnit, boolean showYourWork) {
2220         Rational x = converter.convert(Rational.ONE, firstUnit, secondUnit, showYourWork);
2221         return showDelta(firstUnit + "\t" + secondUnit, x);
2222     }
2223 
showDelta(final String title, Rational rational)2224     private Rational showDelta(final String title, Rational rational) {
2225         System.out.print("*CONST\t" + title);
2226         System.out.print("\t" + rational.toString(FormatStyle.formatted));
2227         System.out.println("\t" + rational.doubleValue());
2228         return rational;
2229     }
2230 
TestRepeating()2231     public void TestRepeating() {
2232         Set<Rational> seen = new HashSet<>();
2233         String[][] tests = {
2234             {"0/0", "NaN"},
2235             {"1/0", "INF"},
2236             {"-1/0", "-INF"},
2237             {"0/1", "0"},
2238             {"1/1", "1"},
2239             {"1/2", "0.5"},
2240             {"1/3", "0.˙3"},
2241             {"1/4", "0.25"},
2242             {"1/5", "0.2"},
2243             {"1/6", "0.1˙6"},
2244             {"1/7", "0.˙142857"},
2245             {"1/8", "0.125"},
2246             {"1/9", "0.˙1"},
2247             {"1/10", "0.1"},
2248             {"1/11", "0.˙09"},
2249             {"1/12", "0.08˙3"},
2250             {"1/13", "0.˙076923"},
2251             {"1/14", "0.0˙714285"},
2252             {"1/15", "0.0˙6"},
2253             {"1/16", "0.0625"},
2254         };
2255         for (String[] test : tests) {
2256             Rational source = Rational.of(test[0]);
2257             seen.add(source);
2258             String expected = test[1];
2259             String actual = source.toString(FormatStyle.repeating);
2260             assertEquals(test[0], expected, actual);
2261             Rational roundtrip = Rational.of(expected);
2262             assertEquals(expected, source, roundtrip);
2263         }
2264         for (int i = -50; i < 200; ++i) {
2265             for (int j = 0; j < 50; ++j) {
2266                 checkFormat(Rational.of(i, j), seen);
2267             }
2268         }
2269         for (Entry<String, TargetInfo> unitAndInfo :
2270                 converter.getInternalConversionData().entrySet()) {
2271             final TargetInfo targetInfo2 = unitAndInfo.getValue();
2272             ConversionInfo targetInfo = targetInfo2.unitInfo;
2273             checkFormat(targetInfo.factor, seen);
2274             if (SHOW_DATA) {
2275                 String rFormat = targetInfo.factor.toString(FormatStyle.repeating);
2276                 String sFormat = targetInfo.factor.toString(FormatStyle.formatted);
2277                 if (!rFormat.equals(sFormat)) {
2278                     System.out.println(
2279                             "\t\t"
2280                                     + unitAndInfo.getKey()
2281                                     + "\t"
2282                                     + targetInfo2.target
2283                                     + "\t"
2284                                     + sFormat
2285                                     + "\t"
2286                                     + rFormat
2287                                     + "\t"
2288                                     + targetInfo.factor.doubleValue());
2289                 }
2290             }
2291         }
2292     }
2293 
checkFormat(Rational source, Set<Rational> seen)2294     private void checkFormat(Rational source, Set<Rational> seen) {
2295         if (seen.contains(source)) {
2296             return;
2297         }
2298         seen.add(source);
2299         String formatted = source.toString(FormatStyle.repeating);
2300         Rational roundtrip = Rational.of(formatted);
2301         assertEquals("roundtrip " + formatted, source, roundtrip);
2302     }
2303 
2304     /** Verify that the items in the validity files match those in the units.xml files */
TestValidityAgainstUnitFile()2305     public void TestValidityAgainstUnitFile() {
2306         Set<String> simpleUnits = converter.getSimpleUnits();
2307         final SetView<String> simpleUnitsRemoveAllValidity =
2308                 Sets.difference(simpleUnits, VALID_SHORT_UNITS);
2309         if (!assertEquals(
2310                 "Simple Units removeAll Validity",
2311                 Collections.emptySet(),
2312                 simpleUnitsRemoveAllValidity)) {
2313             for (String s : simpleUnitsRemoveAllValidity) {
2314                 System.out.println(s);
2315             }
2316         }
2317 
2318         // aliased units
2319         Map<String, R2<List<String>, String>> aliasedUnits = SDI.getLocaleAliasInfo().get("unit");
2320         // TODO adjust
2321         //        final SetView<String> aliasedRemoveAllDeprecated =
2322         // Sets.difference(aliasedUnits.keySet(), DEPRECATED_SHORT_UNITS);
2323         //        if (!assertEquals("aliased Units removeAll deprecated", Collections.emptySet(),
2324         // aliasedRemoveAllDeprecated)) {
2325         //            for (String s : aliasedRemoveAllDeprecated) {
2326         //                System.out.println(converter.getLongId(s));
2327         //            }
2328         //        }
2329         assertEquals(
2330                 "deprecated removeAll aliased Units",
2331                 Collections.emptySet(),
2332                 Sets.difference(DEPRECATED_SHORT_UNITS, aliasedUnits.keySet()));
2333     }
2334 
2335     /** Check that units to be translated are as expected. */
testDistinguishedSetsOfUnits()2336     public void testDistinguishedSetsOfUnits() {
2337         Set<String> comparatorUnitIds = new LinkedHashSet<>(DtdData.getUnitOrder().getOrder());
2338         Set<String> validLongUnitIds = VALID_REGULAR_UNITS;
2339         Set<String> validAndDeprecatedLongUnitIds =
2340                 ImmutableSet.<String>builder()
2341                         .addAll(VALID_REGULAR_UNITS)
2342                         .addAll(DEPRECATED_REGULAR_UNITS)
2343                         .build();
2344 
2345         final BiMap<String, String> shortToLong = Units.LONG_TO_SHORT.inverse();
2346         assertSuperset(
2347                 "converter short-long",
2348                 "units short-long",
2349                 converter.SHORT_TO_LONG_ID.entrySet(),
2350                 shortToLong.entrySet());
2351         assertSuperset(
2352                 "units short-long",
2353                 "converter short-long",
2354                 shortToLong.entrySet(),
2355                 converter.SHORT_TO_LONG_ID.entrySet());
2356 
2357         Set<String> errors = new LinkedHashSet<>();
2358         Set<String> unitsConvertibleLongIds =
2359                 converter.canConvert().stream()
2360                         .map(
2361                                 x -> {
2362                                     String result = shortToLong.get(x);
2363                                     if (result == null) {
2364                                         errors.add("No short form of " + x);
2365                                     }
2366                                     return result;
2367                                 })
2368                         .collect(Collectors.toSet());
2369         assertEquals("", Collections.emptySet(), errors);
2370 
2371         Set<String> simpleConvertibleLongIds =
2372                 converter.canConvert().stream()
2373                         .filter(x -> converter.isSimple(x))
2374                         .map((String x) -> Units.LONG_TO_SHORT.inverse().get(x))
2375                         .collect(Collectors.toSet());
2376         CLDRFile root = CLDR_CONFIG.getCldrFactory().make("root", true);
2377         ImmutableSet<String> unitLongIdsRoot = ImmutableSet.copyOf(getUnits(root, new TreeSet<>()));
2378         ImmutableSet<String> unitLongIdsEnglish =
2379                 ImmutableSet.copyOf(getUnits(CLDR_CONFIG.getEnglish(), new TreeSet<>()));
2380 
2381         final Set<String> longUntranslatedUnitIds =
2382                 converter.getLongIds(UnitConverter.UNTRANSLATED_UNIT_NAMES);
2383 
2384         ImmutableSet<String> onlyEnglish = ImmutableSet.of("pressure-gasoline-energy-density");
2385         assertSameCollections(
2386                 "root unit IDs",
2387                 "English",
2388                 unitLongIdsRoot,
2389                 Sets.difference(
2390                         Sets.difference(unitLongIdsEnglish, longUntranslatedUnitIds), onlyEnglish));
2391 
2392         final Set<String> validRootUnitIdsMinusOddballs = unitLongIdsRoot;
2393         final Set<String> validLongUnitIdsMinusOddballs =
2394                 minus(validLongUnitIds, longUntranslatedUnitIds);
2395         assertSuperset(
2396                 "valid regular",
2397                 "root unit IDs",
2398                 validLongUnitIdsMinusOddballs,
2399                 validRootUnitIdsMinusOddballs);
2400 
2401         assertSameCollections(
2402                 "comparatorUnitIds (DtdData)",
2403                 "valid regular&deprecated",
2404                 comparatorUnitIds,
2405                 validAndDeprecatedLongUnitIds);
2406 
2407         assertSuperset(
2408                 "valid regular", "specials", validLongUnitIds, GrammarInfo.getUnitsToAddGrammar());
2409 
2410         assertSuperset(
2411                 "root unit IDs", "specials", unitLongIdsRoot, GrammarInfo.getUnitsToAddGrammar());
2412 
2413         // assertSuperset("long convertible units", "valid regular", unitsConvertibleLongIds,
2414         // validLongUnitIds);
2415         Output<String> baseUnit = new Output<>();
2416         for (String longUnit : validLongUnitIds) {
2417             String shortUnit = Units.getShort(longUnit);
2418             if (NOT_CONVERTABLE.contains(shortUnit)) {
2419                 continue;
2420             }
2421             ConversionInfo conversionInfo = converter.parseUnitId(shortUnit, baseUnit, false);
2422             if (!assertNotNull("Can convert " + longUnit, conversionInfo)) {
2423                 converter.getUnitInfo(shortUnit, baseUnit);
2424                 int debug = 0;
2425             }
2426         }
2427 
2428         assertSuperset(
2429                 "valid regular",
2430                 "simple convertible units",
2431                 validLongUnitIds,
2432                 simpleConvertibleLongIds);
2433 
2434         SupplementalDataInfo.getInstance().getUnitConverter();
2435     }
2436 
assertSameCollections( String title1, String title2, Collection<String> c1, Collection<String> c2)2437     public void assertSameCollections(
2438             String title1, String title2, Collection<String> c1, Collection<String> c2) {
2439         assertSuperset(title1, title2, c1, c2);
2440         assertSuperset(title2, title1, c2, c1);
2441     }
2442 
assertSuperset( String title1, String title2, Collection<V> c1, Collection<V> c2)2443     public <V> void assertSuperset(
2444             String title1, String title2, Collection<V> c1, Collection<V> c2) {
2445         if (!assertEquals(title1 + " ⊇ " + title2, Collections.emptySet(), minus(c2, c1))) {
2446             int debug = 0;
2447         }
2448     }
2449 
minus(Collection<V> a, Collection<V> b)2450     public <V> Set<V> minus(Collection<V> a, Collection<V> b) {
2451         Set<V> result = new LinkedHashSet<>(a);
2452         result.removeAll(b);
2453         return result;
2454     }
2455 
minus(Collection<V> a, V... b)2456     public <V> Set<V> minus(Collection<V> a, V... b) {
2457         Set<V> result = new LinkedHashSet<>(a);
2458         result.removeAll(Arrays.asList(b));
2459         return result;
2460     }
2461 
getUnits(CLDRFile root, Set<String> unitLongIds)2462     public Set<String> getUnits(CLDRFile root, Set<String> unitLongIds) {
2463         for (String path : root) {
2464             XPathParts parts = XPathParts.getFrozenInstance(path);
2465             int item = parts.findElement("unit");
2466             if (item == -1) {
2467                 continue;
2468             }
2469             String type = parts.getAttributeValue(item, "type");
2470             unitLongIds.add(type);
2471             // "//ldml/units/unitLength[@type=\"long\"]/unit[@type=\"" + unit + "\"]/gender"
2472         }
2473         return unitLongIds;
2474     }
2475 
2476     static final Pattern NORM_SPACES = Pattern.compile("[ \u00A0\u200E]");
2477 
TestGender()2478     public void TestGender() {
2479         Output<String> source = new Output<>();
2480         Multimap<UnitPathType, String> partsUsed = TreeMultimap.create();
2481         Factory factory = CLDR_CONFIG.getFullCldrFactory();
2482         Set<String> available = factory.getAvailable();
2483         int bad = 0;
2484 
2485         for (String locale : SDI.hasGrammarInfo()) {
2486             // skip ones without gender info
2487             GrammarInfo gi = SDI.getGrammarInfo("fr");
2488             Collection<String> genderInfo =
2489                     gi.get(
2490                             GrammaticalTarget.nominal,
2491                             GrammaticalFeature.grammaticalGender,
2492                             GrammaticalScope.general);
2493             if (genderInfo.isEmpty()) {
2494                 continue;
2495             }
2496             if (CLDRConfig.SKIP_SEED && !available.contains(locale)) {
2497                 continue;
2498             }
2499             // check others
2500             CLDRFile resolvedFile = factory.make(locale, true);
2501             for (Entry<String, String> entry : converter.SHORT_TO_LONG_ID.entrySet()) {
2502                 final String shortUnitId = entry.getKey();
2503                 final String longUnitId = entry.getValue();
2504                 final UnitId unitId = converter.createUnitId(shortUnitId);
2505                 partsUsed.clear();
2506                 String rawGender =
2507                         UnitPathType.gender.getTrans(
2508                                 resolvedFile, "long", shortUnitId, null, null, null, partsUsed);
2509 
2510                 if (rawGender != null) {
2511                     String gender = unitId.getGender(resolvedFile, source, partsUsed);
2512                     if (gender != null && !shortUnitId.equals(source.value)) {
2513                         if (!Objects.equals(rawGender, gender)) {
2514                             if (SHOW_DATA) {
2515                                 printlnIfZero(bad);
2516                                 System.out.println(
2517                                         locale
2518                                                 + ": computed gender = raw gender for\t"
2519                                                 + shortUnitId
2520                                                 + "\t"
2521                                                 + Joiner.on("\n\t\t")
2522                                                         .join(partsUsed.asMap().entrySet()));
2523                             }
2524                             ++bad;
2525                         }
2526                     }
2527                 }
2528             }
2529         }
2530         if (bad > 0) {
2531             warnln(
2532                     bad
2533                             + " units x locales with incorrect computed gender. Use -DTestUnits:SHOW_DATA for details.");
2534         }
2535     }
2536 
TestFallbackNames()2537     public void TestFallbackNames() {
2538         String[][] sampleUnits = {
2539             {"fr", "square-meter", "one", "nominative", "{0} mètre carré"},
2540             {"fr", "square-meter", "other", "nominative", "{0} mètres carrés"},
2541             {"fr", "square-decimeter", "other", "nominative", "{0} décimètres carrés"},
2542             {"fr", "meter-per-square-second", "one", "nominative", "{0} mètre par seconde carrée"},
2543             {
2544                 "fr",
2545                 "meter-per-square-second",
2546                 "other",
2547                 "nominative",
2548                 "{0} mètres par seconde carrée"
2549             },
2550             {"de", "square-meter", "other", "nominative", "{0} Quadratmeter"},
2551             {"de", "square-decimeter", "other", "nominative", "{0} Quadratdezimeter"}, // real fail
2552             {"de", "per-meter", "other", "nominative", "{0} pro Meter"},
2553             {"de", "per-square-meter", "other", "nominative", "{0} pro Quadratmeter"},
2554             {"de", "second-per-meter", "other", "nominative", "{0} Sekunden pro Meter"},
2555             {"de", "meter-per-second", "other", "nominative", "{0} Meter pro Sekunde"},
2556             {
2557                 "de",
2558                 "meter-per-square-second",
2559                 "other",
2560                 "nominative",
2561                 "{0} Meter pro Quadratsekunde"
2562             },
2563             {
2564                 "de",
2565                 "gigasecond-per-decimeter",
2566                 "other",
2567                 "nominative",
2568                 "{0} Gigasekunden pro Dezimeter"
2569             },
2570             {
2571                 "de",
2572                 "decimeter-per-gigasecond",
2573                 "other",
2574                 "nominative",
2575                 "{0} Dezimeter pro Gigasekunde"
2576             }, // real fail
2577             {
2578                 "de",
2579                 "gigasecond-milligram-per-centimeter-decisecond",
2580                 "other",
2581                 "nominative",
2582                 "{0} Milligramm⋅Gigasekunden pro Zentimeter⋅Dezisekunde"
2583             },
2584             {
2585                 "de",
2586                 "milligram-per-centimeter-decisecond",
2587                 "other",
2588                 "nominative",
2589                 "{0} Milligramm pro Zentimeter⋅Dezisekunde"
2590             },
2591             {
2592                 "de",
2593                 "per-centimeter-decisecond",
2594                 "other",
2595                 "nominative",
2596                 "{0} pro Zentimeter⋅Dezisekunde"
2597             },
2598             {
2599                 "de",
2600                 "gigasecond-milligram-per-centimeter",
2601                 "other",
2602                 "nominative",
2603                 "{0} Milligramm⋅Gigasekunden pro Zentimeter"
2604             },
2605             {"de", "gigasecond-milligram", "other", "nominative", "{0} Milligramm⋅Gigasekunden"},
2606             {"de", "gigasecond-gram", "other", "nominative", "{0} Gramm⋅Gigasekunden"},
2607             {"de", "gigasecond-kilogram", "other", "nominative", "{0} Kilogramm⋅Gigasekunden"},
2608             {"de", "gigasecond-megagram", "other", "nominative", "{0} Megagramm⋅Gigasekunden"},
2609             {
2610                 "de",
2611                 "dessert-spoon-imperial-per-dessert-spoon-imperial",
2612                 "one",
2613                 "nominative",
2614                 "{0} Imp. Dessertlöffel pro Imp. Dessertlöffel"
2615             },
2616             {
2617                 "de",
2618                 "dessert-spoon-imperial-per-dessert-spoon-imperial",
2619                 "one",
2620                 "accusative",
2621                 "{0} Imp. Dessertlöffel pro Imp. Dessertlöffel"
2622             },
2623             {
2624                 "de",
2625                 "dessert-spoon-imperial-per-dessert-spoon-imperial",
2626                 "other",
2627                 "dative",
2628                 "{0} Imp. Dessertlöffeln pro Imp. Dessertlöffel"
2629             },
2630             {
2631                 "de",
2632                 "dessert-spoon-imperial-per-dessert-spoon-imperial",
2633                 "one",
2634                 "genitive",
2635                 "{0} Imp. Dessertlöffels pro Imp. Dessertlöffel"
2636             },
2637 
2638             // TODO: pick names (eg in Polish) that show differences in case.
2639             // {"de", "foebar-foobar-per-fiebar-faebar", "other", "genitive", null},
2640 
2641         };
2642         ImmutableMap<String, String> frOverrides =
2643                 ImmutableMap.<String, String>builder() // insufficient data in French as yet
2644                         .put(
2645                                 "//ldml/units/unitLength[@type=\"long\"]/compoundUnit[@type=\"power2\"]/compoundUnitPattern1[@count=\"one\"]",
2646                                 "{0} carré") //
2647                         .put(
2648                                 "//ldml/units/unitLength[@type=\"long\"]/compoundUnit[@type=\"power2\"]/compoundUnitPattern1[@count=\"other\"]",
2649                                 "{0} carrés") //
2650                         .put(
2651                                 "//ldml/units/unitLength[@type=\"long\"]/compoundUnit[@type=\"power2\"]/compoundUnitPattern1[@count=\"one\"][@gender=\"feminine\"]",
2652                                 "{0} carrée") //
2653                         .put(
2654                                 "//ldml/units/unitLength[@type=\"long\"]/compoundUnit[@type=\"power2\"]/compoundUnitPattern1[@count=\"other\"][@gender=\"feminine\"]",
2655                                 "{0} carrées") //
2656                         .build();
2657 
2658         Multimap<UnitPathType, String> partsUsed = TreeMultimap.create();
2659         int count = 0;
2660         for (String[] row : sampleUnits) {
2661             ++count;
2662             final String locale = row[0];
2663             CLDRFile resolvedFileRaw = CLDR_CONFIG.getCLDRFile(locale, true);
2664             LocaleStringProvider resolvedFile;
2665             switch (locale) {
2666                 case "fr":
2667                     resolvedFile = resolvedFileRaw.makeOverridingStringProvider(frOverrides);
2668                     break;
2669                 default:
2670                     resolvedFile = resolvedFileRaw;
2671                     break;
2672             }
2673 
2674             String shortUnitId = row[1];
2675             String pluralCategory = row[2];
2676             String caseVariant = row[3];
2677             String expectedName = row[4];
2678             if (shortUnitId.equals("gigasecond-milligram")) {
2679                 int debug = 0;
2680             }
2681             final UnitId unitId = converter.createUnitId(shortUnitId);
2682             final String actual =
2683                     unitId.toString(
2684                             resolvedFile, "long", pluralCategory, caseVariant, partsUsed, false);
2685             assertEquals(
2686                     count
2687                             + ") "
2688                             + Arrays.asList(row).toString()
2689                             + "\n\t"
2690                             + Joiner.on("\n\t").join(partsUsed.asMap().entrySet()),
2691                     fixSpaces(expectedName),
2692                     fixSpaces(actual));
2693         }
2694     }
2695 
TestFileFallbackNames()2696     public void TestFileFallbackNames() {
2697         Multimap<UnitPathType, String> partsUsed = TreeMultimap.create();
2698 
2699         // first gather all the  examples
2700         Set<String> skippedUnits = new LinkedHashSet<>();
2701         Set<String> testSet = STANDARD_CODES.getLocaleCoverageLocales(Organization.cldr);
2702         Counter<String> localeToErrorCount = new Counter<>();
2703         main:
2704         for (String localeId : testSet) {
2705             if (localeId.contains("_")) {
2706                 continue; // skip to make test shorter
2707             }
2708             CLDRFile resolvedFile = CLDR_CONFIG.getCLDRFile(localeId, true);
2709             PluralInfo pluralInfo = CLDR_CONFIG.getSupplementalDataInfo().getPlurals(localeId);
2710             PluralRules pluralRules = pluralInfo.getPluralRules();
2711             GrammarInfo grammarInfo =
2712                     CLDR_CONFIG.getSupplementalDataInfo().getGrammarInfo(localeId);
2713             Collection<String> caseVariants =
2714                     grammarInfo == null
2715                             ? null
2716                             : grammarInfo.get(
2717                                     GrammaticalTarget.nominal,
2718                                     GrammaticalFeature.grammaticalCase,
2719                                     GrammaticalScope.units);
2720             if (caseVariants == null || caseVariants.isEmpty()) {
2721                 caseVariants = Collections.singleton("nominative");
2722             }
2723 
2724             for (Entry<String, String> entry : converter.SHORT_TO_LONG_ID.entrySet()) {
2725                 final String shortUnitId = entry.getKey();
2726                 if (converter.getComplexity(shortUnitId) == UnitComplexity.simple) {
2727                     continue;
2728                 }
2729                 if (UnitConverter.HACK_SKIP_UNIT_NAMES.contains(shortUnitId)) {
2730                     skippedUnits.add(shortUnitId);
2731                     continue;
2732                 }
2733                 final String longUnitId = entry.getValue();
2734                 final UnitId unitId = converter.createUnitId(shortUnitId);
2735                 for (String width : Arrays.asList("long")) { // , "short", "narrow"
2736                     for (String pluralCategory : pluralRules.getKeywords()) {
2737                         for (String caseVariant : caseVariants) {
2738                             String composedName;
2739                             try {
2740                                 composedName =
2741                                         unitId.toString(
2742                                                 resolvedFile,
2743                                                 width,
2744                                                 pluralCategory,
2745                                                 caseVariant,
2746                                                 partsUsed,
2747                                                 false);
2748                             } catch (Exception e) {
2749                                 composedName = "ERROR:" + e.getMessage();
2750                             }
2751                             if (composedName != null
2752                                     && (composedName.contains("′")
2753                                             || composedName.contains("″"))) { // skip special cases
2754                                 continue;
2755                             }
2756                             partsUsed.clear();
2757                             String transName =
2758                                     UnitPathType.unit.getTrans(
2759                                             resolvedFile,
2760                                             width,
2761                                             shortUnitId,
2762                                             pluralCategory,
2763                                             caseVariant,
2764                                             null,
2765                                             isVerbose() ? partsUsed : null);
2766 
2767                             // HACK to fix different spaces around placeholder
2768                             if (!Objects.equals(fixSpaces(transName), fixSpaces(composedName))) {
2769                                 logln(
2770                                         "\t"
2771                                                 + localeId
2772                                                 + "\t"
2773                                                 + shortUnitId
2774                                                 + "\t"
2775                                                 + width
2776                                                 + "\t"
2777                                                 + pluralCategory
2778                                                 + "\t"
2779                                                 + caseVariant
2780                                                 + "\texpected ≠ fallback\t«"
2781                                                 + transName
2782                                                 + "»\t≠\t«"
2783                                                 + composedName
2784                                                 + "»"
2785                                                 + partsUsed);
2786                                 localeToErrorCount.add(localeId, 1);
2787                                 if (!SHOW_COMPOSE && localeToErrorCount.getTotal() > 50) {
2788                                     break main;
2789                                 }
2790                             }
2791                         }
2792                     }
2793                 }
2794             }
2795         }
2796         if (!localeToErrorCount.isEmpty()) {
2797             warnln(
2798                     "composed name ≠ translated name: ≥"
2799                             + localeToErrorCount.getTotal()
2800                             + ". Use -DTestUnits:SHOW_COMPOSE to see summary");
2801             if (SHOW_COMPOSE) {
2802                 System.out.println();
2803                 for (R2<Long, String> entry :
2804                         localeToErrorCount.getEntrySetSortedByCount(false, null)) {
2805                     System.out.println(
2806                             "composed name ≠ translated name: "
2807                                     + entry.get0()
2808                                     + "\t"
2809                                     + entry.get1());
2810                 }
2811             }
2812         }
2813 
2814         if (!skippedUnits.isEmpty()) {
2815             warnln("Skipped unsupported units: " + skippedUnits);
2816         }
2817     }
2818 
fixSpaces(String transName)2819     public String fixSpaces(String transName) {
2820         return transName == null ? null : NORM_SPACES.matcher(transName).replaceAll(" ");
2821     }
2822 
TestCheckUnits()2823     public void TestCheckUnits() {
2824         CheckUnits checkUnits = new CheckUnits();
2825         PathHeader.Factory phf = PathHeader.getFactory();
2826         for (String locale : Arrays.asList("en", "fr", "de", "pl", "el")) {
2827             CLDRFile cldrFile = CLDR_CONFIG.getCldrFactory().make(locale, true);
2828 
2829             Options options = new Options();
2830             List<CheckStatus> possibleErrors = new ArrayList<>();
2831             checkUnits.setCldrFileToCheck(cldrFile, options, possibleErrors);
2832 
2833             for (String path :
2834                     StreamSupport.stream(cldrFile.spliterator(), false)
2835                             .sorted()
2836                             .collect(Collectors.toList())) {
2837                 UnitPathType pathType =
2838                         UnitPathType.getPathType(XPathParts.getFrozenInstance(path));
2839                 if (pathType == null || pathType == UnitPathType.unit) {
2840                     continue;
2841                 }
2842                 String value = cldrFile.getStringValue(path);
2843                 checkUnits.check(path, path, value, options, possibleErrors);
2844                 if (!possibleErrors.isEmpty()) {
2845                     PathHeader ph = phf.fromPath(path);
2846                     logln(locale + "\t" + ph.getCode() + "\t" + possibleErrors.toString());
2847                 }
2848             }
2849         }
2850     }
2851 
TestDerivedCase()2852     public void TestDerivedCase() {
2853         // needs further work
2854         if (logKnownIssue("CLDR-16395", "finish this as part of unit derivation work")) {
2855             return;
2856         }
2857         for (String locale : Arrays.asList("pl", "ru")) {
2858             CLDRFile cldrFile = CLDR_CONFIG.getCldrFactory().make(locale, true);
2859             GrammarInfo gi = SDI.getGrammarInfo(locale);
2860             Collection<String> rawCases =
2861                     gi.get(
2862                             GrammaticalTarget.nominal,
2863                             GrammaticalFeature.grammaticalCase,
2864                             GrammaticalScope.units);
2865 
2866             PluralInfo plurals =
2867                     SupplementalDataInfo.getInstance().getPlurals(PluralType.cardinal, locale);
2868             Collection<Count> adjustedPlurals = plurals.getCounts();
2869 
2870             Output<String> sourceCase = new Output<>();
2871             Output<String> sourcePlural = new Output<>();
2872 
2873             M4<String, String, String, Boolean> myInfo =
2874                     ChainedMap.of(
2875                             new TreeMap<String, Object>(),
2876                             new TreeMap<String, Object>(),
2877                             new TreeMap<String, Object>(),
2878                             Boolean.class);
2879 
2880             int count = 0;
2881             for (String longUnit : GrammarInfo.getUnitsToAddGrammar()) {
2882                 final String shortUnit = converter.getShortId(longUnit);
2883                 String gender =
2884                         UnitPathType.gender.getTrans(
2885                                 cldrFile, "long", shortUnit, null, null, null, null);
2886 
2887                 for (String desiredCase : rawCases) {
2888                     // gather some general information
2889                     for (Count plural : adjustedPlurals) {
2890                         String value =
2891                                 UnitPathType.unit.getTrans(
2892                                         cldrFile,
2893                                         "long",
2894                                         shortUnit,
2895                                         plural.toString(),
2896                                         desiredCase,
2897                                         gender,
2898                                         null);
2899                         myInfo.put(
2900                                 gender,
2901                                 shortUnit + "\t" + value,
2902                                 plural.toString() + "+" + desiredCase,
2903                                 true);
2904                     }
2905 
2906                     // do actual test
2907                     if (desiredCase.contentEquals("nominative")) {
2908                         continue;
2909                     }
2910                     for (String desiredPlural : Arrays.asList("few", "other")) {
2911 
2912                         String value =
2913                                 UnitPathType.unit.getTrans(
2914                                         cldrFile,
2915                                         "long",
2916                                         shortUnit,
2917                                         desiredPlural,
2918                                         desiredCase,
2919                                         gender,
2920                                         null);
2921                         gi.getSourceCaseAndPlural(
2922                                 locale,
2923                                 gender,
2924                                 value,
2925                                 desiredCase,
2926                                 desiredPlural,
2927                                 sourceCase,
2928                                 sourcePlural);
2929                         String sourceValue =
2930                                 UnitPathType.unit.getTrans(
2931                                         cldrFile,
2932                                         "long",
2933                                         shortUnit,
2934                                         sourcePlural.value,
2935                                         sourceCase.value,
2936                                         gender,
2937                                         null);
2938                         assertEquals(
2939                                 count++
2940                                         + ") "
2941                                         + locale
2942                                         + ",\tshort unit/gender: "
2943                                         + shortUnit
2944                                         + " / "
2945                                         + gender
2946                                         + ",\tdesired case/plural: "
2947                                         + desiredCase
2948                                         + " / "
2949                                         + desiredPlural
2950                                         + ",\tsource case/plural: "
2951                                         + sourceCase
2952                                         + " / "
2953                                         + sourcePlural,
2954                                 value,
2955                                 sourceValue);
2956                     }
2957                 }
2958             }
2959             for (Entry<String, Map<String, Map<String, Boolean>>> m : myInfo) {
2960                 for (Entry<String, Map<String, Boolean>> t : m.getValue().entrySet()) {
2961                     System.out.println(
2962                             m.getKey() + "\t" + t.getKey() + "\t" + t.getValue().keySet());
2963                 }
2964             }
2965         }
2966     }
2967 
TestGenderOfCompounds()2968     public void TestGenderOfCompounds() {
2969         Set<String> skipUnits =
2970                 ImmutableSet.of(
2971                         "kilocalorie",
2972                         "kilopascal",
2973                         "terabyte",
2974                         "gigabyte",
2975                         "kilobyte",
2976                         "gigabit",
2977                         "kilobit",
2978                         "megabit",
2979                         "megabyte",
2980                         "terabit");
2981         final ImmutableSet<String> keyValues =
2982                 ImmutableSet.of("length", "mass", "duration", "power");
2983         int noGendersForLocales = 0;
2984         int localesWithNoGenders = 0;
2985         int localesWithSomeMissingGenders = 0;
2986 
2987         for (String localeID : GrammarInfo.getGrammarLocales()) {
2988             GrammarInfo grammarInfo = SDI.getGrammarInfo(localeID);
2989             if (grammarInfo == null) {
2990                 logln("No grammar info for: " + localeID);
2991                 continue;
2992             }
2993             UnitConverter converter = SDI.getUnitConverter();
2994             Collection<String> genderInfo =
2995                     grammarInfo.get(
2996                             GrammaticalTarget.nominal,
2997                             GrammaticalFeature.grammaticalGender,
2998                             GrammaticalScope.units);
2999             if (genderInfo.isEmpty()) {
3000                 continue;
3001             }
3002             CLDRFile cldrFile = info.getCldrFactory().make(localeID, true);
3003             Map<String, String> shortUnitToGender = new TreeMap<>();
3004             Output<String> source = new Output<>();
3005             Multimap<UnitPathType, String> partsUsed = LinkedHashMultimap.create();
3006 
3007             Set<String> units = new HashSet<>();
3008             M4<String, String, String, Boolean> quantityToGenderToUnits =
3009                     ChainedMap.of(
3010                             new TreeMap<String, Object>(),
3011                             new TreeMap<String, Object>(),
3012                             new TreeMap<String, Object>(),
3013                             Boolean.class);
3014             M4<String, String, String, Boolean> genderToQuantityToUnits =
3015                     ChainedMap.of(
3016                             new TreeMap<String, Object>(),
3017                             new TreeMap<String, Object>(),
3018                             new TreeMap<String, Object>(),
3019                             Boolean.class);
3020 
3021             for (String path : cldrFile) {
3022                 if (!path.startsWith("//ldml/units/unitLength[@type=\"long\"]/unit[@type=")) {
3023                     continue;
3024                 }
3025                 XPathParts parts = XPathParts.getFrozenInstance(path);
3026                 final String shortId = converter.getShortId(parts.getAttributeValue(-2, "type"));
3027                 if (NOT_CONVERTABLE.contains(shortId)) {
3028                     continue;
3029                 }
3030                 String quantity = null;
3031                 try {
3032                     quantity = converter.getQuantityFromUnit(shortId, false);
3033                 } catch (Exception e) {
3034                 }
3035 
3036                 if (quantity == null) {
3037                     throw new IllegalArgumentException("No quantity for " + shortId);
3038                 }
3039 
3040                 // ldml/units/unitLength[@type="long"]/unit[@type="duration-year"]/gender
3041                 String gender = null;
3042                 if (parts.size() == 5 && parts.getElement(-1).equals("gender")) {
3043                     gender = cldrFile.getStringValue(path);
3044                     if (true) {
3045                         quantityToGenderToUnits.put(quantity, gender, shortId, true);
3046                         genderToQuantityToUnits.put(quantity, gender, shortId, true);
3047                     }
3048                 } else {
3049                     if (units.contains(shortId)) {
3050                         continue;
3051                     }
3052                     units.add(shortId);
3053                 }
3054                 UnitId unitId = converter.createUnitId(shortId);
3055                 String constructedGender = unitId.getGender(cldrFile, source, partsUsed);
3056                 boolean multiUnit =
3057                         unitId.denUnitsToPowers.size() + unitId.denUnitsToPowers.size() > 1;
3058                 if (gender == null && (constructedGender == null || !multiUnit)) {
3059                     continue;
3060                 }
3061 
3062                 final boolean areEqual = Objects.equals(gender, constructedGender);
3063                 if (SHOW_COMPOSE) {
3064                     final String printInfo =
3065                             localeID
3066                                     + "\t"
3067                                     + unitId
3068                                     + "\t"
3069                                     + gender
3070                                     + "\t"
3071                                     + multiUnit
3072                                     + "\t"
3073                                     + quantity
3074                                     + "\t"
3075                                     + constructedGender
3076                                     + "\t"
3077                                     + areEqual;
3078                     System.out.println(printInfo);
3079                 }
3080 
3081                 if (gender != null && !areEqual && !skipUnits.contains(shortId)) {
3082                     unitId.getGender(cldrFile, source, partsUsed);
3083                     shortUnitToGender.put(
3084                             shortId,
3085                             unitId
3086                                     + "\t actual gender: "
3087                                     + gender
3088                                     + "\t constructed gender:"
3089                                     + constructedGender);
3090                 }
3091             }
3092             if (quantityToGenderToUnits.keySet().isEmpty()) {
3093                 if (SHOW_COMPOSE) {
3094                     printlnIfZero(noGendersForLocales);
3095                     System.out.println("No genders for\t" + localeID);
3096                 }
3097                 localesWithNoGenders++;
3098                 continue;
3099             }
3100 
3101             for (Entry<String, String> entry : shortUnitToGender.entrySet()) {
3102                 if (SHOW_COMPOSE) {
3103                     printlnIfZero(noGendersForLocales);
3104                     System.out.println(localeID + "\t" + entry);
3105                 }
3106                 noGendersForLocales++;
3107             }
3108 
3109             Set<String> missing = new LinkedHashSet<>(genderInfo);
3110             for (String quantity : keyValues) {
3111                 M3<String, String, Boolean> genderToUnits = quantityToGenderToUnits.get(quantity);
3112                 showData(localeID, null, quantity, genderToUnits);
3113                 missing.removeAll(genderToUnits.keySet());
3114             }
3115             for (String quantity : quantityToGenderToUnits.keySet()) {
3116                 M3<String, String, Boolean> genderToUnits = quantityToGenderToUnits.get(quantity);
3117                 showData(localeID, missing, quantity, genderToUnits);
3118             }
3119             for (String gender : missing) {
3120                 if (SHOW_DATA) {
3121                     printlnIfZero(noGendersForLocales);
3122                     System.out.println(
3123                             "Missing values: " + localeID + "\t" + "?" + "\t" + gender + "\t?");
3124                 }
3125                 noGendersForLocales++;
3126             }
3127         }
3128         if (noGendersForLocales > 0) {
3129             warnln(
3130                     noGendersForLocales
3131                             + " units x locales with missing gender. Use -DTestUnits:SHOW_DATA for info, -DTestUnits:SHOW_COMPOSE for compositions");
3132         }
3133     }
3134 
printlnIfZero(int noGendersForLocales)3135     public void printlnIfZero(int noGendersForLocales) {
3136         if (noGendersForLocales == 0) {
3137             System.out.println();
3138         }
3139     }
3140 
showData( String localeID, Set<String> genderFilter, String quantity, final M3<String, String, Boolean> genderToUnits)3141     public void showData(
3142             String localeID,
3143             Set<String> genderFilter,
3144             String quantity,
3145             final M3<String, String, Boolean> genderToUnits) {
3146         for (Entry<String, Map<String, Boolean>> entry2 : genderToUnits) {
3147             String gender = entry2.getKey();
3148             if (genderFilter != null) {
3149                 if (!genderFilter.contains(gender)) {
3150                     continue;
3151                 }
3152                 genderFilter.remove(gender);
3153             }
3154             for (String unit : entry2.getValue().keySet()) {
3155                 logln(localeID + "\t" + quantity + "\t" + gender + "\t" + unit);
3156             }
3157         }
3158     }
3159 
3160     static final boolean DEBUG_DERIVATION = false;
3161 
testDerivation()3162     public void testDerivation() {
3163         int count = 0;
3164         for (String locale : SDI.hasGrammarDerivation()) {
3165             GrammarDerivation gd = SDI.getGrammarDerivation(locale);
3166             if (DEBUG_DERIVATION) System.out.println(locale + " => " + gd);
3167             ++count;
3168         }
3169         assertNotEquals("hasGrammarDerivation", 0, count);
3170     }
3171 
3172     static final boolean DEBUG_ORDER = false;
3173 
TestUnitOrder()3174     public void TestUnitOrder() {
3175         for (Entry<String, String> entry : converter.getBaseUnitToQuantity().entrySet()) {
3176             checkNormalization("base-quantity, " + entry.getValue(), entry.getKey());
3177         }
3178 
3179         // check root list
3180         // crucial that this is stable!!
3181         Set<String> shortUnitsFound =
3182                 checkCldrFileUnits("root unit", CLDRConfig.getInstance().getRoot());
3183         final Set<String> shortValidRegularUnits = VALID_SHORT_UNITS;
3184         assertEquals(
3185                 "root units - regular units",
3186                 Collections.emptySet(),
3187                 Sets.difference(shortUnitsFound, shortValidRegularUnits));
3188 
3189         // check English also
3190         checkCldrFileUnits("en unit", CLDRConfig.getInstance().getEnglish());
3191 
3192         for (String unit : converter.canConvert()) {
3193             checkNormalization("convertable", unit);
3194             String baseUnitId = converter.getBaseUnit(unit);
3195             checkNormalization("convertable base", baseUnitId);
3196         }
3197 
3198         checkNormalization("test case", "foot-acre", "acre-foot");
3199         checkNormalization("test case", "meter-newton", "newton-meter");
3200 
3201         checkNormalization("test case", "newton-meter");
3202         checkNormalization("test case", "acre-foot");
3203         checkNormalization("test case", "portion-per-1e9");
3204         checkNormalization("test case", "portion-per-1000");
3205         checkNormalization("test case", "1e9-meter");
3206         checkNormalization("test case", "1000-meter");
3207 
3208         String stdAcre = converter.getStandardUnit("acre");
3209 
3210         List<String> unitOrdering = new ArrayList<>();
3211         List<String> simpleBaseUnits = new ArrayList<>();
3212 
3213         for (ExternalUnitConversionData data : NistUnits.externalConversionData) {
3214             // unitOrdering.add(data.source);
3215             final String source = data.source;
3216             final String target = data.target;
3217             unitOrdering.add(target);
3218             checkNormalization("nist core, " + source, target);
3219         }
3220         for (Entry<String, TargetInfo> data : NistUnits.derivedUnitToConversion.entrySet()) {
3221             if (DEBUG_ORDER) {
3222                 System.out.println(data);
3223             }
3224             final String target = data.getValue().target;
3225             unitOrdering.add(target);
3226             simpleBaseUnits.add(data.getKey());
3227             checkNormalization("nist derived", target);
3228         }
3229 
3230         for (String baseUnit : converter.getBaseUnitToQuantity().keySet()) {
3231             unitOrdering.add(baseUnit);
3232             String status = converter.getBaseUnitToStatus().get(baseUnit);
3233             if ("simple".equals(status)) {
3234                 simpleBaseUnits.add(baseUnit);
3235             }
3236         }
3237     }
3238 
3239     /**
3240      * Checks the normalization of units found in the file, and returns the set of shortUnitIds
3241      * found in the file
3242      */
checkCldrFileUnits(String title, final CLDRFile cldrFile)3243     public Set<String> checkCldrFileUnits(String title, final CLDRFile cldrFile) {
3244         Set<String> shortUnitsFound = new TreeSet<>();
3245         for (String path : cldrFile) {
3246             if (!path.startsWith("//ldml/units/unitLength")) {
3247                 continue;
3248             }
3249             XPathParts parts = XPathParts.getFrozenInstance(path);
3250             String longUnitId = parts.findAttributeValue("unit", "type");
3251             if (longUnitId == null) {
3252                 continue;
3253             }
3254             String shortUnitId = converter.getShortId(longUnitId);
3255             shortUnitsFound.add(shortUnitId);
3256             checkNormalization(title, shortUnitId);
3257         }
3258         return ImmutableSet.copyOf(shortUnitsFound);
3259     }
3260 
checkNormalization(String title, String source, String expected)3261     public void checkNormalization(String title, String source, String expected) {
3262         String oldExpected = normalizationCache.get(source);
3263         if (oldExpected != null) {
3264             if (!oldExpected.equals(expected)) {
3265                 assertEquals(
3266                         title + ", consistent expected results for " + source,
3267                         oldExpected,
3268                         expected);
3269             }
3270             return;
3271         }
3272         normalizationCache.put(source, expected);
3273         UnitId unitId = converter.createUnitId(source);
3274         if (!assertEquals(title + ", unit order", expected, unitId.toString())) {
3275             unitId = converter.createUnitId(source);
3276             unitId.toString();
3277         }
3278     }
3279 
checkNormalization(String title, String source)3280     public void checkNormalization(String title, String source) {
3281         checkNormalization(title, source, source);
3282     }
3283 
TestElectricConsumption()3284     public void TestElectricConsumption() {
3285         String inputUnit = "kilowatt-hour-per-100-kilometer";
3286         String outputUnit = "kilogram-meter-per-square-second";
3287         Rational result = converter.convert(Rational.ONE, inputUnit, outputUnit, DEBUG);
3288         assertEquals(
3289                 "kilowatt-hour-per-100-kilometer to kilogram-meter-per-square-second",
3290                 Rational.of(36),
3291                 result);
3292     }
3293 
TestEnglishDisplayNames()3294     public void TestEnglishDisplayNames() {
3295         CLDRFile en = CLDRConfig.getInstance().getEnglish();
3296         ImmutableSet<String> unitSkips = ImmutableSet.of("temperature-generic", "graphics-em");
3297         for (String path : en) {
3298             if (path.startsWith("//ldml/units/unitLength[@type=\"long\"]")
3299                     && path.endsWith("/displayName")) {
3300                 if (path.contains("coordinateUnit")) {
3301                     continue;
3302                 }
3303                 XPathParts parts = XPathParts.getFrozenInstance(path);
3304                 final String longUnitId = parts.getAttributeValue(3, "type");
3305                 if (unitSkips.contains(longUnitId)) {
3306                     continue;
3307                 }
3308                 final String width = parts.getAttributeValue(2, "type");
3309                 // ldml/units/unitLength[@type="long"]/unit[@type="duration-decade"]/displayName
3310                 String displayName = en.getStringValue(path);
3311 
3312                 // ldml/units/unitLength[@type="long"]/unit[@type="duration-decade"]/unitPattern[@count="other"]
3313                 String pluralFormPath =
3314                         path.substring(0, path.length() - "/displayName".length())
3315                                 + "/unitPattern[@count=\"other\"]";
3316                 String pluralForm = en.getStringValue(pluralFormPath);
3317                 if (pluralForm == null) {
3318                     errln("Have display name but no plural: " + pluralFormPath);
3319                 } else {
3320                     String cleaned = clean(pluralForm);
3321                     assertEquals(
3322                             "Unit display name should correspond to plural in English "
3323                                     + width
3324                                     + ", "
3325                                     + longUnitId,
3326                             cleaned,
3327                             displayName);
3328                 }
3329             }
3330         }
3331     }
3332 
3333     enum TranslationStatus {
3334         has_grammar_M,
3335         has_grammar_X,
3336         add_grammar,
3337         skip_grammar,
3338         skip_trans("\tspecific langs poss.");
3339 
TranslationStatus()3340         private TranslationStatus() {
3341             outName = name();
3342         }
3343 
3344         private final String outName;
3345 
TranslationStatus(String extra)3346         private TranslationStatus(String extra) {
3347             outName = name() + extra;
3348         }
3349 
3350         @Override
toString()3351         public String toString() {
3352             return outName;
3353         }
3354     }
3355 
3356     /**
3357      * Check which units are enabled for translation. If -v, then generates lines for spreadsheet
3358      * checks.
3359      */
TestUnitsToTranslate()3360     public void TestUnitsToTranslate() {
3361         Set<String> toTranslate = GrammarInfo.getUnitsToAddGrammar();
3362         final CLDRConfig config = CLDRConfig.getInstance();
3363         final UnitConverter converter = config.getSupplementalDataInfo().getUnitConverter();
3364         Map<String, TranslationStatus> shortUnitToTranslationStatus40 =
3365                 new TreeMap<>(converter.getShortUnitIdComparator());
3366         for (String longUnit :
3367                 Validity.getInstance().getStatusToCodes(LstrType.unit).get(Status.regular)) {
3368             String shortUnit = converter.getShortId(longUnit);
3369             shortUnitToTranslationStatus40.put(shortUnit, TranslationStatus.skip_trans);
3370         }
3371         for (String path :
3372                 With.in(
3373                         config.getRoot()
3374                                 .iterator("//ldml/units/unitLength[@type=\"short\"]/unit"))) {
3375             XPathParts parts = XPathParts.getFrozenInstance(path);
3376             String longUnit = parts.getAttributeValue(3, "type");
3377             // Add simple units
3378             String shortUnit = converter.getShortId(longUnit);
3379             Set<UnitSystem> systems = converter.getSystemsEnum(shortUnit);
3380 
3381             boolean unitsToAddGrammar = GrammarInfo.getUnitsToAddGrammar().contains(shortUnit);
3382 
3383             TranslationStatus status =
3384                     toTranslate.contains(longUnit)
3385                             ? (unitsToAddGrammar
3386                                     ? TranslationStatus.has_grammar_M
3387                                     : TranslationStatus.has_grammar_X)
3388                             : unitsToAddGrammar
3389                                     ? TranslationStatus.add_grammar
3390                                     : TranslationStatus.skip_grammar;
3391             shortUnitToTranslationStatus40.put(shortUnit, status);
3392         }
3393         LocalizedNumberFormatter nf =
3394                 NumberFormatter.with()
3395                         .notation(Notation.scientific())
3396                         .precision(Precision.fixedSignificantDigits(7))
3397                         .locale(Locale.ENGLISH);
3398         Output<String> base = new Output<>();
3399         for (Entry<String, TranslationStatus> entry : shortUnitToTranslationStatus40.entrySet()) {
3400             String shortUnit = entry.getKey();
3401             var conversionInfo = converter.parseUnitId(shortUnit, base, false);
3402             String factor =
3403                     conversionInfo == null || conversionInfo.special != null
3404                             ? "n/a"
3405                             : nf.format(conversionInfo.factor.doubleValue())
3406                                     .toString()
3407                                     .replace("E", " × 10^");
3408 
3409             TranslationStatus status40 = entry.getValue();
3410             if (isVerbose())
3411                 System.out.println(
3412                         converter.getQuantityFromUnit(shortUnit, false)
3413                                 + "\t"
3414                                 + shortUnit
3415                                 + "\t"
3416                                 + converter.getSystemsEnum(shortUnit)
3417                                 + "\t"
3418                                 + factor
3419                                 + "\t"
3420                                 + (converter.isSimple(shortUnit) ? "simple" : "complex")
3421                                 + "\t"
3422                                 + status40);
3423         }
3424     }
3425 
3426     static final String marker = "➗";
3427 
TestValidUnitIdComponents()3428     public void TestValidUnitIdComponents() {
3429         for (String longUnit : VALID_REGULAR_UNITS) {
3430             String shortUnit = SDI.getUnitConverter().getShortId(longUnit);
3431             checkShortUnit(shortUnit);
3432         }
3433     }
3434 
TestDeprecatedUnitIdComponents()3435     public void TestDeprecatedUnitIdComponents() {
3436         for (String longUnit : DEPRECATED_REGULAR_UNITS) {
3437             String shortUnit = SDI.getUnitConverter().getShortId(longUnit);
3438             checkShortUnit(shortUnit);
3439         }
3440     }
3441 
TestSelectedUnitIdComponents()3442     public void TestSelectedUnitIdComponents() {
3443         checkShortUnit("curr-chf");
3444     }
3445 
checkShortUnit(String shortUnit)3446     public void checkShortUnit(String shortUnit) {
3447         List<String> parts = SPLIT_DASH.splitToList(shortUnit);
3448         List<String> simpleUnit = new ArrayList<>();
3449         UnitIdComponentType lastType = null;
3450         // structure is (prefix* base* suffix*) per ((prefix* base* suffix*)
3451 
3452         for (String part : parts) {
3453             UnitIdComponentType type = getUnitIdComponentType(part);
3454             switch (type) {
3455                 case prefix:
3456                     if (lastType != UnitIdComponentType.prefix && !simpleUnit.isEmpty()) {
3457                         simpleUnit.add(marker);
3458                     }
3459                     break;
3460                 case base:
3461                     if (lastType != UnitIdComponentType.prefix && !simpleUnit.isEmpty()) {
3462                         simpleUnit.add(marker);
3463                     }
3464                     break;
3465                 case suffix:
3466                     if (!(lastType == UnitIdComponentType.base
3467                             || lastType == UnitIdComponentType.suffix)) {
3468                         if ("metric".equals(part)) { // backward compatibility for metric ton; only
3469                             // needed if deprecated ids are allowed
3470                             lastType = UnitIdComponentType.prefix;
3471                         } else {
3472                             errln(
3473                                     simpleUnit
3474                                             + "/"
3475                                             + part
3476                                             + "; suffix only after base or suffix: "
3477                                             + false);
3478                         }
3479                     }
3480                     break;
3481                     // could add more conditions on these
3482                 case and:
3483                     assertNotNull(simpleUnit + "/" + part + "; not at start", lastType);
3484                     // fall through
3485                 case power:
3486                 case per:
3487                     assertNotEquals(
3488                             simpleUnit + "/" + part + "; illegal after prefix",
3489                             UnitIdComponentType.prefix,
3490                             lastType);
3491                     if (!simpleUnit.isEmpty()) {
3492                         simpleUnit.add(marker);
3493                     }
3494                     break;
3495             }
3496             simpleUnit.add(part + "*" + type.toShortId());
3497             lastType = type;
3498         }
3499         assertTrue(
3500                 simpleUnit + ": last item must be base or suffix",
3501                 lastType == UnitIdComponentType.base || lastType == UnitIdComponentType.suffix);
3502         logln("\t" + shortUnit + "\t" + simpleUnit.toString());
3503     }
3504 
getUnitIdComponentType(String part)3505     public UnitIdComponentType getUnitIdComponentType(String part) {
3506         return SDI.getUnitIdComponentType(part);
3507     }
3508 
TestMetricTon()3509     public void TestMetricTon() {
3510         assertTrue(
3511                 "metric-ton is deprecated", DEPRECATED_REGULAR_UNITS.contains("mass-metric-ton"));
3512         assertEquals(
3513                 "metric-ton is deprecated",
3514                 "tonne",
3515                 SDI.getUnitConverter().fixDenormalized("metric-ton"));
3516         assertEquals(
3517                 "to short", "metric-ton", SDI.getUnitConverter().getShortId("mass-metric-ton"));
3518         // assertEquals("to long", "mass-metric-ton",
3519         // SDI.getUnitConverter().getLongId("metric-ton"));
3520     }
3521 
TestUnitParser()3522     public void TestUnitParser() {
3523         UnitParser up = new UnitParser();
3524         for (String longUnit : VALID_REGULAR_UNITS) {
3525             String shortUnit = SDI.getUnitConverter().getShortId(longUnit);
3526             checkParse(up, shortUnit);
3527         }
3528     }
3529 
checkParse(UnitParser up, String shortUnit)3530     private List<Pair<String, UnitIdComponentType>> checkParse(UnitParser up, String shortUnit) {
3531         up.set(shortUnit);
3532         List<Pair<String, UnitIdComponentType>> results = new ArrayList<>();
3533         Output<UnitIdComponentType> type = new Output<>();
3534         while (true) {
3535             String result = up.nextParse(type);
3536             if (result == null) {
3537                 break;
3538             }
3539             results.add(new Pair<>(result, type.value));
3540         }
3541         logln(shortUnit + "\t" + results);
3542         return results;
3543     }
3544 
TestUnitParserSelected()3545     public void TestUnitParserSelected() {
3546         UnitParser up = new UnitParser();
3547         String[][] tests = {
3548             // unit, exception, resultList
3549             {
3550                 "british-force", "Unit suffix must follow base: british-force → british ❌ force"
3551             }, // prefix-suffix
3552             {"force", "Unit suffix must follow base: force → null ❌ force"}, // suffix
3553             {
3554                 "british-and-french",
3555                 "Unit prefix must be followed with base: british-and-french → british ❌ and"
3556             }, // prefix-and
3557             {
3558                 "british", "Unit prefix must be followed with base: british → british ❌ null"
3559             }, // prefix
3560             {"g-force-light-year", null, "[(g-force,base), (light-year,base)]"}, // suffix
3561         };
3562         for (String[] test : tests) {
3563             String shortUnit = test[0];
3564             String expectedError = test[1];
3565             String expectedResult = test.length <= 2 ? null : test[2];
3566 
3567             String actualError = null;
3568             List<Pair<String, UnitIdComponentType>> actualResult = null;
3569             try {
3570                 actualResult = checkParse(up, shortUnit);
3571             } catch (Exception e) {
3572                 actualError = e.getMessage();
3573             }
3574             assertEquals(shortUnit + " exception", expectedError, actualError);
3575             assertEquals(
3576                     shortUnit + " result",
3577                     expectedResult,
3578                     actualResult == null ? null : actualResult.toString());
3579         }
3580     }
3581 
3582     //    public void TestUnitParserAgainstContinuations() {
3583     //        UnitParser up = new UnitParser();
3584     //        UnitConverter uc = SDI.getUnitConverter();
3585     //        Multimap<String, Continuation> continuations = uc.getContinuations();
3586     //        Output<UnitIdComponentType> type = new Output<>();
3587     //        for (String shortUnit : VALID_SHORT_UNITS) {
3588     //            if (shortUnit.contains("100")) {
3589     //                logKnownIssue("CLDR-15929", "Code doesn't handle 100");
3590     //                continue;
3591     //            }
3592     //            up.set(shortUnit);
3593     //            UnitIterator x = UnitConverter.Continuation.split(shortUnit, continuations);
3594     //
3595     //            int count = 0;
3596     //            while (true) {
3597     //                String upSegment = up.nextParse(type);
3598     //                String continuationSegment = x.hasNext() ? x.next() : null;
3599     //                if (upSegment == null || continuationSegment == null) {
3600     //                    assertEquals(
3601     //                            count + ") " + shortUnit + " Same number of segments ",
3602     //                            continuationSegment == null,
3603     //                            upSegment == null);
3604     //                    break;
3605     //                }
3606     //                assertTrue(
3607     //                        "type is never suffix or prefix",
3608     //                        UnitIdComponentType.suffix != type.value
3609     //                                && UnitIdComponentType.prefix != type.value);
3610     //                ++count;
3611     //                if (!assertEquals(
3612     //                        count + ") " + shortUnit + " Continuation segment vs UnitParser ",
3613     //                        continuationSegment,
3614     //                        upSegment)) {
3615     //                    break; // stop at first difference
3616     //                }
3617     //            }
3618     //        }
3619     //    }
3620 
3621     public static final Set<String> TRUNCATION_EXCEPTIONS =
3622             ImmutableSet.of(
3623                     "sievert",
3624                     "gray",
3625                     "henry",
3626                     "lux",
3627                     "candela",
3628                     "candela-per-square-meter",
3629                     "candela-square-meter-per-square-meter");
3630 
3631     /** Every subtag must be unique to 8 letters. We also check combinations with prefixes */
testTruncation()3632     public void testTruncation() {
3633         UnitConverter uc = SDI.getUnitConverter();
3634         Multimap<String, String> truncatedToFull = TreeMultimap.create();
3635         Set<String> unitsToTest = Sets.union(uc.baseUnits(), uc.getSimpleUnits());
3636 
3637         for (String unit : unitsToTest) {
3638             addTruncation(unit, truncatedToFull);
3639             // also check for adding prefixes
3640             Collection<UnitSystem> systems = uc.getSystemsEnum(unit);
3641             if (systems.contains(UnitSystem.si)
3642                     || UnitConverter.METRIC_TAKING_PREFIXES.contains(unit)) {
3643                 if (TRUNCATION_EXCEPTIONS.contains(unit)) {
3644                     continue;
3645                 }
3646                 // get without prefix
3647                 String baseUnit = removePrefixIfAny(unit);
3648                 for (String prefixPower : UnitConverter.PREFIXES.keySet()) {
3649                     addTruncation(prefixPower + baseUnit, truncatedToFull);
3650                 }
3651             } else if (systems.contains(UnitSystem.metric)) {
3652                 logln("Skipping application of prefixes to: " + unit);
3653             }
3654         }
3655         checkTruncationStatus(truncatedToFull);
3656     }
3657 
removePrefixIfAny(String unit)3658     public String removePrefixIfAny(String unit) {
3659         for (String prefixPower : UnitConverter.PREFIXES.keySet()) {
3660             if (unit.startsWith(prefixPower)) {
3661                 return unit.substring(prefixPower.length());
3662             }
3663         }
3664         return unit;
3665     }
3666 
3667     static Splitter HYPHEN_SPLITTER = Splitter.on('-');
3668 
addTruncation(String unit, Multimap<String, String> truncatedToFull)3669     private void addTruncation(String unit, Multimap<String, String> truncatedToFull) {
3670         for (String subcode : HYPHEN_SPLITTER.split(unit)) {
3671             truncatedToFull.put(subcode.length() <= 8 ? subcode : subcode.substring(0, 8), subcode);
3672         }
3673     }
3674 
checkTruncationStatus(Multimap<String, String> truncatedToFull)3675     public void checkTruncationStatus(Multimap<String, String> truncatedToFull) {
3676         for (Entry<String, Collection<String>> entry : truncatedToFull.asMap().entrySet()) {
3677             final String truncated = entry.getKey();
3678             final Collection<String> longForms = entry.getValue();
3679             if (longForms.size() > 1) {
3680                 errln("Ambiguous bcp47 format: " + entry);
3681             } else if (isVerbose()) {
3682                 if (!longForms.contains(truncated)) {
3683                     logln(entry.toString());
3684                 }
3685             }
3686         }
3687     }
3688 
testGetRelated()3689     public void testGetRelated() {
3690         Map<Rational, String> related2 =
3691                 converter.getRelatedExamples(
3692                         "meter", Sets.difference(UnitSystem.ALL, Set.of(UnitSystem.jpsystem)));
3693         logln(showUnitExamples("meter", related2));
3694 
3695         Set<String> generated = new LinkedHashSet<>();
3696         for (String unit : converter.getSimpleUnits()) {
3697             Map<Rational, String> related =
3698                     converter.getRelatedExamples(
3699                             unit, Sets.difference(UnitSystem.ALL, Set.of(UnitSystem.jpsystem)));
3700             generated.addAll(related.values());
3701             logln(showUnitExamples(unit, related));
3702         }
3703         logln(generated.toString());
3704     }
3705 
showUnitExamples(String unit, Map<Rational, String> related)3706     public String showUnitExamples(String unit, Map<Rational, String> related) {
3707         return "\n"
3708                 + unit
3709                 + "\t#"
3710                 + converter.getSystemsEnum(unit)
3711                 + "\n= "
3712                 + related.entrySet().stream()
3713                         .map(
3714                                 x ->
3715                                         x.getKey().toString(FormatStyle.approx)
3716                                                 + " "
3717                                                 + x.getValue()
3718                                                 + "\t#"
3719                                                 + converter.getSystemsEnum(x.getValue()))
3720                         .collect(Collectors.joining("\n= "));
3721     }
3722 
3723     static class UnitEquivalence implements Comparable<UnitEquivalence> {
3724         final String standard1;
3725         final char operation;
3726         final String standard2;
3727         final UnitId id1;
3728         final UnitId id2;
3729 
UnitEquivalence( String standard1, char operation, String standard2, UnitId id1, UnitId id2)3730         public UnitEquivalence(
3731                 String standard1, char operation, String standard2, UnitId id1, UnitId id2) {
3732             this.standard1 = standard1;
3733             this.operation = operation;
3734             this.standard2 = standard2;
3735             this.id1 = id1;
3736             this.id2 = id2;
3737         }
3738 
3739         @Override
compareTo(UnitEquivalence other)3740         public int compareTo(UnitEquivalence other) {
3741             return ComparisonChain.start()
3742                     .compare(standard1, other.standard1)
3743                     .compare(operation, other.operation)
3744                     .compare(standard2, other.standard2)
3745                     .compare(id1, other.id1)
3746                     .compare(id2, other.id2)
3747                     .result();
3748         }
3749 
3750         @Override
hashCode()3751         public int hashCode() {
3752             return Objects.hash(standard1, operation, standard2, id1, id2);
3753         }
3754 
3755         @Override
equals(Object obj)3756         public boolean equals(Object obj) {
3757             return compareTo((UnitEquivalence) obj) == 0;
3758         }
3759 
3760         @Override
toString()3761         public String toString() {
3762             return standard1 + " " + operation + " " + standard2 + "\t��\t" + id1 + " " + operation
3763                     + " " + id2;
3764         }
3765 
getStandards()3766         public String getStandards() {
3767             return standard1 + " " + operation + " " + standard2;
3768         }
3769     }
3770 
3771     static final Set<String> extras =
3772             Set.of("square-meter", "cubic-meter", "square-second", "cubic-second");
3773 
testRelations()3774     public void testRelations() {
3775         Multimap<String, UnitEquivalence> decomps = TreeMultimap.create();
3776         Set<UnitId> unitIds =
3777                 converter.getBaseUnitToQuantity().entrySet().stream()
3778                         .map(x -> converter.createUnitId(x.getKey()).freeze())
3779                         .collect(Collectors.toSet());
3780         extras.forEach(x -> unitIds.add(converter.createUnitId(x).freeze()));
3781         for (UnitId id1 : unitIds) {
3782             String standard1 = converter.getStandardUnit(id1.toString());
3783             if (skipUnit(standard1)) {
3784                 continue;
3785             }
3786             for (UnitId id2 : unitIds) {
3787                 String standard2 = converter.getStandardUnit(id2.toString());
3788                 if (skipUnit(standard2)) {
3789                     continue;
3790                 }
3791 
3792                 UnitId mul = id1.times(id2);
3793                 String standardMul = converter.getStandardUnit(mul.toString());
3794                 if (!skipUnit(standardMul)) {
3795                     if (standard1.compareTo(standard2) < 0) { // suppress because commutes
3796                         decomps.put(
3797                                 standardMul,
3798                                 new UnitEquivalence(standard1, '×', standard2, id1, id2));
3799                         // decomps.put(standardMul, standard1 + " × " + standard2 + "\t��\t" + id1 +
3800                         // " × " + id2);
3801                     }
3802                 }
3803 
3804                 UnitId id2Recip = id2.getReciprocal();
3805                 UnitId div = id1.times(id2Recip);
3806                 String standardDiv = converter.getStandardUnit(div.toString());
3807                 if (!skipUnit(standardDiv)) {
3808                     decomps.put(
3809                             standardDiv, new UnitEquivalence(standard1, '∕', standard2, id1, id2));
3810                     // decomps.put(standardDiv, standard1 + " ∕ " + standard2 + "\t��\t" + id1 + " ∕
3811                     // " + id2);
3812                 }
3813             }
3814         }
3815         Multimap<String, String> testCases =
3816                 ImmutableMultimap.<String, String>builder()
3817                         .put("joule", "second × watt")
3818                         .put("joule", "meter × newton")
3819                         .put("volt", "ampere × ohm")
3820                         .put("watt", "ampere × volt")
3821                         .build();
3822         Multimap<String, String> missing = TreeMultimap.create(testCases);
3823         for (Entry<String, Collection<UnitEquivalence>> entry : decomps.asMap().entrySet()) {
3824             String unitId = entry.getKey();
3825             logln(unitId + " �� ");
3826             for (UnitEquivalence item : entry.getValue()) {
3827                 logln("\t" + item);
3828                 missing.remove(unitId, item.getStandards());
3829                 Collection<String> others = missing.get(unitId);
3830             }
3831         }
3832         if (!assertEquals("All cases covered", 0, missing.size())) {
3833             for (Entry<String, String> item : missing.entries()) {
3834                 System.out.println(item);
3835             }
3836         }
3837     }
3838 
skipUnit(String unit)3839     private boolean skipUnit(String unit) {
3840         return !extras.contains(unit)
3841                 && (unit == null || unit.contains("-") || unit.equals("becquerel"));
3842     }
3843 
testEquivalents()3844     public void testEquivalents() {
3845         List<List<String>> tests =
3846                 List.of(List.of("gallon-gasoline-energy-density", "33.705", "kilowatt-hour"));
3847         for (List<String> test : tests) {
3848             final String unit1 = test.get(0);
3849             final Rational expectedFactor = Rational.of(test.get(1));
3850             final String unit2 = test.get(2);
3851             Output<String> baseUnit1String = new Output<>();
3852             ConversionInfo base = converter.parseUnitId(unit1, baseUnit1String, false);
3853             UnitId baseUnit1 = converter.createUnitId(baseUnit1String.value).resolve();
3854             Output<String> baseUnit2String = new Output<>();
3855             ConversionInfo other = converter.parseUnitId(unit2, baseUnit2String, false);
3856             UnitId baseUnit2 = converter.createUnitId(baseUnit2String.value).resolve();
3857             Rational actual = base.factor.divide(other.factor);
3858             assertEquals(test.toString() + ", baseUnits", baseUnit1, baseUnit2);
3859             assertEquals(
3860                     test.toString()
3861                             + ", factors, e="
3862                             + expectedFactor.toString(FormatStyle.approx)
3863                             + ", a="
3864                             + actual.toString(FormatStyle.approx),
3865                     expectedFactor,
3866                     actual);
3867         }
3868     }
3869 
testUnitSystems()3870     public void testUnitSystems() {
3871         Set<String> fails = new LinkedHashSet<>();
3872         if (SHOW_SYSTEMS) {
3873             System.out.println("\n# Show Unit Systems\n#Unit\tCLDR\tNIST*");
3874         }
3875         for (String unit : converter.getSimpleUnits()) {
3876             final Set<UnitSystem> cldrSystems = converter.getSystemsEnum(unit);
3877             ExternalUnitConversionData nistInfo = NistUnits.unitToData.get(unit);
3878             final Set<UnitSystem> nistSystems = nistInfo == null ? Set.of() : nistInfo.systems;
3879             if (SHOW_SYSTEMS) {
3880                 System.out.println(
3881                         unit //
3882                                 + "\t"
3883                                 + JOIN_COMMA.join(cldrSystems) //
3884                                 + "\t"
3885                                 + (nistInfo == null ? "" : JOIN_COMMA.join(nistInfo.systems)));
3886             }
3887             UnitSystemInvariant.test(unit, cldrSystems, fails);
3888             if (!nistSystems.isEmpty() && !cldrSystems.containsAll(nistSystems)
3889                     || cldrSystems.contains(UnitSystem.si) && !nistSystems.contains(UnitSystem.si)
3890                     || cldrSystems.contains(UnitSystem.si_acceptable)
3891                             && !nistSystems.contains(UnitSystem.si_acceptable)) {
3892                 if (unit.equals("100-kilometer") || unit.equals("light-speed")) {
3893                     continue;
3894                 }
3895                 fails.add(
3896                         "**\t"
3897                                 + unit
3898                                 + " nistSystems="
3899                                 + nistSystems
3900                                 + " cldrSystems="
3901                                 + cldrSystems);
3902             }
3903         }
3904         if (!fails.isEmpty()) {
3905             errln("Mismatch between NIST and CLDR UnitSystems");
3906             for (String fail : fails) {
3907                 System.out.println(fail);
3908             }
3909         }
3910         if (!SHOW_SYSTEMS) {
3911             warnln("Use -DTestUnits:SHOW_SYSTEMS to see the unit systems for units in units.xml");
3912         }
3913     }
3914 
3915     static class UnitSystemInvariant {
3916         UnitSystem source;
3917         Set<String> exceptUnits;
3918         UnitSystem contains;
3919         boolean invert;
3920 
3921         static final Set<UnitSystemInvariant> invariants =
3922                 Set.of(
3923                         new UnitSystemInvariant(UnitSystem.si, null, UnitSystem.metric, true),
3924                         new UnitSystemInvariant(
3925                                 UnitSystem.si_acceptable,
3926                                 Set.of(
3927                                         "knot",
3928                                         "astronomical-unit",
3929                                         "nautical-mile",
3930                                         "minute",
3931                                         "hour",
3932                                         "day",
3933                                         "arc-second",
3934                                         "arc-minute",
3935                                         "degree",
3936                                         "electronvolt",
3937                                         "light-speed"),
3938                                 UnitSystem.metric,
3939                                 true), //
3940                         new UnitSystemInvariant(
3941                                 UnitSystem.si,
3942                                 Set.of("kilogram", "celsius", "radian", "katal", "steradian"),
3943                                 UnitSystem.prefixable,
3944                                 true),
3945                         new UnitSystemInvariant(
3946                                 UnitSystem.metric,
3947                                 Set.of(
3948                                         "hectare",
3949                                         "100-kilometer",
3950                                         "kilogram",
3951                                         "celsius",
3952                                         "radian",
3953                                         "katal",
3954                                         "steradian"),
3955                                 UnitSystem.prefixable,
3956                                 true));
3957 
3958         /**
3959          * If a set of systems contains source, then it must contain contained (if invert == true)
3960          * or must not (if invert = false).
3961          */
UnitSystemInvariant( UnitSystem source, Set<String> exceptUnits, UnitSystem contained, boolean invert)3962         public UnitSystemInvariant(
3963                 UnitSystem source, Set<String> exceptUnits, UnitSystem contained, boolean invert) {
3964             this.source = source;
3965             this.exceptUnits = exceptUnits == null ? Set.of() : exceptUnits;
3966             this.contains = contained;
3967             this.invert = invert;
3968         }
3969 
ok(String unit, Set<UnitSystem> trial)3970         public boolean ok(String unit, Set<UnitSystem> trial) {
3971             if (!trial.contains(source) || exceptUnits.contains(unit)) {
3972                 return true;
3973             }
3974             if (trial.contains(contains) == invert) {
3975                 return true;
3976             }
3977             return false;
3978         }
3979 
test(String unit, Set<UnitSystem> systems, Set<String> fails)3980         static void test(String unit, Set<UnitSystem> systems, Set<String> fails) {
3981             for (UnitSystemInvariant invariant : invariants) {
3982                 if (!invariant.ok(unit, systems)) {
3983                     if (unit.equals("100-kilometer")) {
3984                         continue;
3985                     }
3986                     fails.add("*\t" + unit + "\tfails\t" + invariant);
3987                 }
3988             }
3989         }
3990 
3991         @Override
toString()3992         public String toString() {
3993             return source + (invert ? " doesn't contain " : " contains ") + contains;
3994         }
3995     }
3996 
TestRationalFormatting()3997     public void TestRationalFormatting() {
3998         Rational.RationalParser rationalParser = new RationalParser();
3999         List<List<String>> tests =
4000                 List.of(
4001                         List.of("plain", "PI", "411557987/131002976"),
4002                         //
4003                         List.of("approx", "125/7", "125/7"),
4004                         List.of("approx", "0.0000007˙716049382", "~771.6×10ˆ-9"),
4005                         List.of("approx", "PI", "~3.1416"),
4006                         //
4007                         List.of("repeating", "125/7", "17.˙857142"),
4008                         List.of("repeating", "0.0000007˙716049382", "0.0000007˙716049382"),
4009                         List.of("repeating", "PI", "12,861,187.09375/4093843"),
4010                         //
4011                         List.of("repeatingAll", "123456/7919", "123,456/7919"),
4012                         List.of("repeatingAll", "PI", "12,861,187.09375/4093843"),
4013                         //
4014                         List.of("formatted", "PI", "12,861,187.09375/4093843"),
4015                         //
4016                         List.of("html", "PI", "<sup>12,861,187.09375</sup>/<sub>4093843<sub>"));
4017         int i = 0;
4018         for (List<String> test : tests) {
4019             FormatStyle formatStyle = FormatStyle.valueOf(test.get(0));
4020             String rawSource = test.get(1);
4021             Rational source = converter.getConstants().get(rawSource);
4022             if (source == null) {
4023                 source = rationalParser.parse(rawSource);
4024             }
4025             String expected = test.get(2);
4026             assertEquals(
4027                     ++i + ") " + formatStyle + "(" + rawSource + ")",
4028                     expected,
4029                     source.toString(formatStyle));
4030         }
4031     }
4032 
TestSystems2()4033     public void TestSystems2() {
4034         Multimap<String, UnitSystem> unitToSystems = converter.getSourceToSystems();
4035         final Comparator<Iterable<UnitSystem>> systemComparator =
4036                 Comparators.lexicographical(Comparator.<UnitSystem>naturalOrder());
4037         Multimap<UnitSystem, String> systemToUnits =
4038                 Multimaps.invertFrom(unitToSystems, TreeMultimap.create());
4039         assertEquals("other doesn't occur", Set.of(), systemToUnits.get(UnitSystem.other));
4040 
4041         Multimap<Set<UnitSystem>, String> systemSetToUnits =
4042                 TreeMultimap.create(systemComparator, Comparator.<String>naturalOrder());
4043 
4044         // skip prefixable, since it isn't relevant
4045 
4046         for (Entry<String, Collection<UnitSystem>> entry : unitToSystems.asMap().entrySet()) {
4047             Set<UnitSystem> systemSet =
4048                     ImmutableSortedSet.copyOf(
4049                             Sets.difference(
4050                                     new TreeSet<>(entry.getValue()),
4051                                     Set.of(UnitSystem.prefixable)));
4052             systemSetToUnits.put(systemSet, entry.getKey());
4053         }
4054         if (SHOW_SYSTEMS) {
4055             System.out.println();
4056             System.out.println("Set of UnitSystems\tUnits they apply to");
4057         }
4058 
4059         Set<String> ONLY_METRIC_AND_OTHERS = Set.of("second", "byte", "bit");
4060         // Test some current invariants
4061 
4062         for (Entry<Set<UnitSystem>, Collection<String>> entry :
4063                 systemSetToUnits.asMap().entrySet()) {
4064             final Set<UnitSystem> systemSet = entry.getKey();
4065             final Collection<String> unitSet = entry.getValue();
4066             if (SHOW_SYSTEMS) {
4067                 System.out.println(systemSet + "\t" + unitSet);
4068             }
4069             if (systemSet.contains(UnitSystem.si)) {
4070                 assertNotContains(systemSet, UnitSystem.si_acceptable, unitSet);
4071                 assertContains(systemSet, UnitSystem.metric, unitSet);
4072             }
4073             if (systemSet.contains(UnitSystem.metric)) {
4074                 assertNotContains(systemSet, UnitSystem.metric_adjacent, unitSet);
4075                 if (!ONLY_METRIC_AND_OTHERS.containsAll(unitSet)) {
4076                     assertNotContains(systemSet, UnitSystem.ussystem, unitSet);
4077                     assertNotContains(systemSet, UnitSystem.uksystem, unitSet);
4078                     assertNotContains(systemSet, UnitSystem.jpsystem, unitSet);
4079                 }
4080             }
4081         }
4082         if (SHOW_SYSTEMS) {
4083             System.out.print("Unit\tQuantity");
4084             for (UnitSystem sys : UnitSystem.ALL) {
4085                 System.out.print("\t" + sys);
4086             }
4087             System.out.println();
4088 
4089             for (Entry<String, Collection<UnitSystem>> entry : unitToSystems.asMap().entrySet()) {
4090                 final TreeSet<UnitSystem> systemSet = new TreeSet<>(entry.getValue());
4091                 final String unit = entry.getKey();
4092                 systemSetToUnits.put(systemSet, unit);
4093                 System.out.print(unit);
4094                 System.out.print("\t");
4095                 System.out.print(converter.getQuantityFromUnit(unit, false));
4096                 for (UnitSystem sys : UnitSystem.ALL) {
4097                     System.out.print("\t" + (systemSet.contains(sys) ? "Y" : ""));
4098                 }
4099                 System.out.println();
4100             }
4101         }
4102         warnln("Use -DTestUnits:SHOW_SYSTEMS to see details");
4103     }
4104 
assertContains( final Set<T> systemSet, T unitSystem, Collection<String> units)4105     public <T> boolean assertContains(
4106             final Set<T> systemSet, T unitSystem, Collection<String> units) {
4107         return assertTrue(
4108                 units + ": " + systemSet + " contains " + unitSystem,
4109                 systemSet.contains(unitSystem));
4110     }
4111 
assertNotContains( final Set<T> systemSet, T unitSystem, Collection<String> units)4112     public <T> boolean assertNotContains(
4113             final Set<T> systemSet, T unitSystem, Collection<String> units) {
4114         return assertFalse(
4115                 units + ": " + systemSet + " does not contain " + unitSystem,
4116                 systemSet.contains(unitSystem));
4117     }
4118 
testQuantitiesMissingFromPreferences()4119     public void testQuantitiesMissingFromPreferences() {
4120         UnitPreferences prefs = SDI.getUnitPreferences();
4121         Set<String> preferenceQuantities = prefs.getQuantities();
4122         Set<String> unitQuantities = converter.getQuantities();
4123         assertEquals(
4124                 "pref - unit quantities",
4125                 Collections.emptySet(),
4126                 Sets.difference(preferenceQuantities, unitQuantities));
4127         final SetView<String> quantitiesNotInPreferences =
4128                 Sets.difference(unitQuantities, preferenceQuantities);
4129         if (!quantitiesNotInPreferences.isEmpty()) {
4130             warnln("unit - pref quantities = " + quantitiesNotInPreferences);
4131         }
4132         for (String unit : converter.getSimpleUnits()) {
4133             String quantity = converter.getQuantityFromUnit(unit, false);
4134             if (!quantitiesNotInPreferences.contains(quantity)) {
4135                 continue;
4136             }
4137             // we have a unit whose quantity is not in preferences
4138             // get its unit preferences
4139             UnitPreference pref =
4140                     prefs.getUnitPreference(Rational.ONE, unit, "default", ULocale.US);
4141             if (pref == null) {
4142                 errln(
4143                         String.format(
4144                                 "Default preference is null: input unit=%s, quantity=%s",
4145                                 unit, quantity));
4146                 continue;
4147             }
4148             // ensure that it is metric
4149             Set<UnitSystem> inputSystems = converter.getSystemsEnum(unit);
4150             if (Collections.disjoint(inputSystems, UnitSystem.SiOrMetric)) {
4151                 warnln(
4152                         String.format(
4153                                 "There are no explicit preferences for %s, but %s is not metric",
4154                                 quantity, unit));
4155             }
4156             Set<UnitSystem> prefSystems = converter.getSystemsEnum(pref.unit);
4157 
4158             String errorOrWarningString =
4159                     String.format(
4160                             "Test default preference is metric: input unit=%s, quantity=%s, pref-unit=%s, systems: %s",
4161                             unit, quantity, pref.unit, prefSystems);
4162             if (Collections.disjoint(prefSystems, UnitSystem.SiOrMetric)) {
4163                 errln(errorOrWarningString);
4164             } else {
4165                 logln("OK " + errorOrWarningString);
4166             }
4167         }
4168     }
4169 
testUnitPreferencesTest()4170     public void testUnitPreferencesTest() {
4171         try {
4172             final Set<String> warnings = new LinkedHashSet<>();
4173             Files.lines(Path.of(CLDRPaths.TEST_DATA + "units/unitPreferencesTest.txt"))
4174                     .forEach(line -> checkUnitPreferencesTest(line, warnings));
4175             if (!warnings.isEmpty()) {
4176                 warnln("Mixed unit identifiers not yet checked, count=" + warnings.size());
4177             }
4178         } catch (IOException e) {
4179             throw new ICUUncheckedIOException(e);
4180         }
4181     }
4182 
checkUnitPreferencesTest(String line, Set<String> warnings)4183     public void checkUnitPreferencesTest(String line, Set<String> warnings) {
4184         if (line.startsWith("#") || line.isBlank()) {
4185             return;
4186         }
4187         // #    Quantity;   Usage;  Region; Input (r);  Input (d);  Input Unit; Output (r);
4188         // Output (d); Output Unit
4189         // Example:
4190         // area;      default;    001;    1100000;    1100000.0;  square-meter;
4191         // 11/10;  1.1;    square-kilometer
4192         // duration;   media;     001;    66;         66.0;       second;        1; minute;   6;
4193         //      6.0;    second
4194         try {
4195             UnitPreferences prefs = SDI.getUnitPreferences();
4196             List<String> parts = SPLIT_SEMI.splitToList(line);
4197             Map<String, Long> highMixed_unit_identifiers = new LinkedHashMap<>();
4198             String quantity = parts.get(0);
4199             String usage = parts.get(1);
4200             String region = parts.get(2);
4201             Rational inputRational = Rational.of(parts.get(3));
4202             double inputDouble = Double.parseDouble(parts.get(4));
4203             String inputUnit = parts.get(5);
4204             // account for multi-part output
4205             int size = parts.size();
4206             // This section has larger elements with integer values
4207             for (int i = 6; i < size - 3; i += 2) {
4208                 highMixed_unit_identifiers.put(parts.get(i + 1), Long.parseLong(parts.get(i)));
4209             }
4210             Rational expectedValue = Rational.of(parts.get(size - 3));
4211             Double expectedValueDouble = Double.parseDouble(parts.get(size - 2));
4212             String expectedOutputUnit = parts.get(size - 1);
4213 
4214             // Check that the double values are approximately the same as
4215             // the Rational ones
4216             assertTrue(
4217                     String.format(
4218                             "input rational ~ input double, %s %s", inputRational, inputDouble),
4219                     inputRational.approximatelyEquals(inputDouble));
4220             assertTrue(
4221                     String.format(
4222                             "output rational ~ output double, %s %s",
4223                             expectedValue, expectedValueDouble),
4224                     expectedValue.approximatelyEquals(expectedValueDouble));
4225 
4226             // check that the quantity is consistent
4227             String expectedQuantity = converter.getQuantityFromUnit(inputUnit, false);
4228             assertEquals("Input: Quantity consistency check", expectedQuantity, quantity);
4229 
4230             // TODO handle mixed_unit_identifiers
4231             if (!highMixed_unit_identifiers.isEmpty()) {
4232                 warnings.add("mixed_unit_identifiers not yet checked: " + line);
4233                 return;
4234             }
4235             // check output unit, then value
4236             UnitPreference unitPreference =
4237                     prefs.getUnitPreference(inputRational, inputUnit, usage, region);
4238             String actualUnit = unitPreference.unit;
4239             assertEquals("Output unit", expectedOutputUnit, actualUnit);
4240 
4241             Rational actualValue = converter.convert(inputRational, inputUnit, actualUnit, false);
4242             assertEquals("Output numeric value", expectedValue, actualValue);
4243         } catch (Exception e) {
4244             errln(e.getMessage() + "\n\t" + line);
4245         }
4246     }
4247 
testUnitsTest()4248     public void testUnitsTest() {
4249         try {
4250             Files.lines(Path.of(CLDRPaths.TEST_DATA + "units/unitsTest.txt"))
4251                     .forEach(line -> checkUnitsTest(line));
4252         } catch (IOException e) {
4253             throw new ICUUncheckedIOException(e);
4254         }
4255     }
4256 
checkUnitsTest(String line)4257     private void checkUnitsTest(String line) {
4258         if (line.startsWith("#") || line.isBlank()) {
4259             return;
4260         }
4261         // Quantity    ;   x   ;   y   ;   conversion to y (rational)  ;   test: 1000 x ⟹ y
4262         //
4263         //        Use: convert 1000 x units to the y unit; the result should match the final column,
4264         //           at the given precision. For example, when the last column is 159.1549,
4265         //           round to 4 decimal digits before comparing.
4266         // Example:
4267         //        acceleration  ;   g-force ;   meter-per-square-second ;   9.80665 * x ;   9806.65
4268         try {
4269             UnitPreferences prefs = SDI.getUnitPreferences();
4270             List<String> parts = SPLIT_SEMI.splitToList(line);
4271             String quantity = parts.get(0);
4272             String sourceUnit = parts.get(1);
4273             String targetUnit = parts.get(2);
4274             String conversion = parts.get(3);
4275             double expectedNumericValueFor1000 = Rational.of(parts.get(4)).doubleValue();
4276 
4277             String expectedQuantity = converter.getQuantityFromUnit(sourceUnit, false);
4278             assertEquals("Input: Quantity consistency check", expectedQuantity, quantity);
4279 
4280             // TODO check conversion equation (not particularly important
4281             Rational actualValue =
4282                     converter.convert(Rational.of(1000), sourceUnit, targetUnit, false);
4283             assertTrue(
4284                     String.format(
4285                             "output rational ~ expected double, %s %s",
4286                             expectedNumericValueFor1000, actualValue.doubleValue()),
4287                     actualValue.approximatelyEquals(expectedNumericValueFor1000));
4288         } catch (Exception e) {
4289             errln(e.getMessage() + "\n\t" + line);
4290         }
4291     }
4292 
testUnitLocalePreferencesTest()4293     public void testUnitLocalePreferencesTest() {
4294         try {
4295             Files.lines(Path.of(CLDRPaths.TEST_DATA + "units/unitLocalePreferencesTest.txt"))
4296                     .forEach(line -> checkUnitLocalePreferencesTest(line));
4297         } catch (IOException e) {
4298             throw new ICUUncheckedIOException(e);
4299         }
4300     }
4301 
checkUnitLocalePreferencesTest(String rawLine)4302     private void checkUnitLocalePreferencesTest(String rawLine) {
4303         int hashPos = rawLine.indexOf('#');
4304         String line = hashPos < 0 ? rawLine : rawLine.substring(0, hashPos);
4305         String comment = hashPos < 0 ? "" : "\t# " + rawLine.substring(hashPos + 1);
4306         if (line.isBlank()) {
4307             return;
4308         }
4309         // #    input-unit; amount; usage;  languageTag; expected-unit; expected-amount # comment
4310         // Example:
4311         // fahrenheit;  1;  default;    en-u-rg-uszzzz-ms-ussystem-mu-celsius;  celsius;    -155/9 #
4312         // mu > ms > rg > (likely) region
4313         try {
4314             UnitPreferences prefs = SDI.getUnitPreferences();
4315             List<String> parts = SPLIT_SEMI.splitToList(line);
4316             String sourceUnit = parts.get(0);
4317             Rational sourceAmount = Rational.of(parts.get(1));
4318             String usage = parts.get(2);
4319             String languageTag = parts.get(3);
4320             String expectedUnit = parts.get(4);
4321             Rational expectedAmount = Rational.of(parts.get(5));
4322 
4323             String actualUnit;
4324             Rational actualValue;
4325             try {
4326                 if (DEBUG)
4327                     System.out.println(
4328                             String.format(
4329                                     "%s;\t%s;\t%s;\t%s;\t%s;\t%s%s",
4330                                     sourceUnit,
4331                                     sourceAmount.toString(FormatStyle.formatted),
4332                                     usage,
4333                                     languageTag,
4334                                     expectedUnit,
4335                                     expectedAmount.toString(FormatStyle.formatted),
4336                                     comment));
4337 
4338                 final ULocale uLocale = ULocale.forLanguageTag(languageTag);
4339                 UnitPreference unitPreference =
4340                         prefs.getUnitPreference(sourceAmount, sourceUnit, usage, uLocale);
4341                 if (unitPreference == null) { // if the quantity isn't found
4342                     throw new IllegalArgumentException(
4343                             String.format(
4344                                     "No unit preferences found for unit: %s, usage: %s, locale:%s",
4345                                     sourceUnit, usage, languageTag));
4346                 }
4347                 actualUnit = unitPreference.unit;
4348                 actualValue =
4349                         converter.convert(sourceAmount, sourceUnit, unitPreference.unit, false);
4350             } catch (Exception e1) {
4351                 actualUnit = e1.getMessage();
4352                 actualValue = Rational.NaN;
4353             }
4354             if (assertEquals(
4355                     String.format(
4356                             "ICU unit pref, %s %s %s %s",
4357                             sourceUnit,
4358                             sourceAmount.toString(FormatStyle.formatted),
4359                             usage,
4360                             languageTag),
4361                     expectedUnit,
4362                     actualUnit)) {
4363                 assertEquals("CLDR value", expectedAmount, actualValue);
4364             } else if (!comment.isBlank()) {
4365                 warnln(comment);
4366             }
4367 
4368         } catch (Exception e) {
4369             errln(e.getStackTrace()[0] + ", " + e.getMessage() + "\n\t" + rawLine);
4370         }
4371     }
4372 
4373     public void testUnitLocalePreferencesTestIcu() {
4374         if (TEST_ICU) {
4375             try {
4376                 Files.lines(Path.of(CLDRPaths.TEST_DATA + "units/unitLocalePreferencesTest.txt"))
4377                         .forEach(line -> checkUnitLocalePreferencesTestIcu(line));
4378             } catch (IOException e) {
4379                 throw new ICUUncheckedIOException(e);
4380             }
4381         } else {
4382             warnln("Skipping ICU test. To enable, set -DTestUnits:TEST_ICU");
4383         }
4384     }
4385 
4386     private void checkUnitLocalePreferencesTestIcu(String rawLine) {
4387         int hashPos = rawLine.indexOf('#');
4388         String line = hashPos < 0 ? rawLine : rawLine.substring(0, hashPos);
4389         String comment = hashPos < 0 ? "" : "\t# " + rawLine.substring(hashPos + 1);
4390         if (line.isBlank()) {
4391             return;
4392         }
4393         // #    input-unit; amount; usage;  languageTag; expected-unit; expected-amount # comment
4394         // Example:
4395         // fahrenheit;  1;  default;    en-u-rg-uszzzz-ms-ussystem-mu-celsius;  celsius;    -155/9 #
4396         // mu > ms > rg > (likely) region
4397         try {
4398             List<String> parts = SPLIT_SEMI.splitToList(line);
4399             String sourceUnit = parts.get(0);
4400             double sourceAmount = icuRational(parts.get(1));
4401             String usage = parts.get(2);
4402             String languageTag = parts.get(3);
4403             String expectedUnit = parts.get(4);
4404             double expectedAmount = icuRational(parts.get(5));
4405 
4406             String actualUnit;
4407 
4408             float actualValueFloat;
4409             try {
4410                 UnlocalizedNumberFormatter nf =
4411                         NumberFormatter.with()
4412                                 .unitWidth(UnitWidth.FULL_NAME)
4413                                 .precision(Precision.maxSignificantDigits(20));
4414                 LocalizedNumberFormatter localized =
4415                         nf.usage(usage).locale(Locale.forLanguageTag(languageTag));
4416                 final FormattedNumber formatted =
4417                         localized.format(
4418                                 new Measure(sourceAmount, MeasureUnit.forIdentifier(sourceUnit)));
4419                 MeasureUnit icuOutputUnit = formatted.getOutputUnit();
4420                 actualUnit = icuOutputUnit.getSubtype();
4421                 actualValueFloat = formatted.toBigDecimal().floatValue();
4422             } catch (Exception e) {
4423                 actualUnit = e.getMessage();
4424                 actualValueFloat = Float.NaN;
4425             }
4426             if (!expectedUnit.equals(actualUnit)) {
4427                 if (!logKnownIssue("CLDR-17581", "No null from unitPreferences")) {
4428 
4429                     assertEquals(
4430                             String.format(
4431                                     "ICU unit pref, %s %s %s %s",
4432                                     sourceUnit, sourceAmount, usage, languageTag),
4433                             expectedUnit,
4434                             actualUnit);
4435                 }
4436             } else if (assertEquals("ICU value", (float) expectedAmount, actualValueFloat)) {
4437                 // no other action
4438             } else if (!comment.isBlank()) {
4439                 warnln(comment);
4440             }
4441         } catch (Exception e) {
4442             errln(e.getStackTrace()[0] + ", " + e.getMessage() + "\n\t" + rawLine);
4443         }
4444     }
4445 
4446     private double icuRational(String string) {
4447         string = string.replace(",", "");
4448         int slashPos = string.indexOf('/');
4449         if (slashPos < 0) {
4450             return Double.parseDouble(string);
4451         } else {
4452             return Double.parseDouble(string.substring(0, slashPos))
4453                     / Double.parseDouble(string.substring(slashPos + 1));
4454         }
4455     }
4456 
4457     public void testFactorsInUnits() {
4458         String[][] tests = {
4459             {
4460                 "2-meter-3-kilogram-per-5-second-7-ampere",
4461                 "kilogram-meter-per-second-ampere",
4462                 "6/35"
4463             },
4464             {"1e9-kilogram", "kilogram", "1000000000"},
4465             {"item-per-1e9-cubic-meter", "item-per-cubic-meter", "1/1000000000"},
4466             {"gram-per-1e9-cubic-meter", "kilogram-per-cubic-meter", "1/1000000000000"},
4467             {"item-per-1e9-item", "item-per-item", "1/1000000000"},
4468             {"item-per-1e9", "item", "1/1000000000"},
4469         };
4470         checkConversionToBase(tests);
4471     }
4472 
4473     public void checkConversionToBase(String[][] tests) {
4474         int count = 0;
4475         for (String[] test : tests) {
4476             ++count;
4477             Output<String> metric = new Output<>();
4478             final String inputUnit = test[0];
4479             final String expectedTarget = test[1];
4480             final Rational expectedFactor = Rational.of(test[2]);
4481             ConversionInfo unitId1 = converter.parseUnitId(inputUnit, metric, DEBUG);
4482             assertEquals(count + ")\t" + inputUnit + " to base", expectedTarget, metric.value);
4483             assertEquals(count + ")\t" + inputUnit + " factor", expectedFactor, unitId1.factor);
4484         }
4485     }
4486 
4487     public void testLightSpeed() {
4488         String[][] tests = {
4489             {"light-speed", "meter-per-second", "299792458"},
4490             {"light-speed-second", "meter-second-per-second", "299792458"},
4491             {"light-speed-minute", "meter-second-per-second", "299792458*60"},
4492             {"light-speed-hour", "meter-second-per-second", "299792458*3600"},
4493             {"light-speed-day", "meter-second-per-second", "299792458*86400"},
4494             {"light-speed-week", "meter-second-per-second", "299792458*604800"},
4495         };
4496         checkConversionToBase(tests);
4497 
4498         Factory factory = CLDR_CONFIG.getFullCldrFactory();
4499         Set<String> available = factory.getAvailableLanguages();
4500         Set<String> TC =
4501                 Sets.intersection(
4502                         available,
4503                         Sets.difference(
4504                                 STANDARD_CODES.getLocaleCoverageLocales(Organization.cldr),
4505                                 STANDARD_CODES.getLocaleCoverageLocales(Organization.special)));
4506         // UnitId id = converter.createUnitId("light-speed-second");
4507         // String lightSeconds1 = id.toString(cldrFile,"long", "other", "nominative", null, true);
4508         if (isVerbose()) {
4509             System.out.println(
4510                     "\nlocale, times, light, years, lightYears, lightYearsC".replace(", ", "\t"));
4511         }
4512         Set<Level> neededCoverageLevel = Set.of(Level.MODERATE, Level.MODERN, Level.COMPREHENSIVE);
4513         for (String locale : TC) {
4514             Level coverage = STANDARD_CODES.getLocaleCoverageLevel(Organization.cldr, locale);
4515             if (!neededCoverageLevel.contains(coverage)) {
4516                 continue;
4517             }
4518             CLDRFile cldrFile = factory.make(locale, true);
4519             String lightYears =
4520                     clean(
4521                             cldrFile.getStringValue(
4522                                     "//ldml/units/unitLength[@type=\"long\"]/unit[@type=\"length-light-year\"]/unitPattern[@count=\"other\"]"));
4523             String years =
4524                     clean(
4525                             cldrFile.getStringValue(
4526                                     "//ldml/units/unitLength[@type=\"long\"]/unit[@type=\"duration-year\"]/unitPattern[@count=\"other\"]"));
4527             String light =
4528                     clean(
4529                             cldrFile.getStringValue(
4530                                     "//ldml/units/unitLength[@type=\"long\"]/unit[@type=\"speed-light-speed\"]/unitPattern[@count=\"one\"]"));
4531             String times =
4532                     cldrFile.getStringValue(
4533                             "//ldml/units/unitLength[@type=\"long\"]/compoundUnit[@type=\"times\"]/compoundUnitPattern");
4534             String lightYearsC = MessageFormat.format(times, light, clean(years));
4535             // String seconds =
4536             // clean(cldrFile.getStringValue("//ldml/units/unitLength[@type=\"long\"]/unit[@type=\"duration-second\"]/unitPattern[@count=\"other\"]"));
4537             // String lightSecondsC = MessageFormat.format(times, light, clean(seconds));
4538 
4539             times = clean(times);
4540             if (isVerbose()) {
4541                 System.out.println(
4542                         JOIN_TAB.join(locale, times, light, years, lightYears, lightYearsC));
4543             }
4544         }
4545     }
4546 
4547     public String clean(String unitPattern) {
4548         return unitPattern.replace("{0}", "").replace("{1}", "").trim();
4549     }
4550 }
4551