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