• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.unicode.cldr.unittest;
2 
3 import java.io.File;
4 import java.io.IOException;
5 import java.io.OutputStreamWriter;
6 import java.io.PrintWriter;
7 import java.math.BigDecimal;
8 import java.math.BigInteger;
9 import java.math.MathContext;
10 import java.nio.file.Files;
11 import java.util.ArrayList;
12 import java.util.Arrays;
13 import java.util.Collection;
14 import java.util.Collections;
15 import java.util.Comparator;
16 import java.util.HashSet;
17 import java.util.LinkedHashMap;
18 import java.util.LinkedHashSet;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Map.Entry;
22 import java.util.Objects;
23 import java.util.Set;
24 import java.util.TreeMap;
25 import java.util.TreeSet;
26 import java.util.logging.Logger;
27 import java.util.regex.Matcher;
28 import java.util.regex.Pattern;
29 import java.util.stream.Collectors;
30 import java.util.stream.Stream;
31 import java.util.stream.StreamSupport;
32 
33 import org.unicode.cldr.draft.FileUtilities;
34 import org.unicode.cldr.test.CheckCLDR.CheckStatus;
35 import org.unicode.cldr.test.CheckCLDR.Options;
36 import org.unicode.cldr.test.CheckUnits;
37 import org.unicode.cldr.test.ExampleGenerator;
38 import org.unicode.cldr.util.CLDRConfig;
39 import org.unicode.cldr.util.CLDRFile;
40 import org.unicode.cldr.util.ChainedMap;
41 import org.unicode.cldr.util.ChainedMap.M3;
42 import org.unicode.cldr.util.ChainedMap.M4;
43 import org.unicode.cldr.util.CldrUtility;
44 import org.unicode.cldr.util.Counter;
45 import org.unicode.cldr.util.DtdData;
46 import org.unicode.cldr.util.DtdType;
47 import org.unicode.cldr.util.Factory;
48 import org.unicode.cldr.util.GrammarDerivation;
49 import org.unicode.cldr.util.GrammarInfo;
50 import org.unicode.cldr.util.GrammarInfo.GrammaticalFeature;
51 import org.unicode.cldr.util.GrammarInfo.GrammaticalScope;
52 import org.unicode.cldr.util.GrammarInfo.GrammaticalTarget;
53 import org.unicode.cldr.util.LocaleStringProvider;
54 import org.unicode.cldr.util.MapComparator;
55 import org.unicode.cldr.util.Organization;
56 import org.unicode.cldr.util.Pair;
57 import org.unicode.cldr.util.PathHeader;
58 import org.unicode.cldr.util.Rational;
59 import org.unicode.cldr.util.Rational.ContinuedFraction;
60 import org.unicode.cldr.util.Rational.FormatStyle;
61 import org.unicode.cldr.util.Rational.RationalParser;
62 import org.unicode.cldr.util.SimpleXMLSource;
63 import org.unicode.cldr.util.StandardCodes;
64 import org.unicode.cldr.util.StandardCodes.LstrType;
65 import org.unicode.cldr.util.SupplementalDataInfo;
66 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo;
67 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo.Count;
68 import org.unicode.cldr.util.SupplementalDataInfo.PluralType;
69 import org.unicode.cldr.util.SupplementalDataInfo.UnitIdComponentType;
70 import org.unicode.cldr.util.UnitConverter;
71 import org.unicode.cldr.util.UnitConverter.Continuation;
72 import org.unicode.cldr.util.UnitConverter.Continuation.UnitIterator;
73 import org.unicode.cldr.util.UnitConverter.ConversionInfo;
74 import org.unicode.cldr.util.UnitConverter.TargetInfo;
75 import org.unicode.cldr.util.UnitConverter.UnitComplexity;
76 import org.unicode.cldr.util.UnitConverter.UnitId;
77 import org.unicode.cldr.util.UnitConverter.UnitSystem;
78 import org.unicode.cldr.util.UnitParser;
79 import org.unicode.cldr.util.UnitPathType;
80 import org.unicode.cldr.util.UnitPreferences;
81 import org.unicode.cldr.util.UnitPreferences.UnitPreference;
82 import org.unicode.cldr.util.Units;
83 import org.unicode.cldr.util.Validity;
84 import org.unicode.cldr.util.Validity.Status;
85 import org.unicode.cldr.util.With;
86 import org.unicode.cldr.util.XMLSource;
87 import org.unicode.cldr.util.XPathParts;
88 
89 import com.google.common.base.Joiner;
90 import com.google.common.base.Splitter;
91 import com.google.common.collect.BiMap;
92 import com.google.common.collect.Comparators;
93 import com.google.common.collect.HashMultimap;
94 import com.google.common.collect.ImmutableList;
95 import com.google.common.collect.ImmutableMap;
96 import com.google.common.collect.ImmutableMultimap;
97 import com.google.common.collect.ImmutableSet;
98 import com.google.common.collect.LinkedHashMultimap;
99 import com.google.common.collect.Multimap;
100 import com.google.common.collect.Multimaps;
101 import com.google.common.collect.Ordering;
102 import com.google.common.collect.Sets;
103 import com.google.common.collect.TreeMultimap;
104 import com.ibm.icu.dev.test.TestFmwk;
105 import com.ibm.icu.impl.Row;
106 import com.ibm.icu.impl.Row.R2;
107 import com.ibm.icu.impl.Row.R3;
108 import com.ibm.icu.text.PluralRules;
109 import com.ibm.icu.text.UnicodeSet;
110 import com.ibm.icu.util.ICUUncheckedIOException;
111 import com.ibm.icu.util.Output;
112 
113 public class TestUnits extends TestFmwk {
114     private static final Set<String> VALID_REGULAR_UNITS = Validity.getInstance().getStatusToCodes(LstrType.unit).get(Validity.Status.regular);
115     private static final Set<String> DEPRECATED_REGULAR_UNITS = Validity.getInstance().getStatusToCodes(LstrType.unit).get(Validity.Status.deprecated);
116     private static final CLDRConfig CLDR_CONFIG = CLDRConfig.getInstance();
117     private static final Integer INTEGER_ONE = Integer.valueOf(1);
118     private static final boolean SHOW_DATA = CldrUtility.getProperty("TestUnits:SHOW_DATA", false); // set for verbose debugging information
119     private static final boolean SHOW_COMPOSE = CldrUtility.getProperty("TestUnits:SHOW_COMPOSE", false); // set for verbose debugging information
120     private static final boolean GENERATE_TESTS = CldrUtility.getProperty("TestUnits:GENERATE_TESTS", false);
121 
122     private static final String TEST_SEP = ";\t";
123 
124     private static final ImmutableSet<String> WORLD_SET = ImmutableSet.of("001");
125     private static final CLDRConfig info = CLDR_CONFIG;
126     private static final SupplementalDataInfo SDI = info.getSupplementalDataInfo();
127 
128     static final UnitConverter converter = SDI.getUnitConverter();
129     static final Splitter SPLIT_SEMI = Splitter.on(Pattern.compile("\\s*;\\s*")).trimResults();
130     static final Splitter SPLIT_SPACE = Splitter.on(' ').trimResults().omitEmptyStrings();
131     static final Splitter SPLIT_AND = Splitter.on("-and-").trimResults().omitEmptyStrings();
132     static final Splitter SPLIT_DASH = Splitter.on('-').trimResults().omitEmptyStrings();
133 
134     static final Rational R1000 = Rational.of(1000);
135 
136     static Map<String,String> normalizationCache = new TreeMap<>();
137 
main(String[] args)138     public static void main(String[] args) {
139         new TestUnits().run(args);
140     }
141 
142     private Map<String, String> BASE_UNIT_TO_QUANTITY = converter.getBaseUnitToQuantity();
143 
TestSpaceInNarrowUnits()144     public void TestSpaceInNarrowUnits() {
145         final CLDRFile english = CLDR_CONFIG.getEnglish();
146         final Matcher m = Pattern.compile("narrow.*unitPattern").matcher("");
147         for (String path : english) {
148             if (m.reset(path).find()) {
149                 String value = english.getStringValue(path);
150                 if (value.contains("} ")) {
151                     errln(path + " fails, «" + value + "» contains } + space");
152                 }
153             }
154         }
155     }
156 
157     static final String[][] COMPOUND_TESTS = {
158         {"area-square-centimeter", "square", "length-centimeter"},
159         {"area-square-foot", "square", "length-foot"},
160         {"area-square-inch", "square", "length-inch"},
161         {"area-square-kilometer", "square", "length-kilometer"},
162         {"area-square-meter", "square", "length-meter"},
163         {"area-square-mile", "square", "length-mile"},
164         {"area-square-yard", "square", "length-yard"},
165         {"digital-gigabit", "giga", "digital-bit"},
166         {"digital-gigabyte", "giga", "digital-byte"},
167         {"digital-kilobit", "kilo", "digital-bit"},
168         {"digital-kilobyte", "kilo", "digital-byte"},
169         {"digital-megabit", "mega", "digital-bit"},
170         {"digital-megabyte", "mega", "digital-byte"},
171         {"digital-petabyte", "peta", "digital-byte"},
172         {"digital-terabit", "tera", "digital-bit"},
173         {"digital-terabyte", "tera", "digital-byte"},
174         {"duration-microsecond", "micro", "duration-second"},
175         {"duration-millisecond", "milli", "duration-second"},
176         {"duration-nanosecond", "nano", "duration-second"},
177         {"electric-milliampere", "milli", "electric-ampere"},
178         {"energy-kilocalorie", "kilo", "energy-calorie"},
179         {"energy-kilojoule", "kilo", "energy-joule"},
180         {"frequency-gigahertz", "giga", "frequency-hertz"},
181         {"frequency-kilohertz", "kilo", "frequency-hertz"},
182         {"frequency-megahertz", "mega", "frequency-hertz"},
183         {"graphics-megapixel", "mega", "graphics-pixel"},
184         {"length-centimeter", "centi", "length-meter"},
185         {"length-decimeter", "deci", "length-meter"},
186         {"length-kilometer", "kilo", "length-meter"},
187         {"length-micrometer", "micro", "length-meter"},
188         {"length-millimeter", "milli", "length-meter"},
189         {"length-nanometer", "nano", "length-meter"},
190         {"length-picometer", "pico", "length-meter"},
191         {"mass-kilogram", "kilo", "mass-gram"},
192         {"mass-microgram", "micro", "mass-gram"},
193         {"mass-milligram", "milli", "mass-gram"},
194         {"power-gigawatt", "giga", "power-watt"},
195         {"power-kilowatt", "kilo", "power-watt"},
196         {"power-megawatt", "mega", "power-watt"},
197         {"power-milliwatt", "milli", "power-watt"},
198         {"pressure-hectopascal", "hecto", "pressure-pascal"},
199         {"pressure-millibar", "milli", "pressure-bar"},
200         {"pressure-kilopascal", "kilo", "pressure-pascal"},
201         {"pressure-megapascal", "mega", "pressure-pascal"},
202         {"volume-centiliter", "centi", "volume-liter"},
203         {"volume-cubic-centimeter", "cubic", "length-centimeter"},
204         {"volume-cubic-foot", "cubic", "length-foot"},
205         {"volume-cubic-inch", "cubic", "length-inch"},
206         {"volume-cubic-kilometer", "cubic", "length-kilometer"},
207         {"volume-cubic-meter", "cubic", "length-meter"},
208         {"volume-cubic-mile", "cubic", "length-mile"},
209         {"volume-cubic-yard", "cubic", "length-yard"},
210         {"volume-deciliter", "deci", "volume-liter"},
211         {"volume-hectoliter", "hecto", "volume-liter"},
212         {"volume-megaliter", "mega", "volume-liter"},
213         {"volume-milliliter", "milli", "volume-liter"},
214     };
215 
216     static final String[][] PREFIX_NAME_TYPE = {
217         {"deci", "10p-1"},
218         {"centi", "10p-2"},
219         {"milli", "10p-3"},
220         {"micro", "10p-6"},
221         {"nano", "10p-9"},
222         {"pico", "10p-12"},
223         {"femto", "10p-15"},
224         {"atto", "10p-18"},
225         {"zepto", "10p-21"},
226         {"yocto", "10p-24"},
227         {"deka", "10p1"},
228         {"hecto", "10p2"},
229         {"kilo", "10p3"},
230         {"mega", "10p6"},
231         {"giga", "10p9"},
232         {"tera", "10p12"},
233         {"peta", "10p15"},
234         {"exa", "10p18"},
235         {"zetta", "10p21"},
236         {"yotta", "10p24"},
237         {"square", "power2"},
238         {"cubic", "power3"},
239     };
240 
241     static final String PATH_UNIT_PATTERN = "//ldml/units/unitLength[@type=\"{0}\"]/unit[@type=\"{1}\"]/unitPattern[@count=\"{2}\"]";
242 
243     static final String PATH_PREFIX_PATTERN = "//ldml/units/unitLength[@type=\"{0}\"]/compoundUnit[@type=\"{1}\"]/unitPrefixPattern";
244     static final String PATH_SUFFIX_PATTERN = "//ldml/units/unitLength[@type=\"{0}\"]/compoundUnit[@type=\"{1}\"]/compoundUnitPattern1";
245 
246     static final String PATH_MILLI_PATTERN = "//ldml/units/unitLength[@type=\"{0}\"]/compoundUnit[@type=\"10p-3\"]/unitPrefixPattern";
247     static final String PATH_SQUARE_PATTERN = "//ldml/units/unitLength[@type=\"{0}\"]/compoundUnit[@type=\"power2\"]/compoundUnitPattern1";
248 
249 
250     static final String PATH_METER_PATTERN = "//ldml/units/unitLength[@type=\"{0}\"]/unit[@type=\"length-meter\"]/unitPattern[@count=\"{1}\"]";
251     static final String PATH_MILLIMETER_PATTERN = "//ldml/units/unitLength[@type=\"{0}\"]/unit[@type=\"length-millimeter\"]/unitPattern[@count=\"{1}\"]";
252     static final String PATH_SQUARE_METER_PATTERN = "//ldml/units/unitLength[@type=\"{0}\"]/unit[@type=\"area-square-meter\"]/unitPattern[@count=\"{1}\"]";
253 
TestCompoundUnit3()254     public void TestCompoundUnit3() {
255         Factory factory = CLDR_CONFIG.getCldrFactory();
256 
257         Map<String,String> prefixToType = new LinkedHashMap<>();
258         for (String[] prefixRow : PREFIX_NAME_TYPE) {
259             prefixToType.put(prefixRow[0], prefixRow[1]);
260         }
261         prefixToType = ImmutableMap.copyOf(prefixToType);
262 
263         Set<String> localesToTest = ImmutableSet.of("en"); // factory.getAvailableLanguages();
264         int testCount = 0;
265         for (String locale : localesToTest) {
266             CLDRFile file = factory.make(locale, true);
267             //ExampleGenerator exampleGenerator = getExampleGenerator(locale);
268             PluralInfo pluralInfo = SDI.getPlurals(PluralType.cardinal, locale);
269             final boolean isEnglish = locale.contentEquals("en");
270             int errMsg = isEnglish ? ERR : WARN;
271 
272             for (String[] compoundTest : COMPOUND_TESTS) {
273                 String targetUnit = compoundTest[0];
274                 String prefix = compoundTest[1];
275                 String baseUnit = compoundTest[2];
276                 String prefixType = prefixToType.get(prefix); // will be null for square, cubic
277                 final boolean isPrefix = prefixType.startsWith("1");
278 
279                 for (String len : Arrays.asList("long", "short", "narrow")) {
280                     String prefixPath = ExampleGenerator.format(isPrefix ? PATH_PREFIX_PATTERN
281                         : PATH_SUFFIX_PATTERN,
282                         len, prefixType);
283                     String prefixValue = file.getStringValue(prefixPath);
284                     boolean lowercaseIfSpaced = len.equals("long");
285 
286                     for (Count count : pluralInfo.getCounts()) {
287                         final String countString = count.toString();
288                         String targetUnitPath = ExampleGenerator.format(PATH_UNIT_PATTERN, len, targetUnit, countString);
289                         String targetUnitPattern = file.getStringValue(targetUnitPath);
290 
291                         String baseUnitPath = ExampleGenerator.format(PATH_UNIT_PATTERN, len, baseUnit, countString);
292                         String baseUnitPattern = file.getStringValue(baseUnitPath);
293 
294                         String composedTargetUnitPattern = Units.combinePattern(baseUnitPattern, prefixValue, lowercaseIfSpaced);
295                         if (isEnglish && !targetUnitPattern.equals(composedTargetUnitPattern)) {
296                             if (allowEnglishException(targetUnitPattern, composedTargetUnitPattern)) {
297                                 continue;
298                             }
299                         }
300                         if (!assertEquals2(errMsg, testCount++ + ") "
301                             + locale + "/" + len + "/" + count + "/" + prefix + "+" + baseUnit
302                             + ": constructed pattern",
303                             targetUnitPattern,
304                             composedTargetUnitPattern)) {
305                             Units.combinePattern(baseUnitPattern, prefixValue, lowercaseIfSpaced);
306                             int debug = 0;
307                         }
308                     }
309                 }
310             }
311         }
312     }
313 
314     /**
315      * Curated list of known exceptions. Usually because the short form of a unit is shorter when combined with a prefix or suffix
316      */
317     static final Map<String,String> ALLOW_ENGLISH_EXCEPTION = ImmutableMap.<String,String>builder()
318         .put("sq ft", "ft²")
319         .put("sq mi", "mi²")
320         .put("ft", "′")
321         .put("in", "″")
322         .put("MP", "Mpx")
323         .put("b", "bit")
324         .put("mb", "mbar")
325         .put("B", "byte")
326         .put("s", "sec")
327         .build();
allowEnglishException(String targetUnitPattern, String composedTargetUnitPattern)328     private boolean allowEnglishException(String targetUnitPattern, String composedTargetUnitPattern) {
329         for (Entry<String, String> entry : ALLOW_ENGLISH_EXCEPTION.entrySet()) {
330             String mod = targetUnitPattern.replace(entry.getKey(), entry.getValue());
331             if (mod.contentEquals(composedTargetUnitPattern)) {
332                 return true;
333             }
334         }
335         return false;
336     }
337 
338     // TODO Work this into a generating and then maintaining a data table for the units
339     /*
340         CLDRFile english = factory.make("en", false);
341         Set<String> prefixes = new TreeSet<>();
342         for (String path : english) {
343             XPathParts parts = XPathParts.getFrozenInstance(path);
344             String lastElement = parts.getElement(-1);
345             if (lastElement.equals("unitPrefixPattern") || lastElement.equals("compoundUnitPattern1")) {
346                 if (!parts.getAttributeValue(2, "type").equals("long")) {
347                     continue;
348                 }
349                 String value = english.getStringValue(path);
350                 prefixes.add(value.replace("{0}", "").trim());
351             }
352         }
353         Map<Status, Set<String>> unitValidity = Validity.getInstance().getStatusToCodes(LstrType.unit);
354         Multimap<String, String> from = LinkedHashMultimap.create();
355         for (String unit : unitValidity.get(Status.regular)) {
356             String[] parts = unit.split("[-]");
357             String main = parts[1];
358             for (String prefix : prefixes) {
359                 if (main.startsWith(prefix)) {
360                     if (main.length() == prefix.length()) { // square,...
361                         from.put(unit, main);
362                     } else { // milli
363                         from.put(unit, main.substring(0,prefix.length()));
364                         from.put(unit, main.substring(prefix.length()));
365                     }
366                     for (int i = 2; i < parts.length; ++i) {
367                         from.put(unit, parts[i]);
368                     }
369                 }
370             }
371         }
372         for (Entry<String, Collection<String>> set : from.asMap().entrySet()) {
373             System.out.println(set.getKey() + "\t" + CollectionUtilities.join(set.getValue(), "\t"));
374         }
375      */
assertEquals2(int TestERR, String title, String sqmeterPattern, String conSqmeterPattern)376     private boolean assertEquals2(int TestERR, String title, String sqmeterPattern, String conSqmeterPattern) {
377         if (!Objects.equals(sqmeterPattern, conSqmeterPattern)) {
378             msg(title + ", expected «" + sqmeterPattern + "», got «" + conSqmeterPattern + "»", TestERR, true, true);
379             return false;
380         } else if (isVerbose()) {
381             msg(title + ", expected «" + sqmeterPattern + "», got «" + conSqmeterPattern + "»", LOG, true, true);
382         }
383         return true;
384     }
385 
386     static final boolean DEBUG = false;
387 
TestConversion()388     public void TestConversion() {
389         String[][] tests = {
390             {"foot", "12", "inch"},
391             {"gallon", "4", "quart"},
392             {"gallon", "16", "cup"},
393         };
394         for (String[] test : tests) {
395             String sourceUnit = test[0];
396             Rational factor = Rational.of(test[1]);
397             String targetUnit = test[2];
398             final Rational convert = converter.convertDirect(Rational.ONE, sourceUnit, targetUnit);
399             assertEquals(sourceUnit + " to " + targetUnit, factor, convert);
400         }
401 
402         // test conversions are disjoint
403         Set<String> gotAlready = new HashSet<>();
404         List<Set<String>> equivClasses = new ArrayList<>();
405         Map<String,String> classToId = new TreeMap<>();
406         for (String unit : converter.canConvert()) {
407             if (gotAlready.contains(unit)) {
408                 continue;
409             }
410             Set<String> set = converter.canConvertBetween(unit);
411             final String id = "ID" + equivClasses.size();
412             equivClasses.add(set);
413             gotAlready.addAll(set);
414             for (String s : set) {
415                 classToId.put(s, id);
416             }
417         }
418 
419         // check not overlapping
420         // now handled by TestParseUnit, but we might revive a modified version of this.
421 //        for (int i = 0; i < equivClasses.size(); ++i) {
422 //            Set<String> eclass1 = equivClasses.get(i);
423 //            for (int j = i+1; j < equivClasses.size(); ++j) {
424 //                Set<String> eclass2 = equivClasses.get(j);
425 //                if (!Collections.disjoint(eclass1, eclass2)) {
426 //                    errln("Overlapping equivalence classes: " + eclass1 + " ~ " + eclass2 + "\n\tProbably bad chain requiring 3 steps.");
427 //                }
428 //            }
429 //
430 //            // check that all elements of an equivalence class have the same type
431 //            Multimap<String,String> breakdown = TreeMultimap.create();
432 //            for (String item : eclass1) {
433 //                String type = CORE_TO_TYPE.get(item);
434 //                if (type == null) {
435 //                    type = "?";
436 //                }
437 //                breakdown.put(type, item);
438 //            }
439 //            if (DEBUG) System.out.println("type to item: " + breakdown);
440 //            if (breakdown.keySet().size() != 1) {
441 //                errln("mixed categories: " + breakdown);
442 //            }
443 //
444 //        }
445 //
446 //        // check that all units with the same type have the same equivalence class
447 //        for (Entry<String, Collection<String>> entry : TYPE_TO_CORE.asMap().entrySet()) {
448 //            Multimap<String,String> breakdown = TreeMultimap.create();
449 //            for (String item : entry.getValue()) {
450 //                String id = classToId.get(item);
451 //                if (id == null) {
452 //                    continue;
453 //                }
454 //                breakdown.put(id, item);
455 //            }
456 //            if (DEBUG) System.out.println(entry.getKey() + " id to item: " + breakdown);
457 //            if (breakdown.keySet().size() != 1) {
458 //                errln(entry.getKey() + " mixed categories: " + breakdown);
459 //            }
460 //        }
461     }
462 
TestBaseUnits()463     public void TestBaseUnits() {
464         Splitter barSplitter = Splitter.on('-');
465         for (String unit : converter.baseUnits()) {
466             for (String piece : barSplitter.split(unit)) {
467                 assertTrue(unit + ": " + piece + " in " + UnitConverter.BASE_UNIT_PARTS, UnitConverter.BASE_UNIT_PARTS.contains(piece));
468             }
469         }
470     }
471 
TestUnitId()472     public void TestUnitId() {
473 
474         for (String simple : converter.getSimpleUnits()) {
475             String canonicalUnit = converter.getBaseUnit(simple);
476             UnitId unitId = converter.createUnitId(canonicalUnit);
477             String output = unitId.toString();
478             if (!assertEquals(simple + ": targets should be in canonical form",
479                 output, canonicalUnit)) {
480                 // for debugging
481                 converter.createUnitId(canonicalUnit);
482                 unitId.toString();
483             }
484         }
485         for (Entry<String, String> baseUnitToQuantity : BASE_UNIT_TO_QUANTITY.entrySet()) {
486             String baseUnit = baseUnitToQuantity.getKey();
487             String quantity = baseUnitToQuantity.getValue();
488             try {
489                 UnitId unitId = converter.createUnitId(baseUnit);
490                 String output = unitId.toString();
491                 if (!assertEquals(quantity + ": targets should be in canonical form",
492                     output, baseUnit)) {
493                     // for debugging
494                     converter.createUnitId(baseUnit);
495                     unitId.toString();
496                 }
497             } catch (Exception e) {
498                 errln("Can't convert baseUnit: " + baseUnit);
499             }
500         }
501 
502         for (String baseUnit : CORE_TO_TYPE.keySet()) {
503             try {
504                 UnitId unitId = converter.createUnitId(baseUnit);
505                 assertNotNull("Can't parse baseUnit: " + baseUnit, unitId);
506             } catch (Exception e) {
507                 converter.createUnitId(baseUnit); // for debugging
508                 errln("Can't parse baseUnit: " + baseUnit);
509             }
510         }
511 
512     }
513 
TestParseUnit()514     public void TestParseUnit() {
515         Output<String> compoundBaseUnit = new Output<>();
516         String[][] tests = {
517             {"kilometer-pound-per-hour", "kilogram-meter-per-second", "45359237/360000000"},
518             {"kilometer-per-hour", "meter-per-second", "5/18"},
519         };
520         for (String[] test : tests) {
521             String source = test[0];
522             String expectedUnit = test[1];
523             Rational expectedRational = new Rational.RationalParser().parse(test[2]);
524             ConversionInfo unitInfo = converter.parseUnitId(source, compoundBaseUnit, false);
525             assertEquals(source, expectedUnit, compoundBaseUnit.value);
526             assertEquals(source, expectedRational, unitInfo.factor);
527         }
528 
529         // check all
530         if (GENERATE_TESTS) System.out.println();
531         Set<String> badUnits = new LinkedHashSet<>();
532         Set<String> noQuantity = new LinkedHashSet<>();
533         Multimap<Pair<String,Double>, String> testPrintout = TreeMultimap.create();
534 
535         // checkUnitConvertability(converter, compoundBaseUnit, badUnits, "pint-metric-per-second");
536 
537         for (Entry<String, String> entry : TYPE_TO_CORE.entries()) {
538             String type = entry.getKey();
539             String unit = entry.getValue();
540             if (NOT_CONVERTABLE.contains(unit)) {
541                 continue;
542             }
543             checkUnitConvertability(converter, compoundBaseUnit, badUnits, noQuantity, type, unit, testPrintout);
544         }
545         if (GENERATE_TESTS) { // test data
546             System.out.println(
547                 "# Test data for unit conversions\n"
548                     + CldrUtility.getCopyrightString("#  ") + "\n"
549                     + "#\n"
550                     + "# Format:\n"
551                     + "#\tQuantity\t;\tx\t;\ty\t;\tconversion to y (rational)\t;\ttest: 1000 x ⟹ y\n"
552                     + "#\n"
553                     + "# Use: convert 1000 x units to the y unit; the result should match the final column,\n"
554                     + "#   at the given precision. For example, when the last column is 159.1549,\n"
555                     + "#   round to 4 decimal digits before comparing.\n"
556                     + "# Note that certain conversions are approximate, such as degrees to radians\n"
557                     + "#\n"
558                     + "# Generation: Set GENERATE_TESTS in TestUnits.java, and look at TestParseUnit results.\n"
559                 );
560             for (Entry<Pair<String, Double>, String> entry : testPrintout.entries()) {
561                 System.out.println(entry.getValue());
562             }
563         }
564         assertEquals("Unconvertable units", Collections.emptySet(), badUnits);
565         assertEquals("Units without Quantity", Collections.emptySet(), noQuantity);
566     }
567 
568     static final Set<String> NOT_CONVERTABLE = ImmutableSet.of("generic");
569 
checkUnitConvertability(UnitConverter converter, Output<String> compoundBaseUnit, Set<String> badUnits, Set<String> noQuantity, String type, String unit, Multimap<Pair<String, Double>, String> testPrintout)570     private void checkUnitConvertability(UnitConverter converter, Output<String> compoundBaseUnit,
571         Set<String> badUnits, Set<String> noQuantity, String type, String unit,
572         Multimap<Pair<String, Double>, String> testPrintout) {
573 
574         if (converter.isBaseUnit(unit)) {
575             String quantity = converter.getQuantityFromBaseUnit(unit);
576             if (quantity == null) {
577                 noQuantity.add(unit);
578             }
579             if (GENERATE_TESTS) {
580                 testPrintout.put(
581                     new Pair<>(quantity, 1000d),
582                     quantity
583                     + "\t;\t" + unit
584                     + "\t;\t" + unit
585                     + "\t;\t1 * x\t;\t1,000.00");
586             }
587         } else {
588             ConversionInfo unitInfo = converter.getUnitInfo(unit, compoundBaseUnit);
589             if (unitInfo == null) {
590                 unitInfo = converter.parseUnitId(unit, compoundBaseUnit, false);
591             }
592             if (unitInfo == null) {
593                 badUnits.add(unit);
594             } else if (GENERATE_TESTS){
595                 String quantity = converter.getQuantityFromBaseUnit(compoundBaseUnit.value);
596                 if (quantity == null) {
597                     noQuantity.add(compoundBaseUnit.value);
598                 }
599                 final double testValue = unitInfo.convert(R1000).toBigDecimal(MathContext.DECIMAL32).doubleValue();
600                 testPrintout.put(
601                     new Pair<>(quantity, testValue),
602                     quantity
603                     + "\t;\t" + unit
604                     + "\t;\t" + compoundBaseUnit
605                     + "\t;\t" + unitInfo
606                     + "\t;\t" + testValue
607 //                    + "\t" + unitInfo.factor.toBigDecimal(MathContext.DECIMAL32)
608 //                    + "\t" + unitInfo.factor.reciprocal().toBigDecimal(MathContext.DECIMAL32)
609                     );
610             }
611         }
612     }
613 
TestRational()614     public void TestRational() {
615         Rational a3_5 = Rational.of(3,5);
616 
617         Rational a6_10 = Rational.of(6,10);
618         assertEquals("", a3_5, a6_10);
619 
620         Rational a5_3 = Rational.of(5,3);
621         assertEquals("", a3_5, a5_3.reciprocal());
622 
623         assertEquals("", Rational.ONE, a3_5.multiply(a3_5.reciprocal()));
624         assertEquals("", Rational.ZERO, a3_5.add(a3_5.negate()));
625 
626         assertEquals("", Rational.INFINITY, Rational.ZERO.reciprocal());
627         assertEquals("", Rational.NEGATIVE_INFINITY, Rational.INFINITY.negate());
628         assertEquals("", Rational.NEGATIVE_ONE, Rational.ONE.negate());
629 
630         assertEquals("", Rational.NaN, Rational.ZERO.divide(Rational.ZERO));
631 
632         assertEquals("", BigDecimal.valueOf(2), Rational.of(2,1).toBigDecimal());
633         assertEquals("", BigDecimal.valueOf(0.5), Rational.of(1,2).toBigDecimal());
634 
635         assertEquals("", BigDecimal.valueOf(100), Rational.of(100,1).toBigDecimal());
636         assertEquals("", BigDecimal.valueOf(0.01), Rational.of(1,100).toBigDecimal());
637 
638         assertEquals("", Rational.of(12370,1), Rational.of(BigDecimal.valueOf(12370)));
639         assertEquals("", Rational.of(1237,10), Rational.of(BigDecimal.valueOf(1237.0/10)));
640         assertEquals("", Rational.of(1237,10000), Rational.of(BigDecimal.valueOf(1237.0/10000)));
641 
642         ConversionInfo uinfo = new ConversionInfo(Rational.of(2), Rational.of(3));
643         assertEquals("", Rational.of(3), uinfo.convert(Rational.ZERO));
644         assertEquals("", Rational.of(7), uinfo.convert(Rational.of(2)));
645     }
646 
TestRationalParse()647     public void TestRationalParse() {
648         Rational.RationalParser parser = SDI.getRationalParser();
649 
650         Rational a3_5 = Rational.of(3,5);
651 
652         assertEquals("", a3_5, parser.parse("6/10"));
653 
654         assertEquals("", a3_5, parser.parse("0.06/0.10"));
655 
656         assertEquals("", Rational.of(381, 1250), parser.parse("ft_to_m"));
657         assertEquals("", 6.02214076E+23d, parser.parse("6.02214076E+23").toBigDecimal().doubleValue());
658         Rational temp = parser.parse("gal_to_m3");
659         //System.out.println(" " + temp);
660         assertEquals("", 0.003785411784, temp.numerator.doubleValue()/temp.denominator.doubleValue());
661     }
662 
663 
664     static final Map<String,String> CORE_TO_TYPE;
665     static final Multimap<String,String> TYPE_TO_CORE;
666     static {
667         Set<String> VALID_UNITS = Validity.getInstance().getStatusToCodes(LstrType.unit).get(Status.regular);
668 
669         Map<String, String> coreToType = new TreeMap<>();
670         TreeMultimap<String, String> typeToCore = TreeMultimap.create();
671         for (String s : VALID_UNITS) {
672             int dashPos = s.indexOf('-');
673             String unitType = s.substring(0,dashPos);
674             String coreUnit = s.substring(dashPos+1);
675             coreUnit = converter.fixDenormalized(coreUnit);
coreToType.put(coreUnit, unitType)676             coreToType.put(coreUnit, unitType);
typeToCore.put(unitType, coreUnit)677             typeToCore.put(unitType, coreUnit);
678         }
679         CORE_TO_TYPE = ImmutableMap.copyOf(coreToType);
680         TYPE_TO_CORE = ImmutableMultimap.copyOf(typeToCore);
681     }
682 
TestUnitCategory()683     public void TestUnitCategory() {
684         if (SHOW_DATA) System.out.println();
685 
686         Map<String,Multimap<String,String>> bad = new TreeMap<>();
687         for (Entry<String, String> entry : TYPE_TO_CORE.entries()) {
688             final String coreUnit = entry.getValue();
689             final String unitType = entry.getKey();
690             if (coreUnit.equals("generic")) {
691                 continue;
692             }
693             String quantity = converter.getQuantityFromUnit(coreUnit, false);
694             if (SHOW_DATA) {
695                 System.out.format("%s\t%s\t%s\n", coreUnit, quantity, unitType);
696             }
697             if (quantity == null) {
698                 converter.getQuantityFromUnit(coreUnit, true);
699                 errln("Null quantity " + coreUnit);
700             } else if (!unitType.equals(quantity)) {
701                 switch (unitType) {
702                 case "concentr":
703                     switch (quantity) {
704                     case "portion": case "mass-density": case "concentration": case "substance-amount": case "concentration-mass": continue;
705                     }
706                     break;
707                 case "consumption":
708                     switch (quantity) {
709                     case "consumption-inverse": continue;
710                     }
711                     break;
712                 case "duration":
713                     switch (quantity) {
714                     case "year-duration": continue;
715                     }
716                     break;
717                 case "electric":
718                     switch (quantity) {
719                     case "electric-current": case "electric-resistance": case "voltage": continue;
720                     }
721                     break;
722                 case "graphics":
723                     switch (quantity) {
724                     case "resolution": case "typewidth": continue;
725                     }
726                     break;
727                 case "light":
728                     switch (quantity) {
729                     case "lumen": case "luminous-flux": case "power": case "luminous-intensity": case "luminance": case "illuminance": continue;
730                     }
731                     break;
732                 case "mass":
733                     switch (quantity) {
734                     case "energy": continue;
735                     }
736                     break;
737                 case "torque":
738                     switch (quantity) {
739                     case "energy": continue;
740                     }
741                     break;
742                 case "pressure":
743                     switch (quantity) {
744                     case "pressure-per-length": continue;
745                     }
746                     break;
747                 }
748                 Multimap<String, String> badMap = bad.get(unitType);
749                 if (badMap == null) {
750                     bad.put(unitType, badMap = TreeMultimap.create());
751                 }
752                 badMap.put(quantity, coreUnit);
753             }
754         }
755         for (Entry<String, Multimap<String, String>> entry : bad.entrySet()) {
756             assertNull("UnitType != quantity: " + entry.getKey(), '"' + Joiner.on("\", \"").join(entry.getValue().asMap().entrySet()) + '"');
757         }
758     }
759 
TestQuantities()760     public void TestQuantities() {
761         // put quantities in order
762         Multimap<String,String> quantityToBaseUnits = LinkedHashMultimap.create();
763 
764         Multimaps.invertFrom(Multimaps.forMap(BASE_UNIT_TO_QUANTITY), quantityToBaseUnits);
765 
766         for ( Entry<String, Collection<String>> entry : quantityToBaseUnits.asMap().entrySet()) {
767             assertEquals(entry.toString(), 1, entry.getValue().size());
768         }
769 
770         TreeMultimap<String, String> quantityToConvertible = TreeMultimap.create();
771         Set<String> missing = new TreeSet<>(CORE_TO_TYPE.keySet());
772         missing.removeAll(NOT_CONVERTABLE);
773 
774         for (Entry<String, String> entry : BASE_UNIT_TO_QUANTITY.entrySet()) {
775             String baseUnit = entry.getKey();
776             String quantity = entry.getValue();
777             Set<String> convertible = converter.canConvertBetween(baseUnit);
778             missing.removeAll(convertible);
779             quantityToConvertible.putAll(quantity, convertible);
780         }
781 
782         // handle missing
783         for (String missingUnit : ImmutableSet.copyOf(missing)) {
784             if (missingUnit.equals("mile-per-gallon")) {
785                 int debug = 0;
786             }
787             String quantity = converter.getQuantityFromUnit(missingUnit, false);
788             if (quantity != null) {
789                 quantityToConvertible.put(quantity, missingUnit);
790                 missing.remove(missingUnit);
791             } else {
792                 quantity = converter.getQuantityFromUnit(missingUnit, true); // for debugging
793             }
794         }
795         assertEquals("all units have quantity", Collections.emptySet(), missing);
796 
797         if (SHOW_DATA) {
798             System.out.println();
799             for (Entry<String, String> entry : BASE_UNIT_TO_QUANTITY.entrySet()) {
800                 String baseUnit = entry.getKey();
801                 String quantity = entry.getValue();
802                 System.out.println("        <unitQuantity"
803                     + " baseUnit='" + baseUnit + "'"
804                     + " quantity='" + quantity + "'"
805                     + "/>");
806             }
807             System.out.println();
808             System.out.println("Quantities");
809             for (Entry<String, Collection<String>> entry : quantityToConvertible.asMap().entrySet()) {
810                 String quantity = entry.getKey();
811                 Collection<String> convertible = entry.getValue();
812                 System.out.println(quantity + "\t" + convertible);
813             }
814         }
815     }
816 
817     static final UnicodeSet ALLOWED_IN_COMPONENT = new UnicodeSet("[a-z0-9]").freeze();
818     static final Set<String> STILL_RECOGNIZED_SIMPLES = ImmutableSet.of("em", "g-force", "therm-us");
819 
TestOrder()820     public void TestOrder() {
821         if (SHOW_DATA) System.out.println();
822         for (String s : UnitConverter.BASE_UNITS) {
823             String quantity = converter.getQuantityFromBaseUnit(s);
824             if (SHOW_DATA) {
825                 System.out.println("\"" + quantity + "\",");
826             }
827         }
828         for (String unit : CORE_TO_TYPE.keySet()) {
829             if (!STILL_RECOGNIZED_SIMPLES.contains(unit)) {
830                 for (String part : unit.split("-")) {
831                     assertTrue(unit + " has no parts < 2 in length", part.length() > 2);
832                     assertTrue(unit + " has only allowed characters", ALLOWED_IN_COMPONENT.containsAll(part));
833                 }
834             }
835             if (unit.equals("generic")) {
836                 continue;
837             }
838             String quantity = converter.getQuantityFromUnit(unit, false); // make sure doesn't crash
839         }
840     }
841 
TestConversionLineOrder()842     public void TestConversionLineOrder() {
843         Map<String, TargetInfo> data = converter.getInternalConversionData();
844         Multimap<TargetInfo, String> sorted = TreeMultimap.create(converter.targetInfoComparator,
845             Comparator.naturalOrder());
846         Multimaps.invertFrom(Multimaps.forMap(data), sorted);
847 
848         String lastBase = "";
849 
850         // Test that sorted is in same order as the file.
851         MapComparator<String> conversionOrder = new MapComparator<>(data.keySet());
852         String lastUnit = null;
853         for (Entry<TargetInfo, String> entry : sorted.entries()) {
854             final TargetInfo tInfo = entry.getKey();
855             final String unit = entry.getValue();
856             if (lastUnit != null) {
857                 if (!(conversionOrder.compare(lastUnit, unit) < 0)) {
858                     Output<String> metricUnit = new Output<>();
859                     ConversionInfo lastInfo = converter.parseUnitId(lastUnit, metricUnit, false);
860                     String lastMetric = metricUnit.value;
861                     ConversionInfo info = converter.parseUnitId(unit, metricUnit, false);
862                     String metric = metricUnit.value;
863                     if (metric.equals(lastMetric)) {
864                         warnln("Expected " + lastUnit + " < " + unit
865                             + "\t" + lastMetric + " " + lastInfo + " < " + metric + " " + info);
866                     }
867                 }
868             }
869             lastUnit = unit;
870             if (SHOW_DATA) {
871                 if (!lastBase.equals(tInfo.target)) {
872                     lastBase = tInfo.target;
873                     System.out.println("\n      <!-- " + converter.getQuantityFromBaseUnit(lastBase) + " -->");
874                 }
875                 //  <convertUnit source='week-person' target='second' factor='604800'/>
876                 System.out.println("        " + tInfo.formatOriginalSource(entry.getValue()));
877             }
878         }
879     }
880 
TestSimplify()881     public final void TestSimplify() {
882         Set<Rational> seen = new HashSet<>();
883         checkSimplify("ZERO", Rational.ZERO, seen);
884         checkSimplify("ONE", Rational.ONE, seen);
885         checkSimplify("NEGATIVE_ONE", Rational.NEGATIVE_ONE, seen);
886         checkSimplify("INFINITY", Rational.INFINITY, seen);
887         checkSimplify("NEGATIVE_INFINITY", Rational.NEGATIVE_INFINITY, seen);
888         checkSimplify("NaN", Rational.NaN, seen);
889 
890         checkSimplify("Simplify", Rational.of(25, 300), seen);
891         checkSimplify("Simplify", Rational.of(100, 1), seen);
892         checkSimplify("Simplify", Rational.of(2, 5), seen);
893         checkSimplify("Simplify", Rational.of(4, 25), seen);
894         checkSimplify("Simplify", Rational.of(5, 2), seen);
895         checkSimplify("Simplify", Rational.of(25, 4), seen);
896 
897         for (Entry<String, TargetInfo> entry : converter.getInternalConversionData().entrySet()) {
898             final Rational factor = entry.getValue().unitInfo.factor;
899             checkSimplify(entry.getKey(), factor, seen);
900             if (!factor.equals(Rational.ONE)) {
901                 checkSimplify(entry.getKey(), factor, seen);
902             }
903             final Rational offset = entry.getValue().unitInfo.offset;
904             if (!offset.equals(Rational.ZERO)) {
905                 checkSimplify(entry.getKey(), offset, seen);
906             }
907         }
908     }
909 
checkSimplify(String title, Rational expected, Set<Rational> seen)910     private void checkSimplify(String title, Rational expected, Set<Rational> seen) {
911         if (!seen.contains(expected)) {
912             seen.add(expected);
913             String simpleStr = expected.toString(FormatStyle.simple);
914             if (SHOW_DATA) System.out.println(title + ": " + expected + " => " + simpleStr);
915             Rational actual = RationalParser.BASIC.parse(simpleStr);
916             assertEquals("simplify", expected, actual);
917         }
918     }
919 
TestContinuationOrder()920     public void TestContinuationOrder() {
921         Continuation fluid = new Continuation(Arrays.asList("fluid"), "fluid-ounce");
922         Continuation fluid_imperial = new Continuation(Arrays.asList("fluid", "imperial"), "fluid-ounce-imperial");
923         final int fvfl = fluid.compareTo(fluid_imperial);
924         assertTrue(fluid + " vs " + fluid_imperial, fvfl > 0);
925         assertTrue(fluid_imperial + " vs " + fluid, fluid_imperial.compareTo(fluid) < 0);
926     }
927 
928     private static final Pattern usSystemPattern = Pattern.compile("\\b(lb_to_kg|ft_to_m|ft2_to_m2|ft3_to_m3|in3_to_m3|gal_to_m3|cup_to_m3)\\b");
929     private static final Pattern ukSystemPattern = Pattern.compile("\\b(lb_to_kg|ft_to_m|ft2_to_m2|ft3_to_m3|in3_to_m3|gal_imp_to_m3)\\b");
930 
931     static final Set<String> OK_BOTH = ImmutableSet.of(
932         "ounce-troy", "nautical-mile", "fahrenheit", "inch-ofhg",
933         "british-thermal-unit", "foodcalorie", "knot");
934 
935     static final Set<String> OK_US = ImmutableSet.of(
936         "therm-us", "bushel");
937     static final Set<String> NOT_US = ImmutableSet.of(
938         "stone");
939 
940     static final Set<String> OK_UK = ImmutableSet.of();
941     static final Set<String> NOT_UK = ImmutableSet.of(
942         "therm-us", "bushel", "barrel");
943 
944     public static final Set<String> OTHER_SYSTEM = ImmutableSet.of(
945         "g-force", "dalton", "calorie", "earth-radius",
946         "solar-radius", "solar-radius", "astronomical-unit", "light-year", "parsec", "earth-mass",
947         "solar-mass", "bit", "byte", "karat", "solar-luminosity", "ofhg", "atmosphere",
948         "pixel", "dot", "permillion", "permyriad", "permille", "percent", "karat", "portion",
949         "minute", "hour", "day", "day-person", "week", "week-person",
950         "year", "year-person", "decade", "month", "month-person", "century", "quarter",
951         "arc-second", "arc-minute", "degree", "radian", "revolution",
952         "electronvolt",
953         // quasi-metric
954         "dunam", "mile-scandinavian", "carat", "cup-metric", "pint-metric"
955         );
956 
TestSystems()957     public void TestSystems() {
958         final Logger logger = getLogger();
959 //        Map<String, TargetInfo> data = converter.getInternalConversionData();
960         Output<String> metricUnit = new Output<>();
961         Multimap<Set<UnitSystem>, R3<String, ConversionInfo, String>> systemsToUnits = TreeMultimap.create(Comparators.lexicographical(Ordering.natural()), Ordering.natural());
962         for (String longUnit : VALID_REGULAR_UNITS) {
963             String unit = Units.getShort(longUnit);
964             if (unit.equals("generic")) {
965                 continue;
966             }
967             if (unit.contentEquals("centiliter")) {
968                 int debug = 0;
969             }
970             Set<UnitSystem> systems = converter.getSystemsEnum(unit);
971             ConversionInfo parseInfo = converter.parseUnitId(unit, metricUnit, false);
972             String mUnit = metricUnit.value;
973             final R3<String, ConversionInfo, String> row = Row.of(mUnit, parseInfo, unit);
974             systemsToUnits.put(systems, row);
975 //            if (systems.isEmpty()) {
976 //                Rational factor = parseInfo.factor;
977 //                if (factor.isPowerOfTen()) {
978 //                    log("System should be 'metric': " + unit);
979 //                } else {
980 //                    log("System should be ???: " + unit);
981 //                }
982 //            }
983         }
984         String std = converter.getStandardUnit("kilogram-meter-per-square-meter-square-second");
985         logger.fine("");
986         Output<Rational> outFactor = new Output<>();
987         for (Entry<Set<UnitSystem>, Collection<R3<String, ConversionInfo, String>>> systemsAndUnits : systemsToUnits.asMap().entrySet()) {
988             Set<UnitSystem> systems = systemsAndUnits.getKey();
989             for (R3<String, ConversionInfo, String> unitInfo : systemsAndUnits.getValue()) {
990                 String unit = unitInfo.get2();
991                 switch (unit) {
992                 case "gram": continue;
993                 case "kilogram": break;
994                 default:
995                     String paredUnit = UnitConverter.stripPrefix(unit, outFactor);
996                     if (!paredUnit.equals(unit)) {
997                         continue;
998                     }
999                 }
1000                 final String metric = unitInfo.get0();
1001                 String standard = converter.getStandardUnit(metric);
1002                 final String quantity = converter.getQuantityFromUnit(unit, false);
1003                 final Rational factor = unitInfo.get1().factor;
1004                 // show non-metric relations
1005                 String specialRef = "";
1006                 String specialUnit = converter.getSpecialBaseUnit(quantity, systems);
1007                 if (specialUnit != null) {
1008                     Rational specialFactor = converter.convert(Rational.ONE, unit, specialUnit, false);
1009                     specialRef = "\t" + specialFactor + "\t" + specialUnit;
1010                 }
1011                 logger.fine(systems + "\t" + quantity
1012                     + "\t" + unit
1013                     + "\t" + factor
1014                     + "\t" + standard
1015                     + specialRef);
1016             }
1017         }
1018     }
1019 
TestTestFile()1020     public void TestTestFile() {
1021         File base = info.getCldrBaseDirectory();
1022         File testFile = new File(base, "common/testData/units/unitsTest.txt");
1023         Output<String> metricUnit = new Output<>();
1024         Stream<String> lines;
1025         try {
1026             lines = Files.lines(testFile.toPath());
1027         } catch (IOException e) {
1028             throw new ICUUncheckedIOException("Couldn't process " + testFile);
1029         }
1030         lines.forEach(line -> {
1031             // angle   ;   arc-second  ;   revolution  ;   1 / 1296000 * x ;   7.716049E-4
1032             line = line.trim();
1033             if (line.isEmpty() || line.charAt(0) == '#') {
1034                 return;
1035             }
1036             List<String> fields = SPLIT_SEMI.splitToList(line);
1037             ConversionInfo unitInfo;
1038             try {
1039                 unitInfo = converter.parseUnitId(fields.get(1), metricUnit, false);
1040             } catch (Exception e1) {
1041                 throw new IllegalArgumentException("Couldn't access fields on " + line);
1042             }
1043             if (unitInfo == null) {
1044                 throw new IllegalArgumentException("Couldn't get unitInfo on " + line);
1045             }
1046             double expected;
1047             try {
1048                 expected = Double.parseDouble(fields.get(4).replace(",", ""));
1049             } catch (NumberFormatException e) {
1050                 errln("Can't parse double in: " + line);
1051                 return;
1052             }
1053             double actual = unitInfo.convert(R1000).toBigDecimal(MathContext.DECIMAL32).doubleValue();
1054             assertEquals(Joiner.on(" ; ").join(fields), expected, actual);
1055         });
1056         lines.close();
1057     }
1058 
TestSpecialCases()1059     public void TestSpecialCases() {
1060         String [][] tests = {
1061             {"1", "millimole-per-liter", "milligram-ofglucose-per-deciliter", "18.01557"},
1062             {"1", "millimole-per-liter", "item-per-cubic-meter", "602214076000000000000000"},
1063 
1064             {"50",  "foot", "xxx", "0/0"},
1065             {"50",  "xxx", "mile", "0/0"},
1066             {"50",  "foot", "second", "0/0"},
1067             {"50",  "foot-per-xxx", "mile-per-hour", "0/0"},
1068             {"50",  "foot-per-minute", "mile", "0/0"},
1069             {"50",  "foot-per-ampere", "mile-per-hour", "0/0"},
1070 
1071             {"50",  "foot", "mile", "5 / 528"},
1072             {"50",  "foot-per-minute", "mile-per-hour", "25 / 44"},
1073             {"50",  "foot-per-minute", "hour-per-mile", "44 / 25"},
1074             {"50",  "mile-per-gallon", "liter-per-100-kilometer", "112903 / 24000"},
1075             {"50",  "celsius-per-second", "kelvin-per-second", "50"},
1076             {"50",  "celsius-per-second", "fahrenheit-per-second", "90"},
1077             {"50",  "pound-force", "kilogram-meter-per-square-second", "8896443230521 / 40000000000"},
1078             // Note: pound-foot-per-square-second is a pound-force divided by gravity
1079             {"50",  "pound-foot-per-square-second", "kilogram-meter-per-square-second", "17281869297 / 2500000000"},
1080         };
1081         int count = 0;
1082         for (String[] test : tests) {
1083             final Rational sourceValue = Rational.of(test[0]);
1084             final String sourceUnit = test[1];
1085             final String targetUnit = test[2];
1086             final Rational expectedValue = Rational.of(test[3]);
1087             final Rational conversion = converter.convert(sourceValue, sourceUnit, targetUnit, SHOW_DATA);
1088             if (!assertEquals(count++ + ") " + sourceValue + " " + sourceUnit + " ⟹ " + targetUnit, expectedValue, conversion)) {
1089                 converter.convert(sourceValue, sourceUnit, targetUnit, SHOW_DATA);
1090             }
1091         }
1092     }
1093 
1094     static Multimap<String,String> EXTRA_UNITS = ImmutableMultimap.<String,String>builder()
1095         .putAll("area", "square-foot", "square-yard", "square-mile")
1096         .putAll("volume", "cubic-inch", "cubic-foot", "cubic-yard")
1097         .build();
1098 
TestEnglishSystems()1099     public void TestEnglishSystems() {
1100         Multimap<String, String> systemToUnits = TreeMultimap.create();
1101         for (String unit : converter.canConvert()) {
1102             Set<String> systems = converter.getSystems(unit);
1103             if (systems.isEmpty()) {
1104                 systemToUnits.put("other", unit);
1105             } else for (String s : systems) {
1106                 systemToUnits.put(s, unit);
1107             }
1108         }
1109         for (Entry<String, Collection<String>> systemAndUnits : systemToUnits.asMap().entrySet()) {
1110             String system = systemAndUnits.getKey();
1111             final Collection<String> units = systemAndUnits.getValue();
1112             printSystemUnits(system, units);
1113         }
1114     }
1115 
printSystemUnits(String system, Collection<String> units)1116     private void printSystemUnits(String system, Collection<String> units) {
1117         Multimap<String,String> quantityToUnits = TreeMultimap.create();
1118         boolean metric = system.equals("metric");
1119         for (String unit : units) {
1120             quantityToUnits.put(converter.getQuantityFromUnit(unit, false), unit);
1121         }
1122         for (Entry<String, Collection<String>> entry : quantityToUnits.asMap().entrySet()) {
1123             String quantity = entry.getKey();
1124             String baseUnit = converter.getBaseUnitToQuantity().inverse().get(quantity);
1125             Multimap<Rational,String> sorted = TreeMultimap.create();
1126             sorted.put(Rational.ONE, baseUnit);
1127             if (!metric) {
1128                 String englishBaseUnit = getEnglishBaseUnit(baseUnit);
1129                 addUnit(baseUnit, englishBaseUnit, sorted);
1130                 Collection<String> extras = EXTRA_UNITS.get(quantity);
1131                 if (extras != null) {
1132                     for (String unit2 : extras) {
1133                         addUnit(baseUnit, unit2, sorted);
1134                     }
1135                 }
1136             }
1137             for (String unit : entry.getValue()) {
1138                 addUnit(baseUnit, unit, sorted);
1139             }
1140             Set<String> comparableUnits = ImmutableSet.copyOf(sorted.values());
1141 
1142             printUnits(system, quantity, comparableUnits);
1143         }
1144     }
1145 
addUnit(String baseUnit, String englishBaseUnit, Multimap<Rational, String> sorted)1146     private void addUnit(String baseUnit, String englishBaseUnit, Multimap<Rational, String> sorted) {
1147         Rational value = converter.convert(Rational.ONE, englishBaseUnit, baseUnit, false);
1148         sorted.put(value, englishBaseUnit);
1149     }
1150 
printUnits(String system, String quantity, Set<String> comparableUnits)1151     private void printUnits(String system, String quantity, Set<String> comparableUnits) {
1152         if (SHOW_DATA) System.out.print("\n"+ system + "\t" + quantity);
1153         for (String targetUnit : comparableUnits) {
1154             if (SHOW_DATA) System.out.print("\t" + targetUnit);
1155         }
1156         if (SHOW_DATA) System.out.println();
1157         for (String sourceUnit : comparableUnits) {
1158             if (SHOW_DATA) System.out.print("\t" + sourceUnit);
1159             for (String targetUnit : comparableUnits) {
1160                 Rational rational = converter.convert(Rational.ONE, sourceUnit, targetUnit, false);
1161                 if (SHOW_DATA) System.out.print("\t" + rational.toBigDecimal(MathContext.DECIMAL64).doubleValue());
1162             }
1163             if (SHOW_DATA) System.out.println();
1164         }
1165     }
1166 
getEnglishBaseUnit(String baseUnit)1167     private String getEnglishBaseUnit(String baseUnit) {
1168         return baseUnit.replace("kilogram", "pound").replace("meter", "foot");
1169     }
1170 
TestPI()1171     public void TestPI() {
1172         Rational PI = converter.getConstants().get("PI");
1173         double PID = PI.toBigDecimal(MathContext.DECIMAL128).doubleValue();
1174         final BigDecimal bigPi = new BigDecimal("3.141592653589793238462643383279502884197169399375105820974944");
1175         double bigPiD = bigPi.doubleValue();
1176         assertEquals("pi accurate enough", bigPiD, PID);
1177 
1178         // also test continued fractions used in deriving values
1179 
1180         Object[][] tests0 = {
1181             {new ContinuedFraction(0, 1, 5, 2, 2), Rational.of(27, 32), ImmutableList.of(Rational.of(0), Rational.of(1), Rational.of(5,6), Rational.of(11, 13))},
1182         };
1183         for (Object[] test : tests0) {
1184             ContinuedFraction source = (ContinuedFraction) test[0];
1185             Rational expected = (Rational) test[1];
1186             @SuppressWarnings("unchecked")
1187             List<Rational> expectedIntermediates = (List<Rational>) test[2];
1188             List<Rational> intermediates = new ArrayList<>();
1189             final Rational actual = source.toRational(intermediates);
1190             assertEquals("continued", expected, actual);
1191             assertEquals("continued", expectedIntermediates, intermediates);
1192         }
1193         Object[][] tests = {
1194             {Rational.of(3245,1000), new ContinuedFraction(3, 4, 12, 4)},
1195             {Rational.of(39,10), new ContinuedFraction(3, 1, 9)},
1196             {Rational.of(-3245,1000), new ContinuedFraction(-4, 1, 3, 12, 4)},
1197         };
1198         for (Object[] test : tests) {
1199             Rational source = (Rational) test[0];
1200             ContinuedFraction expected =(ContinuedFraction) test[1];
1201             ContinuedFraction actual = new ContinuedFraction(source);
1202             assertEquals(source.toString(), expected, actual);
1203             assertEquals(actual.toString(), source, actual.toRational(null));
1204         }
1205 
1206 
1207         if (SHOW_DATA) {
1208             ContinuedFraction actual = new ContinuedFraction(Rational.of(bigPi));
1209             List<Rational> intermediates = new ArrayList<>();
1210             actual.toRational(intermediates);
1211             System.out.println("\nRational\tdec64\tdec128\tgood enough");
1212             System.out.println("Target\t"
1213                 + bigPi.round(MathContext.DECIMAL64)+"x"
1214                 + "\t" + bigPi.round(MathContext.DECIMAL128)+"x"
1215                 + "\t" + "delta");
1216             int goodCount = 0;
1217             for (Rational item : intermediates) {
1218                 final BigDecimal dec64 = item.toBigDecimal(MathContext.DECIMAL64);
1219                 final BigDecimal dec128 = item.toBigDecimal(MathContext.DECIMAL128);
1220                 final boolean goodEnough = bigPiD == item.toBigDecimal(MathContext.DECIMAL128).doubleValue();
1221                 System.out.println(item
1222                     + "\t" + dec64
1223                     + "x\t" + dec128
1224                     + "x\t" + goodEnough
1225                     + "\t" + item.toBigDecimal(MathContext.DECIMAL128).subtract(bigPi));
1226                 if (goodEnough && goodCount++ > 6) {
1227                     break;
1228                 }
1229             }
1230         }
1231     }
TestUnitPreferenceSource()1232     public void TestUnitPreferenceSource() {
1233         XMLSource xmlSource = new SimpleXMLSource("units");
1234         xmlSource.setNonInheriting(true);
1235         CLDRFile foo = new CLDRFile(xmlSource );
1236         foo.setDtdType(DtdType.supplementalData);
1237         UnitPreferences uprefs = new UnitPreferences();
1238         int order = 0;
1239         for (String line : FileUtilities.in(TestUnits.class, "UnitPreferenceSource.txt")) {
1240             line = line.trim();
1241             if (line.isEmpty() || line.startsWith("#")) {
1242                 continue;
1243             }
1244             List<String> items = SPLIT_SEMI.splitToList(line);
1245             try {
1246                 String quantity = items.get(0);
1247                 String usage = items.get(1);
1248                 String regionsStr = items.get(2);
1249                 List<String> regions = SPLIT_SPACE.splitToList(items.get(2));
1250                 String geqStr = items.get(3);
1251                 Rational geq = geqStr.isEmpty() ? Rational.ONE : Rational.of(geqStr);
1252                 String skeleton = items.get(4);
1253                 String unit = items.get(5);
1254                 uprefs.add(quantity, usage, regionsStr, geqStr, skeleton, unit);
1255                 String path = uprefs.getPath(order++, quantity, usage, regions, geq, skeleton);
1256                 xmlSource.putValueAtPath(path, unit);
1257             } catch (Exception e) {
1258                 errln("Failure on line: " + line + "; " + e.getMessage());
1259             }
1260         }
1261         if (SHOW_DATA) {
1262             PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
1263             foo.write(out);
1264             out.flush();
1265         } else {
1266             warnln("Use  -DTestUnits:SHOW_DATA to get the reformatted source");
1267         }
1268     }
1269 
1270     static final Joiner JOIN_SPACE = Joiner.on(' ');
1271 
checkUnitPreferences(UnitPreferences uprefs)1272     private void checkUnitPreferences(UnitPreferences uprefs) {
1273         Set<String> usages = new LinkedHashSet<>();
1274         for (Entry<String, Map<String, Multimap<Set<String>, UnitPreference>>> entry1 : uprefs.getData().entrySet()) {
1275             String quantity = entry1.getKey();
1276 
1277             // Each of the quantities is valid.
1278             assertNotNull("quantity is convertible", converter.getBaseUnitFromQuantity(quantity));
1279 
1280             Map<String, Multimap<Set<String>, UnitPreference>> usageToRegionToUnitPreference = entry1.getValue();
1281 
1282             // each of the quantities has a default usage
1283             assertTrue("Quantity " + quantity + " contains default usage", usageToRegionToUnitPreference.containsKey("default"));
1284 
1285             for (Entry<String, Multimap<Set<String>, UnitPreference>> entry2 : usageToRegionToUnitPreference.entrySet()) {
1286                 String usage = entry2.getKey();
1287                 final String quantityPlusUsage = quantity + "/" + usage;
1288                 Multimap<Set<String>, UnitPreference> regionsToUnitPreference = entry2.getValue();
1289                 usages.add(usage);
1290                 Set<Set<String>> regionSets = regionsToUnitPreference.keySet();
1291 
1292                 // all quantity + usage pairs must contain 001 (one exception)
1293                 assertTrue("For " + quantityPlusUsage + ", the set of sets of regions must contain 001", regionSets.contains(WORLD_SET)
1294                     || quantityPlusUsage.contentEquals("concentration/blood-glucose"));
1295 
1296                 // Check that regions don't overlap for same quantity/usage
1297                 Multimap<String, Set<String>> checkOverlap = LinkedHashMultimap.create();
1298                 for (Set<String> regionSet : regionsToUnitPreference.keySet()) {
1299                     for (String region : regionSet) {
1300                         checkOverlap.put(region, regionSet);
1301                     }
1302                 }
1303                 for (Entry<String, Collection<Set<String>>> entry : checkOverlap.asMap().entrySet()) {
1304                     assertEquals(quantityPlusUsage + ": regions must be in only one set: " + entry.getValue(), 1, entry.getValue().size());
1305                 }
1306 
1307                 Set<String> systems = new TreeSet<>();
1308                 for (Entry<Set<String>, Collection<UnitPreference>> entry : regionsToUnitPreference.asMap().entrySet()) {
1309                     Collection<UnitPreference> uPrefs = entry.getValue();
1310                     Set<String> regions = entry.getKey();
1311 
1312 
1313                     // reset these for every new set of regions
1314                     Rational lastSize = null;
1315                     String lastUnit = null;
1316                     Rational lastgeq = null;
1317                     systems.clear();
1318                     Set<String> lastRegions = null;
1319                     String unitQuantity = null;
1320 
1321                     preferences:
1322                         for (UnitPreference up : uPrefs) {
1323                             String topUnit = null;
1324                             if ("minute:second".equals(up.unit)) {
1325                                 int debug = 0;
1326                             }
1327                             String lastQuantity = null;
1328                             Rational lastValue = null;
1329                             Rational geq = converter.parseRational(String.valueOf(up.geq));
1330 
1331                             // where we have an 'and' unit, get its information
1332                             for (String unit : SPLIT_AND.split(up.unit)) {
1333                                 try {
1334                                     if (topUnit == null) {
1335                                         topUnit = unit;
1336                                     }
1337                                     unitQuantity = converter.getQuantityFromUnit(unit, false);
1338                                 } catch (Exception e) {
1339                                     errln("Unit is not covertible: " + up.unit);
1340                                     continue preferences;
1341                                 }
1342                                 String baseUnit = converter.getBaseUnitFromQuantity(unitQuantity);
1343                                 if (geq.compareTo(Rational.ZERO) < 0) {
1344                                     throw new IllegalArgumentException("geq must be > 0" + geq);
1345                                 }
1346                                 Rational value = converter.convert(Rational.ONE, unit, baseUnit, false);
1347                                 if (lastQuantity != null) {
1348                                     int diff = value.compareTo(lastValue);
1349                                     if (diff >= 0) {
1350                                         throw new IllegalArgumentException("Bad mixed unit; biggest unit must be first: " + up.unit);
1351                                     }
1352                                     if (!lastQuantity.contentEquals(quantity)) {
1353                                         throw new IllegalArgumentException("Inconsistent quantities for mixed unit: " + up.unit);
1354                                     }
1355                                 }
1356                                 lastValue = value;
1357                                 lastQuantity = quantity;
1358                                 systems.addAll(converter.getSystems(unit));
1359                             }
1360                             String baseUnit = converter.getBaseUnitFromQuantity(unitQuantity);
1361                             Rational size = converter.convert(up.geq, topUnit, baseUnit, false);
1362                             if (lastSize != null) { // ensure descending order
1363                                 if (!assertTrue("Successive items must be ≥ previous:\n\t" + quantityPlusUsage
1364                                     + "; unit: " + up.unit
1365                                     + "; size: " + size
1366                                     + "; regions: " + regions
1367                                     + "; lastUnit: " + lastUnit
1368                                     + "; lastSize: " + lastSize
1369                                     + "; lastRegions: " + lastRegions
1370                                     , size.compareTo(lastSize) <= 0)) {
1371                                     int debug = 0;
1372                                 }
1373                             }
1374                             lastSize = size;
1375                             lastUnit = up.unit;
1376                             lastgeq = geq;
1377                             lastRegions = regions;
1378                             if (SHOW_DATA) System.out.println(quantity + "\t" + usage + "\t" + regions + "\t" + up.geq + "\t" + up.unit + "\t" + up.skeleton);
1379                         }
1380                     // Check that last geq is ONE.
1381                     assertEquals(usage + " + " + regions + ": the least unit must have geq=1 (or equivalently, no geq)", Rational.ONE, lastgeq);
1382 
1383                     // Check that each set has a consistent system.
1384                     assertTrue(usage + " + " + regions + " has mixed systems: " + systems + "\n\t" + uPrefs, areConsistent(systems, unitQuantity));
1385                 }
1386             }
1387         }
1388     }
1389 
areConsistent(Set<String> systems, String unitQuantity)1390     private boolean areConsistent(Set<String> systems, String unitQuantity) {
1391         return unitQuantity.equals("duration")
1392             || !(systems.contains("metric") && (systems.contains("ussystem") || systems.contains("uksystem")));
1393     }
1394 
TestBcp47()1395     public void TestBcp47() {
1396         checkBcp47("Quantity", converter.getQuantities(), lowercaseAZ, false);
1397         checkBcp47("Usage", SDI.getUnitPreferences().getUsages(), lowercaseAZ09, true);
1398         checkBcp47("Unit", converter.getSimpleUnits(), lowercaseAZ09, true);
1399     }
1400 
checkBcp47(String identifierType, Set<String> identifiers, UnicodeSet allowed, boolean allowHyphens)1401     private void checkBcp47(String identifierType, Set<String> identifiers, UnicodeSet allowed, boolean allowHyphens) {
1402         Output<Integer> counter = new Output<>(0);
1403         Multimap<String,String> truncatedToFullIdentifier = TreeMultimap.create();
1404         final Set<String> simpleUnits = identifiers;
1405         for (String unit : simpleUnits) {
1406             if (!allowHyphens && unit.contains("-")) {
1407                 truncatedToFullIdentifier.put(unit, "-");
1408             }
1409             checkBcp47(counter, identifierType, unit, allowed, truncatedToFullIdentifier);
1410         }
1411         for (Entry<String, Collection<String>> entry : truncatedToFullIdentifier.asMap().entrySet()) {
1412             Set<String> identifierSet = ImmutableSet.copyOf(entry.getValue());
1413             assertEquals(identifierType + ": truncated identifier " + entry.getKey() + " must be unique", ImmutableSet.of(identifierSet.iterator().next()), identifierSet);
1414         }
1415     }
1416 
1417     private static int MIN_SUBTAG_LENGTH = 3;
1418     private static int MAX_SUBTAG_LENGTH = 8;
1419 
1420     static final UnicodeSet lowercaseAZ = new UnicodeSet("[a-z]").freeze();
1421     static final UnicodeSet lowercaseAZ09 = new UnicodeSet("[a-z0-9]").freeze();
1422 
checkBcp47(Output<Integer> counter, String title, String identifier, UnicodeSet allowed, Multimap<String,String> truncatedToFullIdentifier)1423     private void checkBcp47(Output<Integer> counter, String title, String identifier, UnicodeSet allowed, Multimap<String,String> truncatedToFullIdentifier) {
1424         StringBuilder shortIdentifer = new StringBuilder();
1425         boolean fail = false;
1426         for (String subtag : identifier.split("-")) {
1427             assertTrue(++counter.value + ") " + title + " identifier=" + identifier + " subtag=" + subtag + " has right characters", allowed.containsAll(subtag));
1428             if (!(subtag.length() >= MIN_SUBTAG_LENGTH && subtag.length() <= MAX_SUBTAG_LENGTH)) {
1429                 for (Entry<String, Rational> entry : UnitConverter.PREFIXES.entrySet()) {
1430                     String prefix = entry.getKey();
1431                     if (subtag.startsWith(prefix)) {
1432                         subtag = subtag.substring(prefix.length());
1433                         break;
1434                     }
1435                 }
1436             }
1437             if (shortIdentifer.length() != 0) {
1438                 shortIdentifer.append('-');
1439             }
1440             if (subtag.length() > MAX_SUBTAG_LENGTH) {
1441                 shortIdentifer.append(subtag.substring(0, MAX_SUBTAG_LENGTH));
1442                 fail = true;
1443             } else {
1444                 shortIdentifer.append(subtag);
1445             }
1446         }
1447         if (fail) {
1448             String shortIdentiferStr = shortIdentifer.toString();
1449             truncatedToFullIdentifier.put(shortIdentiferStr, identifier);
1450         }
1451     }
1452 
TestUnitPreferences()1453     public void TestUnitPreferences() {
1454         warnln("If this fails, check the output of TestUnitPreferencesSource (with -DTestUnits:SHOW_DATA), fix as needed, then incorporate.");
1455         UnitPreferences prefs = SDI.getUnitPreferences();
1456         checkUnitPreferences(prefs);
1457 //        Map<String, Map<String, Map<String, UnitPreference>>> fastMap = prefs.getFastMap(converter);
1458 //        for (Entry<String, Map<String, Map<String, UnitPreference>>> entry : fastMap.entrySet()) {
1459 //            String quantity = entry.getKey();
1460 //            String baseUnit = converter.getBaseUnitFromQuantity(quantity);
1461 //            for (Entry<String, Map<String, UnitPreference>> entry2 : entry.getValue().entrySet()) {
1462 //                String usage = entry2.getKey();
1463 //                for (Entry<String, UnitPreference> entry3 : entry2.getValue().entrySet()) {
1464 //                    String region = entry3.getKey();
1465 //                    UnitPreference pref = entry3.getValue();
1466 //                    System.out.println(quantity + "\t" + usage + "\t" + region + "\t" + pref.toString(baseUnit));
1467 //                }
1468 //            }
1469 //        }
1470         prefs.getFastMap(converter); // call just to make sure we don't get an exception
1471 
1472         if (GENERATE_TESTS) {
1473             System.out.println(
1474                 "\n# Test data for unit preferences\n"
1475                     + CldrUtility.getCopyrightString("#  ") + "\n"
1476                     + "#\n"
1477                     + "# Format:\n"
1478                     + "#\tQuantity;\tUsage;\tRegion;\tInput (r);\tInput (d);\tInput Unit;\tOutput (r);\tOutput (d);\tOutput Unit\n"
1479                     + "#\n"
1480                     + "# Use: Convert the Input amount & unit according to the Usage and Region.\n"
1481                     + "#\t The result should match the Output amount and unit.\n"
1482                     + "#\t Both rational (r) and double64 (d) forms of the input and output amounts are supplied so that implementations\n"
1483                     + "#\t have two options for testing based on the precision in their implementations. For example:\n"
1484                     + "#\t   3429 / 12500; 0.27432; meter;\n"
1485                     + "#\t The Output amount and Unit are repeated for mixed units. In such a case, only the smallest unit will have\n"
1486                     + "#\t both a rational and decimal amount; the others will have a single integer value, such as:\n"
1487                     + "#\t   length; person-height; CA; 3429 / 12500; 0.27432; meter; 2; foot; 54 / 5; 10.8; inch\n"
1488                     + "#\t The input and output units are unit identifers; in particular, the output does not have further processing:\n"
1489                     + "#\t\t • no localization\n"
1490                     + "#\t\t • no adjustment for pluralization\n"
1491                     + "#\t\t • no formatted with the skeleton\n"
1492                     + "#\t\t • no suppression of zero values (for secondary -and- units such as pound in stone-and-pound)\n"
1493                     + "#\n"
1494                     + "# Generation: Set GENERATE_TESTS in TestUnits.java, and look at TestUnitPreferences results.\n"
1495                 );
1496             Rational ONE_TENTH = Rational.of(1,10);
1497 
1498             // Note that for production usage, precomputed data like the prefs.getFastMap(converter) would be used instead of the raw data.
1499 
1500             for (Entry<String, Map<String, Multimap<Set<String>, UnitPreference>>> entry : prefs.getData().entrySet()) {
1501                 String quantity = entry.getKey();
1502                 String baseUnit = converter.getBaseUnitFromQuantity(quantity);
1503                 for (Entry<String, Multimap<Set<String>, UnitPreference>> entry2 : entry.getValue().entrySet()) {
1504                     String usage = entry2.getKey();
1505 
1506                     // collect samples of base units
1507                     for (Entry<Set<String>, Collection<UnitPreference>> entry3 : entry2.getValue().asMap().entrySet()) {
1508                         boolean first = true;
1509                         Set<Rational> samples = new TreeSet<>(Comparator.reverseOrder());
1510                         for (UnitPreference pref : entry3.getValue()) {
1511                             final String topUnit = UnitPreferences.SPLIT_AND.split(pref.unit).iterator().next();
1512                             if (first) {
1513                                 samples.add(converter.convert(pref.geq.add(ONE_TENTH), topUnit, baseUnit, false));
1514                                 first = false;
1515                             }
1516                             samples.add(converter.convert(pref.geq, topUnit, baseUnit, false));
1517                             samples.add(converter.convert(pref.geq.subtract(ONE_TENTH), topUnit, baseUnit, false));
1518                         }
1519                         // show samples
1520                         Set<String> regions = entry3.getKey();
1521                         String sampleRegion = regions.iterator().next();
1522                         Collection<UnitPreference> uprefs = entry3.getValue();
1523                         for (Rational sample : samples) {
1524                             showSample(quantity, usage, sampleRegion, sample, baseUnit, uprefs);
1525                         }
1526                         System.out.println();
1527                     }
1528                 }
1529             }
1530         }
1531     }
1532 
showSample(String quantity, String usage, String sampleRegion, Rational sampleBaseValue, String baseUnit, Collection<UnitPreference> prefs)1533     private void showSample(String quantity, String usage, String sampleRegion, Rational sampleBaseValue, String baseUnit, Collection<UnitPreference> prefs) {
1534         String lastUnit = null;
1535         boolean gotOne = false;
1536         for (UnitPreference pref : prefs) {
1537             final String topUnit = UnitPreferences.SPLIT_AND.split(pref.unit).iterator().next();
1538             Rational baseGeq = converter.convert(pref.geq, topUnit, baseUnit, false);
1539             if (sampleBaseValue.compareTo(baseGeq) >= 0) {
1540                 showSample2(quantity, usage, sampleRegion, sampleBaseValue, baseUnit, pref.unit);
1541                 gotOne = true;
1542                 break;
1543             }
1544             lastUnit = pref.unit;
1545         }
1546         if (!gotOne) {
1547             showSample2(quantity, usage, sampleRegion, sampleBaseValue, baseUnit, lastUnit);
1548         }
1549     }
1550 
showSample2(String quantity, String usage, String sampleRegion, Rational sampleBaseValue, String baseUnit, String lastUnit)1551     private void showSample2(String quantity, String usage, String sampleRegion, Rational sampleBaseValue, String baseUnit, String lastUnit) {
1552         Rational originalSampleBaseValue = sampleBaseValue;
1553         // Known slow algorithm for mixed values, but for generating tests we don't care.
1554         final List<String> units = UnitPreferences.SPLIT_AND.splitToList(lastUnit);
1555         StringBuilder formattedUnit = new StringBuilder();
1556         int remaining = units.size();
1557         for (String unit : units) {
1558             --remaining;
1559             Rational sample = converter.convert(sampleBaseValue, baseUnit, unit, false);
1560             if (formattedUnit.length() != 0) {
1561                 formattedUnit.append(TEST_SEP);
1562             }
1563             if (remaining != 0) {
1564                 BigInteger floor = sample.floor();
1565                 formattedUnit.append(floor + TEST_SEP + unit);
1566                 // convert back to base unit
1567                 sampleBaseValue = converter.convert(sample.subtract(Rational.of(floor)), unit, baseUnit, false);
1568             } else {
1569                 formattedUnit.append(sample + TEST_SEP + sample.doubleValue() + TEST_SEP + unit);
1570             }
1571         }
1572         System.out.println(quantity + TEST_SEP + usage + TEST_SEP + sampleRegion
1573             + TEST_SEP + originalSampleBaseValue + TEST_SEP + originalSampleBaseValue.doubleValue() + TEST_SEP + baseUnit
1574             + TEST_SEP + formattedUnit);
1575     }
1576 
TestWithExternalData()1577     public void TestWithExternalData() throws IOException {
1578 
1579         Multimap<String, ExternalUnitConversionData> seen = HashMultimap.create();
1580         Set<ExternalUnitConversionData> cantConvert = new LinkedHashSet<>();
1581         Map<ExternalUnitConversionData, Rational> convertDiff = new LinkedHashMap<>();
1582         Set<String> remainingCldrUnits = new LinkedHashSet<>(converter.getInternalConversionData().keySet());
1583         Set<ExternalUnitConversionData> couldAdd = new LinkedHashSet<>();
1584 
1585         if (SHOW_DATA) {
1586             System.out.println();
1587         }
1588         for (ExternalUnitConversionData data : NistUnits.externalConversionData) {
1589             Rational externalResult = data.info.convert(Rational.ONE);
1590             Rational cldrResult = converter.convert(Rational.ONE, data.source, data.target, false);
1591             seen.put(data.source + "⟹" + data.target, data);
1592 
1593             if (externalResult.isPowerOfTen()) {
1594                 couldAdd.add(data);
1595             }
1596 
1597             if (cldrResult.equals(Rational.NaN)) {
1598                 cantConvert.add(data);
1599             } else {
1600                 final Rational symmetricDiff = externalResult.symmetricDiff(cldrResult);
1601                 if (symmetricDiff.abs().compareTo(Rational.of(1, 1000000)) > 0){
1602                     convertDiff.put(data, cldrResult);
1603                 } else {
1604                     remainingCldrUnits.remove(data.source);
1605                     remainingCldrUnits.remove(data.target);
1606                     if (SHOW_DATA) System.out.println("*Converted"
1607                         + "\t" + cldrResult.doubleValue()
1608                         + "\t" + externalResult.doubleValue()
1609                         + "\t" + symmetricDiff.doubleValue()
1610                         + "\t" + data);
1611                 }
1612             }
1613         }
1614 
1615         // get additional data on derived units
1616 //        for (Entry<String, TargetInfo> e : NistUnits.derivedUnitToConversion.entrySet()) {
1617 //            String sourceUnit = e.getKey();
1618 //            TargetInfo targetInfo = e.getValue();
1619 //
1620 //            Rational conversion = converter.convert(Rational.ONE, sourceUnit, targetInfo.target, false);
1621 //            if (conversion.equals(Rational.NaN)) {
1622 //                couldAdd.add(new ExternalUnitConversionData("", sourceUnit, targetInfo.target, conversion, "?", null));
1623 //            }
1624 //        }
1625         if (SHOW_DATA) {
1626             for (Entry<String, Collection<String>> e : NistUnits.unitToQuantity.asMap().entrySet()) {
1627                 System.out.println("*Quantities:"  + "\t" +  e.getKey()  + "\t" +  e.getValue());
1628             }
1629         }
1630 
1631         // check for missing external data
1632         int unitsWithoutExternalCheck = 0;
1633         for (String remainingUnit : remainingCldrUnits) {
1634             final TargetInfo targetInfo = converter.getInternalConversionData().get(remainingUnit);
1635             if (!targetInfo.target.contentEquals(remainingUnit)) {
1636                 if (SHOW_DATA) {
1637                     printlnIfZero(unitsWithoutExternalCheck);
1638                     System.out.println("Not tested against external data\t" + remainingUnit + "\t" + targetInfo);
1639                 }
1640                 unitsWithoutExternalCheck++;
1641             }
1642         }
1643         if (unitsWithoutExternalCheck != 0 && !SHOW_DATA) {
1644             warnln(unitsWithoutExternalCheck + " units without external data verification.  Use -DTestUnits:SHOW_DATA for details.");
1645         }
1646 
1647         boolean showDiagnostics = false;
1648         for (Entry<String, Collection<ExternalUnitConversionData>> entry : seen.asMap().entrySet()) {
1649             if (entry.getValue().size() != 1) {
1650                 Multimap<ConversionInfo, ExternalUnitConversionData> factors = HashMultimap.create();
1651                 for (ExternalUnitConversionData s : entry.getValue()) {
1652                     factors.put(s.info, s);
1653                 }
1654                 if (factors.keySet().size() > 1) {
1655                     for (ExternalUnitConversionData s : entry.getValue()) {
1656                         errln("*DUP-" + s);
1657                         showDiagnostics = true;
1658                     }
1659                 }
1660             }
1661         }
1662 
1663         if (convertDiff.size() > 0) {
1664             for (Entry<ExternalUnitConversionData, Rational> e : convertDiff.entrySet()) {
1665                 final Rational computed = e.getValue();
1666                 final ExternalUnitConversionData external = e.getKey();
1667                 Rational externalResult = external.info.convert(Rational.ONE);
1668                 showDiagnostics = true;
1669                 // for debugging
1670                 converter.convert(Rational.ONE, external.source, external.target, true);
1671 
1672                 errln("*DIFF CONVERT:"
1673                     + "\t" + external.source
1674                     + "\t⟹\t" + external.target
1675                     + "\texpected\t" + externalResult.doubleValue()
1676                     + "\tactual:\t" + computed.doubleValue()
1677                     + "\tsdiff:\t" + computed.symmetricDiff(externalResult).abs().doubleValue()
1678                     + "\txdata:\t" + external);
1679             }
1680         }
1681 
1682         // temporary: show the items that didn't covert correctly
1683         if (showDiagnostics) {
1684             System.out.println();
1685             Rational x = showDelta("pound-fahrenheit", "gram-celsius", false);
1686             Rational y = showDelta("calorie", "joule", false);
1687             showDelta("product\t", x.multiply(y));
1688             showDelta("british-thermal-unit", "calorie", false);
1689             showDelta("inch-ofhg", "pascal", false);
1690             showDelta("millimeter-ofhg", "pascal", false);
1691             showDelta("ofhg", "kilogram-per-square-meter-square-second", false);
1692             showDelta("13595.1*gravity", Rational.of("9.80665*13595.1"));
1693 
1694             showDelta("fahrenheit-hour-square-foot-per-british-thermal-unit-inch", "meter-kelvin-per-watt", true);
1695         }
1696 
1697         if (showDiagnostics && NistUnits.skipping.size() > 0) {
1698             System.out.println();
1699             for (String s : NistUnits.skipping) {
1700                 System.out.println("*SKIPPING " + s);
1701             }
1702         }
1703         if (showDiagnostics && NistUnits.idChanges.size() > 0) {
1704             System.out.println();
1705             for (Entry<String, Collection<String>> e : NistUnits.idChanges.asMap().entrySet()) {
1706                 if (SHOW_DATA) System.out.println("*CHANGES\t" + e.getKey() + "\t" + Joiner.on('\t').join(e.getValue()));
1707             }
1708         }
1709 
1710         if (showDiagnostics && cantConvert.size() > 0) {
1711             System.out.println();
1712             for (ExternalUnitConversionData e : cantConvert) {
1713                 System.out.println("*CANT CONVERT-" + e);
1714             }
1715         }
1716         Output<String> baseUnit = new Output<>();
1717         for (ExternalUnitConversionData s : couldAdd) {
1718             String target = s.target;
1719             Rational endFactor = s.info.factor;
1720             String mark = "";
1721             TargetInfo baseUnit2 = NistUnits.derivedUnitToConversion.get(s.target);
1722             if (baseUnit2 != null) {
1723                 target = baseUnit2.target;
1724                 endFactor = baseUnit2.unitInfo.factor;
1725                 mark="¹";
1726             } else {
1727                 ConversionInfo conversionInfo = converter.getUnitInfo(s.target, baseUnit);
1728                 if (conversionInfo != null && !s.target.equals(baseUnit.value)) {
1729                     target = baseUnit.value;
1730                     endFactor = conversionInfo.convert(s.info.factor);
1731                     mark="²";
1732                 }
1733             }
1734             if (SHOW_DATA) System.out.println("Could add 10^X conversion from a"
1735                 + "\t" +  s.source
1736                 + "\tto" + mark
1737                 + "\t" + endFactor.toString(FormatStyle.simple)
1738                 + "\t" + target);
1739         }
1740         if (!SHOW_DATA) warnln("Use -DTestUnits:SHOW_DATA to show units we could add from NIST.");
1741     }
1742 
showDelta(String firstUnit, String secondUnit, boolean showYourWork)1743     private Rational showDelta(String firstUnit, String secondUnit, boolean showYourWork) {
1744         Rational x = converter.convert(Rational.ONE, firstUnit, secondUnit, showYourWork);
1745         return showDelta(firstUnit + "\t" + secondUnit, x);
1746     }
1747 
showDelta(final String title, Rational rational)1748     private Rational showDelta(final String title, Rational rational) {
1749         System.out.print("*CONST\t" + title);
1750         System.out.print("\t" + rational.toString(FormatStyle.simple));
1751         System.out.println("\t" + rational.doubleValue());
1752         return rational;
1753     }
1754 
TestRepeating()1755     public void TestRepeating() {
1756         Set<Rational> seen = new HashSet<>();
1757         String[][] tests = {
1758             {"0/0", "NaN"},
1759             {"1/0", "INF"},
1760             {"-1/0", "-INF"},
1761             {"0/1", "0"},
1762             {"1/1", "1"},
1763             {"1/2", "0.5"},
1764             {"1/3", "0.˙3"},
1765             {"1/4", "0.25"},
1766             {"1/5", "0.2"},
1767             {"1/6", "0.1˙6"},
1768             {"1/7", "0.˙142857"},
1769             {"1/8", "0.125"},
1770             {"1/9", "0.˙1"},
1771             {"1/10", "0.1"},
1772             {"1/11", "0.˙09"},
1773             {"1/12", "0.08˙3"},
1774             {"1/13", "0.˙076923"},
1775             {"1/14", "0.0˙714285"},
1776             {"1/15", "0.0˙6"},
1777             {"1/16", "0.0625"},
1778         };
1779         for (String[] test : tests) {
1780             Rational source = Rational.of(test[0]);
1781             seen.add(source);
1782             String expected = test[1];
1783             String actual = source.toString(FormatStyle.repeating);
1784             assertEquals(test[0], expected, actual);
1785             Rational roundtrip = Rational.of(expected);
1786             assertEquals(expected, source, roundtrip);
1787         }
1788         for (int i = -50; i < 200; ++i) {
1789             for (int j = 0; j < 50; ++j) {
1790                 checkFormat(Rational.of(i, j), seen);
1791             }
1792         }
1793         for (Entry<String, TargetInfo> unitAndInfo : converter.getInternalConversionData().entrySet()) {
1794             final TargetInfo targetInfo2 = unitAndInfo.getValue();
1795             ConversionInfo targetInfo = targetInfo2.unitInfo;
1796             checkFormat(targetInfo.factor, seen);
1797             if (SHOW_DATA) {
1798                 String rFormat = targetInfo.factor.toString(FormatStyle.repeating);
1799                 String sFormat = targetInfo.factor.toString(FormatStyle.simple);
1800                 if (!rFormat.equals(sFormat)) {
1801                     System.out.println("\t\t" + unitAndInfo.getKey() + "\t" + targetInfo2.target + "\t" + sFormat + "\t" + rFormat + "\t" + targetInfo.factor.doubleValue());
1802                 }
1803             }
1804         }
1805     }
1806 
checkFormat(Rational source, Set<Rational> seen)1807     private void checkFormat(Rational source, Set<Rational> seen) {
1808         if (seen.contains(source)) {
1809             return;
1810         }
1811         seen.add(source);
1812         String formatted = source.toString(FormatStyle.repeating);
1813         Rational roundtrip = Rational.of(formatted);
1814         assertEquals("roundtrip " + formatted, source, roundtrip);
1815     }
1816 
1817     /** Check that units to be translated are as expected. */
testDistinguishedSetsOfUnits()1818     public void testDistinguishedSetsOfUnits() {
1819         Set<String> comparatorUnitIds = new LinkedHashSet<>(DtdData.unitOrder.getOrder());
1820         Set<String> validLongUnitIds = VALID_REGULAR_UNITS;
1821         Set<String> validAndDeprecatedLongUnitIds = ImmutableSet.<String>builder().addAll(VALID_REGULAR_UNITS).addAll(DEPRECATED_REGULAR_UNITS).build();
1822 
1823         final BiMap<String, String> shortToLong = Units.LONG_TO_SHORT.inverse();
1824         assertSuperset("converter short-long", "units short-long", converter.SHORT_TO_LONG_ID.entrySet(), shortToLong.entrySet());
1825         assertSuperset("units short-long", "converter short-long", shortToLong.entrySet(), converter.SHORT_TO_LONG_ID.entrySet());
1826 
1827         Set<String> errors = new LinkedHashSet<>();
1828         Set<String> unitsConvertibleLongIds = converter.canConvert().stream()
1829             .map(x -> {
1830                 String result = shortToLong.get(x);
1831                 if (result == null) {
1832                     errors.add("No short form of " + x);
1833                 }
1834                 return result;
1835             })
1836             .collect(Collectors.toSet());
1837         assertEquals("", Collections.emptySet(), errors);
1838 
1839         Set<String> simpleConvertibleLongIds = converter.canConvert().stream()
1840             .filter(x -> converter.isSimple(x))
1841             .map((String x) -> Units.LONG_TO_SHORT.inverse().get(x))
1842             .collect(Collectors.toSet());
1843         CLDRFile root = CLDR_CONFIG.getCldrFactory().make("root", true);
1844         ImmutableSet<String> unitLongIdsRoot = ImmutableSet.copyOf(getUnits(root, new TreeSet<>()));
1845         ImmutableSet<String> unitLongIdsEnglish = ImmutableSet.copyOf(getUnits(CLDR_CONFIG.getEnglish(), new TreeSet<>()));
1846 
1847         assertSameCollections("root unit IDs", "English", unitLongIdsRoot, unitLongIdsEnglish);
1848 
1849         final Set<String> validRootUnitIdsMinusOddballs = unitLongIdsRoot;
1850         final Set<String> validLongUnitIdsMinusOddballs = minus(validLongUnitIds, converter.getLongIds(UnitConverter.UNTRANSLATED_UNIT_NAMES));
1851         assertSameCollections("root unit IDs", "valid regular", validRootUnitIdsMinusOddballs, validLongUnitIdsMinusOddballs);
1852 
1853         assertSameCollections("comparatorUnitIds (DtdData)", "valid regular", comparatorUnitIds, validAndDeprecatedLongUnitIds);
1854 
1855         assertSuperset("valid regular", "specials", validLongUnitIds, GrammarInfo.getUnitsToAddGrammar());
1856 
1857         assertSuperset("root unit IDs", "specials", unitLongIdsRoot, GrammarInfo.getUnitsToAddGrammar());
1858 
1859         //assertSuperset("long convertible units", "valid regular", unitsConvertibleLongIds, validLongUnitIds);
1860         Output<String> baseUnit = new Output<>();
1861         for (String longUnit : validLongUnitIds) {
1862             if (longUnit.equals("temperature-generic")) {
1863                 continue;
1864             }
1865             String shortUnit = Units.getShort(longUnit);
1866             ConversionInfo conversionInfo = converter.parseUnitId(shortUnit, baseUnit, false);
1867             if (!assertNotNull("Can convert " + longUnit, conversionInfo)) {
1868                 converter.getUnitInfo(shortUnit, baseUnit);
1869                 int debug = 0;
1870             }
1871         }
1872 
1873         assertSuperset("valid regular", "simple convertible units", validLongUnitIds, simpleConvertibleLongIds);
1874 
1875         SupplementalDataInfo.getInstance().getUnitConverter();
1876     }
1877 
assertSameCollections(String title1, String title2, Collection<String> c1, Collection<String> c2)1878     public void assertSameCollections(String title1, String title2, Collection<String> c1, Collection<String> c2) {
1879         assertSuperset(title1, title2, c1, c2);
1880         assertSuperset(title2, title1, c2, c1);
1881     }
1882 
assertSuperset(String title1, String title2, Collection<V> c1, Collection<V> c2)1883     public <V> void assertSuperset(String title1, String title2, Collection<V> c1, Collection<V> c2) {
1884         if (!assertEquals(title1 + " ⊇ " + title2, Collections.emptySet(), minus(c2, c1))) {
1885             int debug = 0;
1886         }
1887     }
1888 
minus(Collection<V> a, Collection<V> b)1889     public <V> Set<V> minus(Collection<V> a, Collection<V> b) {
1890         Set<V> result = new LinkedHashSet<>(a);
1891         result.removeAll(b);
1892         return result;
1893     }
1894 
minus(Collection<V> a, V... b)1895     public <V> Set<V> minus(Collection<V> a, V... b) {
1896         Set<V> result = new LinkedHashSet<>(a);
1897         result.removeAll(Arrays.asList(b));
1898         return result;
1899     }
1900 
getUnits(CLDRFile root, Set<String> unitLongIds)1901     public Set<String> getUnits(CLDRFile root, Set<String> unitLongIds) {
1902         for (String path : root) {
1903             XPathParts parts = XPathParts.getFrozenInstance(path);
1904             int item = parts.findElement("unit");
1905             if (item == -1) {
1906                 continue;
1907             }
1908             String type = parts.getAttributeValue(item, "type");
1909             unitLongIds.add(type);
1910             // "//ldml/units/unitLength[@type=\"long\"]/unit[@type=\"" + unit + "\"]/gender"
1911         }
1912         return unitLongIds;
1913     }
1914 
1915     static final Pattern NORM_SPACES = Pattern.compile("[ \u00A0\u200E]");
1916 
TestGender()1917     public void TestGender() {
1918         Output<String> source = new Output<>();
1919         Multimap<UnitPathType, String> partsUsed = TreeMultimap.create();
1920         Factory factory = CLDR_CONFIG.getFullCldrFactory();
1921         Set<String> available = factory.getAvailable();
1922         int bad = 0;
1923 
1924         for (String locale : SDI.hasGrammarInfo()) {
1925             // skip ones without gender info
1926             GrammarInfo gi = SDI.getGrammarInfo("fr");
1927             Collection<String> genderInfo = gi.get(GrammaticalTarget.nominal, GrammaticalFeature.grammaticalGender, GrammaticalScope.general);
1928             if (genderInfo.isEmpty()) {
1929                 continue;
1930             }
1931             if (CLDRConfig.SKIP_SEED && !available.contains(locale)) {
1932                 continue;
1933             }
1934             // check others
1935             CLDRFile resolvedFile = factory.make(locale, true);
1936             for (Entry<String, String> entry : converter.SHORT_TO_LONG_ID.entrySet()) {
1937                 final String shortUnitId = entry.getKey();
1938                 final String longUnitId = entry.getValue();
1939                 final UnitId unitId = converter.createUnitId(shortUnitId);
1940                 partsUsed.clear();
1941                 String rawGender = UnitPathType.gender.getTrans(resolvedFile, "long", shortUnitId, null, null, null, partsUsed);
1942 
1943                 if (rawGender != null) {
1944                     String gender = unitId.getGender(resolvedFile, source, partsUsed);
1945                     if (gender != null && !shortUnitId.equals(source.value)) {
1946                         if (!Objects.equals(rawGender, gender)) {
1947                             if (SHOW_DATA) {
1948                                 printlnIfZero(bad);
1949                                 System.out.println(locale + ": computed gender = raw gender for\t" + shortUnitId + "\t"
1950                                     + Joiner.on("\n\t\t").join(partsUsed.asMap().entrySet()));
1951                             }
1952                             ++bad;
1953                         }
1954                     }
1955                 }
1956             }
1957         }
1958         if (bad > 0) {
1959             warnln(bad + " units x locales with incorrect computed gender. Use -DTestUnits:SHOW_DATA for details." );
1960         }
1961     }
1962 
TestFallbackNames()1963     public void TestFallbackNames() {
1964         String[][] sampleUnits = {
1965             {"fr", "square-meter", "one", "nominative", "{0} mètre carré"},
1966             {"fr", "square-meter", "other", "nominative", "{0} mètres carrés"},
1967             {"fr", "square-decimeter", "other", "nominative", "{0} décimètres carrés"},
1968             {"fr", "meter-per-square-second", "one", "nominative", "{0} mètre par seconde carrée"},
1969             {"fr", "meter-per-square-second", "other", "nominative", "{0} mètres par seconde carrée"},
1970 
1971             {"de", "square-meter", "other", "nominative", "{0} Quadratmeter"},
1972             {"de", "square-decimeter", "other", "nominative", "{0} Quadratdezimeter"}, // real fail
1973 
1974             {"de", "per-meter", "other", "nominative", "{0} pro Meter"},
1975             {"de", "per-square-meter", "other", "nominative", "{0} pro Quadratmeter"},
1976             {"de", "second-per-meter", "other", "nominative", "{0} Sekunden pro Meter"},
1977             {"de", "meter-per-second", "other", "nominative", "{0} Meter pro Sekunde"},
1978             {"de", "meter-per-square-second", "other", "nominative", "{0} Meter pro Quadratsekunde"},
1979 
1980             {"de", "gigasecond-per-decimeter", "other", "nominative", "{0} Gigasekunden pro Dezimeter"},
1981             {"de", "decimeter-per-gigasecond", "other", "nominative", "{0} Dezimeter pro Gigasekunde"}, // real fail
1982 
1983             {"de", "gigasecond-milligram-per-centimeter-decisecond", "other", "nominative", "{0} Milligramm⋅Gigasekunden pro Zentimeter⋅Dezisekunde"},
1984             {"de", "milligram-per-centimeter-decisecond", "other", "nominative", "{0} Milligramm pro Zentimeter⋅Dezisekunde"},
1985             {"de", "per-centimeter-decisecond", "other", "nominative", "{0} pro Zentimeter⋅Dezisekunde"},
1986             {"de", "gigasecond-milligram-per-centimeter", "other", "nominative", "{0} Milligramm⋅Gigasekunden pro Zentimeter"},
1987             {"de", "gigasecond-milligram", "other", "nominative", "{0} Milligramm⋅Gigasekunden"},
1988             {"de", "gigasecond-gram", "other", "nominative", "{0} Gramm⋅Gigasekunden"},
1989             {"de", "gigasecond-kilogram", "other", "nominative", "{0} Kilogramm⋅Gigasekunden"},
1990             {"de", "gigasecond-megagram", "other", "nominative", "{0} Megagramm⋅Gigasekunden"},
1991 
1992             {"de", "dessert-spoon-imperial-per-dessert-spoon-imperial", "one", "nominative", "{0} Imp. Dessertlöffel pro Imp. Dessertlöffel"},
1993             {"de", "dessert-spoon-imperial-per-dessert-spoon-imperial", "one", "accusative", "{0} Imp. Dessertlöffel pro Imp. Dessertlöffel"},
1994             {"de", "dessert-spoon-imperial-per-dessert-spoon-imperial", "other", "dative", "{0} Imp. Dessertlöffeln pro Imp. Dessertlöffel"},
1995             {"de", "dessert-spoon-imperial-per-dessert-spoon-imperial", "one", "genitive", "{0} Imp. Dessertlöffels pro Imp. Dessertlöffel"},
1996 
1997             // TODO: pick names (eg in Polish) that show differences in case.
1998             // {"de", "foebar-foobar-per-fiebar-faebar", "other", "genitive", null},
1999 
2000         };
2001         ImmutableMap<String, String> frOverrides = ImmutableMap.<String, String>builder() // insufficient data in French as yet
2002             .put("//ldml/units/unitLength[@type=\"long\"]/compoundUnit[@type=\"power2\"]/compoundUnitPattern1[@count=\"one\"]", "{0} carré") //
2003             .put("//ldml/units/unitLength[@type=\"long\"]/compoundUnit[@type=\"power2\"]/compoundUnitPattern1[@count=\"other\"]", "{0} carrés") //
2004             .put("//ldml/units/unitLength[@type=\"long\"]/compoundUnit[@type=\"power2\"]/compoundUnitPattern1[@count=\"one\"][@gender=\"feminine\"]", "{0} carrée") //
2005             .put("//ldml/units/unitLength[@type=\"long\"]/compoundUnit[@type=\"power2\"]/compoundUnitPattern1[@count=\"other\"][@gender=\"feminine\"]", "{0} carrées") //
2006             .build();
2007 
2008         Multimap<UnitPathType, String> partsUsed = TreeMultimap.create();
2009         int count = 0;
2010         for (String[] row : sampleUnits) {
2011             ++count;
2012             final String locale = row[0];
2013             CLDRFile resolvedFileRaw = CLDR_CONFIG.getCLDRFile(locale, true);
2014             LocaleStringProvider resolvedFile;
2015             switch(locale) {
2016             case "fr":  resolvedFile = resolvedFileRaw.makeOverridingStringProvider(frOverrides); break;
2017             default: resolvedFile = resolvedFileRaw; break;
2018             }
2019 
2020             String shortUnitId = row[1];
2021             String pluralCategory = row[2];
2022             String caseVariant = row[3];
2023             String expectedName = row[4];
2024             if (shortUnitId.equals("gigasecond-milligram")) {
2025                 int debug = 0;
2026             }
2027             final UnitId unitId = converter.createUnitId(shortUnitId);
2028             final String actual = unitId.toString(resolvedFile, "long", pluralCategory, caseVariant, partsUsed, false);
2029             assertEquals(count + ") " + Arrays.asList(row).toString() + "\n\t" + Joiner.on("\n\t").join(partsUsed.asMap().entrySet()), fixSpaces(expectedName), fixSpaces(actual));
2030         }
2031 
2032     }
TestFileFallbackNames()2033     public void TestFileFallbackNames() {
2034         Multimap<UnitPathType, String> partsUsed = TreeMultimap.create();
2035 
2036         // first gather all the  examples
2037         Set<String> skippedUnits = new LinkedHashSet<>();
2038         Set<String> testSet = StandardCodes.make().getLocaleCoverageLocales(Organization.cldr);
2039         Counter<String> localeToErrorCount = new Counter<>();
2040         for (String localeId : testSet) {
2041             if (localeId.contains("_")) {
2042                 continue; // skip to make test shorter
2043             }
2044             CLDRFile resolvedFile = CLDR_CONFIG.getCLDRFile(localeId, true);
2045             PluralInfo pluralInfo = CLDR_CONFIG.getSupplementalDataInfo().getPlurals(localeId);
2046             PluralRules pluralRules = pluralInfo.getPluralRules();
2047             GrammarInfo grammarInfo =CLDR_CONFIG.getSupplementalDataInfo().getGrammarInfo(localeId);
2048             Collection<String> caseVariants = grammarInfo == null ? null
2049                 : grammarInfo.get(GrammaticalTarget.nominal, GrammaticalFeature.grammaticalCase, GrammaticalScope.units);
2050             if (caseVariants == null || caseVariants.isEmpty()) {
2051                 caseVariants = Collections.singleton("nominative");
2052             }
2053 
2054 
2055             for (Entry<String, String> entry : converter.SHORT_TO_LONG_ID.entrySet()) {
2056                 final String shortUnitId = entry.getKey();
2057                 if (converter.getComplexity(shortUnitId) == UnitComplexity.simple) {
2058                     continue;
2059                 }
2060                 if (UnitConverter.HACK_SKIP_UNIT_NAMES.contains(shortUnitId)) {
2061                     skippedUnits.add(shortUnitId);
2062                     continue;
2063                 }
2064                 final String longUnitId = entry.getValue();
2065                 final UnitId unitId = converter.createUnitId(shortUnitId);
2066                 for (String width : Arrays.asList("long")) { // , "short", "narrow"
2067                     for (String pluralCategory : pluralRules.getKeywords()) {
2068                         for (String caseVariant : caseVariants) {
2069                             String composedName;
2070                             try {
2071                                 composedName = unitId.toString(resolvedFile, width, pluralCategory, caseVariant, partsUsed, false);
2072                             } catch (Exception e) {
2073                                 composedName = "ERROR:" + e.getMessage();
2074                             }
2075                             if (composedName != null && (composedName.contains("′") || composedName.contains("″"))) { // skip special cases
2076                                 continue;
2077                             }
2078                             partsUsed.clear();
2079                             String transName = UnitPathType.unit.getTrans(resolvedFile, width, shortUnitId, pluralCategory, caseVariant, null, isVerbose() ? partsUsed : null);
2080 
2081                             // HACK to fix different spaces around placeholder
2082                             if (!Objects.equals(fixSpaces(transName), fixSpaces(composedName))) {
2083                                 logln("\t" + localeId
2084                                     + "\t" + shortUnitId
2085                                     + "\t" + width
2086                                     + "\t" + pluralCategory
2087                                     + "\t" + caseVariant
2088                                     + "\texpected ≠ fallback\t«" + transName + "»\t≠\t«" + composedName+ "»"
2089                                     + partsUsed);
2090                                 localeToErrorCount.add(localeId, 1);
2091                             }
2092                         }
2093                     }
2094                 }
2095             }
2096         }
2097         if (!localeToErrorCount.isEmpty()) {
2098             warnln("composed name ≠ translated name: " + localeToErrorCount.getTotal() + ". Use -DTestUnits:SHOW_COMPOSE to see summary");
2099             if (SHOW_COMPOSE) {
2100                 System.out.println();
2101                 for (R2<Long, String> entry : localeToErrorCount.getEntrySetSortedByCount(false, null)) {
2102                     System.out.println("composed name ≠ translated name: " + entry.get0() + "\t" + entry.get1());
2103                 }
2104             }
2105         }
2106 
2107         if (!skippedUnits.isEmpty()) {
2108             warnln("Skipped unsupported units: " + skippedUnits);
2109         }
2110     }
2111 
fixSpaces(String transName)2112     public String fixSpaces(String transName) {
2113         return transName == null ? null : NORM_SPACES.matcher(transName).replaceAll(" ");
2114     }
2115 
TestCheckUnits()2116     public void TestCheckUnits() {
2117         CheckUnits checkUnits = new CheckUnits();
2118         PathHeader.Factory phf = PathHeader.getFactory();
2119         for (String locale : Arrays.asList("en", "fr", "de", "pl", "el")) {
2120             CLDRFile cldrFile = CLDR_CONFIG.getCldrFactory().make(locale, true);
2121 
2122             Options options = new Options();
2123             List<CheckStatus> possibleErrors = new ArrayList<>();
2124             checkUnits.setCldrFileToCheck(cldrFile, options, possibleErrors);
2125 
2126             for (String path : StreamSupport.stream(cldrFile.spliterator(), false).sorted().collect(Collectors.toList())) {
2127                 UnitPathType pathType = UnitPathType.getPathType(XPathParts.getFrozenInstance(path));
2128                 if (pathType == null || pathType == UnitPathType.unit) {
2129                     continue;
2130                 }
2131                 String value = cldrFile.getStringValue(path);
2132                 checkUnits.check(path, path, value, options, possibleErrors);
2133                 if (!possibleErrors.isEmpty()) {
2134                     PathHeader ph = phf.fromPath(path);
2135                     logln(locale + "\t" + ph.getCode() + "\t" + possibleErrors.toString());
2136                 }
2137             }
2138         }
2139     }
2140 
TestDerivedCase()2141     public void TestDerivedCase() {
2142         // needs further work
2143         if (logKnownIssue("CLDR-13920", "finish this as part of unit derivation work")) {
2144             return;
2145         }
2146         for (String locale : Arrays.asList("pl", "ru")) {
2147             CLDRFile cldrFile = CLDR_CONFIG.getCldrFactory().make(locale, true);
2148             GrammarInfo gi = SDI.getGrammarInfo(locale);
2149             Collection<String> rawCases = gi.get(GrammaticalTarget.nominal, GrammaticalFeature.grammaticalCase, GrammaticalScope.units);
2150 
2151             PluralInfo plurals = SupplementalDataInfo.getInstance().getPlurals(PluralType.cardinal, locale);
2152             Collection<Count> adjustedPlurals = plurals.getCounts();
2153 
2154             Output<String> sourceCase = new Output<>();
2155             Output<String> sourcePlural = new Output<>();
2156 
2157             M4<String, String, String, Boolean> myInfo = ChainedMap.of(new TreeMap<String,Object>(), new TreeMap<String,Object>(), new TreeMap<String,Object>(), Boolean.class);
2158 
2159             int count = 0;
2160             for (String longUnit : GrammarInfo.getUnitsToAddGrammar()) {
2161                 final String shortUnit = converter.getShortId(longUnit);
2162                 String gender = UnitPathType.gender.getTrans(cldrFile, "long", shortUnit, null, null, null, null);
2163 
2164                 for (String desiredCase : rawCases) {
2165                     // gather some general information
2166                     for (Count plural : adjustedPlurals) {
2167                         String value = UnitPathType.unit.getTrans(cldrFile, "long", shortUnit, plural.toString(), desiredCase, gender, null);
2168                         myInfo.put(gender, shortUnit + "\t" + value, plural.toString() + "+" + desiredCase, true);
2169                     }
2170 
2171                     // do actual test
2172                     if (desiredCase.contentEquals("nominative")) {
2173                         continue;
2174                     }
2175                     for (String desiredPlural : Arrays.asList("few", "other")) {
2176 
2177                         String value = UnitPathType.unit.getTrans(cldrFile, "long", shortUnit, desiredPlural, desiredCase, gender, null);
2178                         gi.getSourceCaseAndPlural(locale, gender, value, desiredCase, desiredPlural, sourceCase, sourcePlural);
2179                         String sourceValue = UnitPathType.unit.getTrans(cldrFile, "long", shortUnit, sourcePlural.value, sourceCase.value, gender, null);
2180                         assertEquals(count++ + ") " + locale
2181                             + ",\tshort unit/gender: " + shortUnit
2182                             + " / " + gender
2183                             + ",\tdesired case/plural: " + desiredCase
2184                             + " / " + desiredPlural
2185                             + ",\tsource case/plural: " + sourceCase
2186                             + " / " + sourcePlural
2187                             , value, sourceValue);
2188                     }
2189                 }
2190             }
2191             for (Entry<String, Map<String, Map<String, Boolean>>> m : myInfo) {
2192                 for (Entry<String, Map<String, Boolean>> t : m.getValue().entrySet()) {
2193                     System.out.println(m.getKey() + "\t" + t.getKey() + "\t" + t.getValue().keySet());
2194                 }
2195             }
2196         }
2197     }
TestGenderOfCompounds()2198     public void TestGenderOfCompounds() {
2199         Set<String> skipUnits = ImmutableSet.of("kilocalorie", "kilopascal", "terabyte", "gigabyte", "kilobyte", "gigabit", "kilobit", "megabit", "megabyte", "terabit");
2200         final ImmutableSet<String> keyValues = ImmutableSet.of("length", "mass", "duration", "power");
2201         int noGendersForLocales = 0;
2202         int localesWithNoGenders = 0;
2203         int localesWithSomeMissingGenders = 0;
2204 
2205         for (String localeID : GrammarInfo.getGrammarLocales()) {
2206             GrammarInfo grammarInfo = SDI.getGrammarInfo(localeID);
2207             if (grammarInfo == null) {
2208                 logln("No grammar info for: " + localeID);
2209                 continue;
2210             }
2211             UnitConverter converter = SDI.getUnitConverter();
2212             Collection<String> genderInfo = grammarInfo.get(GrammaticalTarget.nominal, GrammaticalFeature.grammaticalGender, GrammaticalScope.units);
2213             if (genderInfo.isEmpty()) {
2214                 continue;
2215             }
2216             CLDRFile cldrFile = info.getCldrFactory().make(localeID, true);
2217             Map<String,String> shortUnitToGender = new TreeMap<>();
2218             Output<String> source = new Output<>();
2219             Multimap<UnitPathType, String> partsUsed = LinkedHashMultimap.create();
2220 
2221             Set<String> units = new HashSet<>();
2222             M4<String, String, String, Boolean> quantityToGenderToUnits = ChainedMap.of(new TreeMap<String,Object>(), new TreeMap<String,Object>(), new TreeMap<String,Object>(), Boolean.class);
2223             M4<String, String, String, Boolean> genderToQuantityToUnits = ChainedMap.of(new TreeMap<String,Object>(), new TreeMap<String,Object>(), new TreeMap<String,Object>(), Boolean.class);
2224 
2225             for (String path : cldrFile) {
2226                 if (!path.startsWith("//ldml/units/unitLength[@type=\"long\"]/unit[@type=")) {
2227                     continue;
2228                 }
2229                 XPathParts parts = XPathParts.getFrozenInstance(path);
2230                 final String shortId = converter.getShortId(parts.getAttributeValue(-2, "type"));
2231                 if (NOT_CONVERTABLE.contains(shortId)) {
2232                     continue;
2233                 }
2234                 String quantity = null;
2235                 try {
2236                     quantity = converter.getQuantityFromUnit(shortId, false);
2237                 } catch (Exception e) {}
2238 
2239                 if (quantity == null) {
2240                     throw new IllegalArgumentException("No quantity for " + shortId);
2241                 }
2242 
2243                 //ldml/units/unitLength[@type="long"]/unit[@type="duration-year"]/gender
2244                 String gender = null;
2245                 if (parts.size() == 5 && parts.getElement(-1).equals("gender")) {
2246                     gender = cldrFile.getStringValue(path);
2247                     if (true) {
2248                         quantityToGenderToUnits.put(quantity, gender, shortId, true);
2249                         genderToQuantityToUnits.put(quantity, gender, shortId, true);
2250                     }
2251                 } else {
2252                     if (units.contains(shortId)) {
2253                         continue;
2254                     }
2255                     units.add(shortId);
2256                 }
2257                 UnitId unitId = converter.createUnitId(shortId);
2258                 String constructedGender = unitId.getGender(cldrFile, source, partsUsed);
2259                 boolean multiUnit = unitId.denUnitsToPowers.size() + unitId.denUnitsToPowers.size() > 1;
2260                 if (gender == null && (constructedGender == null || !multiUnit)) {
2261                     continue;
2262                 }
2263 
2264                 final boolean areEqual = Objects.equals(gender, constructedGender);
2265                 if (false) {
2266                     final String printInfo = localeID + "\t" + unitId + "\t" + gender + "\t" + multiUnit + "\t" + quantity + "\t" + constructedGender + "\t" + areEqual;
2267                     System.out.println(printInfo);
2268                 }
2269 
2270                 if (gender != null && !areEqual && !skipUnits.contains(shortId)) {
2271                     unitId.getGender(cldrFile, source, partsUsed);
2272                     shortUnitToGender.put(shortId, unitId + "\t actual gender: " + gender + "\t constructed gender:" + constructedGender);
2273                 }
2274             }
2275             if (quantityToGenderToUnits.keySet().isEmpty()) {
2276                 if (SHOW_DATA) {
2277                     printlnIfZero(noGendersForLocales);
2278                     System.out.println("No genders for\t" + localeID);
2279                 }
2280                 localesWithNoGenders++;
2281                 continue;
2282             }
2283 
2284             for (Entry<String,String> entry : shortUnitToGender.entrySet()) {
2285                 if (SHOW_COMPOSE) {
2286                     printlnIfZero(noGendersForLocales);
2287                     System.out.println(localeID + "\t" + entry);
2288                 }
2289                 noGendersForLocales++;
2290             }
2291 
2292             Set<String> missing = new LinkedHashSet<>(genderInfo);
2293             for (String quantity : keyValues) {
2294                 M3<String, String, Boolean> genderToUnits = quantityToGenderToUnits.get(quantity);
2295                 showData(localeID, null, quantity, genderToUnits);
2296                 missing.removeAll(genderToUnits.keySet());
2297             }
2298             for (String quantity : quantityToGenderToUnits.keySet()) {
2299                 M3<String, String, Boolean> genderToUnits = quantityToGenderToUnits.get(quantity);
2300                 showData(localeID, missing, quantity, genderToUnits);
2301             }
2302             for (String gender : missing) {
2303                 if (SHOW_DATA) {
2304                     printlnIfZero(noGendersForLocales);
2305                     System.out.println("Missing values: " + localeID + "\t" + "?" + "\t" + gender + "\t?");
2306                 }
2307                 noGendersForLocales++;
2308             }
2309         }
2310         if (noGendersForLocales > 0) {
2311             warnln(noGendersForLocales + " units x locales with missing gender. Use -DTestUnits:SHOW_DATA for info, -DTestUnits:SHOW_COMPOSE for compositions" );
2312         }
2313 
2314     }
2315 
printlnIfZero(int noGendersForLocales)2316     public void printlnIfZero(int noGendersForLocales) {
2317         if (noGendersForLocales == 0) {
2318             System.out.println();
2319         }
2320     }
2321 
showData(String localeID, Set<String> genderFilter, String quantity, final M3<String, String, Boolean> genderToUnits)2322     public void showData(String localeID, Set<String> genderFilter, String quantity, final M3<String, String, Boolean> genderToUnits) {
2323         for (Entry<String, Map<String, Boolean>> entry2 : genderToUnits) {
2324             String gender = entry2.getKey();
2325             if (genderFilter != null) {
2326                 if(!genderFilter.contains(gender)) {
2327                     continue;
2328                 }
2329                 genderFilter.remove(gender);
2330             }
2331             for (String unit : entry2.getValue().keySet()) {
2332                 logln(localeID + "\t" + quantity + "\t" + gender + "\t" + unit);
2333             }
2334         }
2335     }
2336 
2337     static final boolean DEBUG_DERIVATION = false;
2338 
testDerivation()2339     public void testDerivation() {
2340         int count = 0;
2341         for (String locale : SDI.hasGrammarDerivation()) {
2342             GrammarDerivation gd = SDI.getGrammarDerivation(locale);
2343             if (DEBUG_DERIVATION) System.out.println(locale + " => " + gd);
2344             ++count;
2345         }
2346         assertNotEquals("hasGrammarDerivation", 0, count);
2347     }
2348 
2349     static final boolean DEBUG_ORDER = false;
2350 
TestUnitOrder()2351     public void TestUnitOrder() {
2352         if (DEBUG_ORDER) {
2353             System.out.println();
2354             for (Entry<String, Collection<Continuation>> entry : converter.getContinuations().asMap().entrySet()) {
2355                 System.out.println(entry);
2356             }
2357         }
2358 
2359         for (Entry<String, String> entry : converter.getBaseUnitToQuantity().entrySet()) {
2360             checkNormalization("base-quantity, " + entry.getValue(), entry.getKey());
2361         }
2362 
2363         // check root list
2364         // crucial that this is stable!!
2365         Set<String> shortUnitsFound = checkCldrFileUnits("root unit", CLDRConfig.getInstance().getRoot());
2366         final Set<String> shortValidRegularUnits = converter.getShortIds(VALID_REGULAR_UNITS);
2367         assertEquals("root units - regular units", Collections.emptySet(),
2368             Sets.difference(shortUnitsFound, shortValidRegularUnits));
2369         assertEquals("regular units - special_untranslated - root units", Collections.emptySet(),
2370             Sets.difference(Sets.difference(shortValidRegularUnits, UnitConverter.UNTRANSLATED_UNIT_NAMES), shortUnitsFound));
2371 
2372         // check English also
2373         checkCldrFileUnits("en unit", CLDRConfig.getInstance().getEnglish());
2374 
2375         for (String unit : converter.canConvert()) {
2376             checkNormalization("convertable", unit);
2377             String baseUnitId = converter.getBaseUnit(unit);
2378             checkNormalization("convertable base", baseUnitId);
2379         }
2380 
2381         checkNormalization("test case", "foot-acre", "acre-foot");
2382         checkNormalization("test case", "meter-newton", "newton-meter");
2383 
2384         checkNormalization("test case", "newton-meter");
2385         checkNormalization("test case", "acre-foot");
2386 
2387         String stdAcre = converter.getStandardUnit("acre");
2388 
2389         UnitOrdering unitOrdering = new UnitOrdering();
2390         List<String> simpleBaseUnits = new ArrayList<>();
2391 
2392         for (ExternalUnitConversionData data : NistUnits.externalConversionData) {
2393             // unitOrdering.add(data.source);
2394             final String source = data.source;
2395             final String target = data.target;
2396             unitOrdering.add(target);
2397             checkNormalization("nist core, " + source, target);
2398         }
2399         for (Entry<String, TargetInfo> data : NistUnits.derivedUnitToConversion.entrySet()) {
2400             if (DEBUG_ORDER) {
2401                 System.out.println(data);
2402             }
2403             final String target = data.getValue().target;
2404             unitOrdering.add(target);
2405             simpleBaseUnits.add(data.getKey());
2406             checkNormalization("nist derived", target);
2407         }
2408 
2409         if (DEBUG_ORDER) {
2410             System.out.println("Pass 1\n" + unitOrdering.orderingData);
2411         }
2412 
2413         for (String baseUnit : converter.getBaseUnitToQuantity().keySet()) {
2414             unitOrdering.add(baseUnit);
2415             String status = converter.getBaseUnitToStatus().get(baseUnit);
2416             if ("simple".equals(status)) {
2417                 simpleBaseUnits.add(baseUnit);
2418             }
2419         }
2420         if (DEBUG_ORDER) {
2421             System.out.println("Pass 2\n" + unitOrdering.orderingData);
2422         }
2423 
2424         if (DEBUG_ORDER) System.out.println("Extracted data\n" + Joiner.on('\n').join(unitOrdering.orderingData.asMap().entrySet()));
2425         if (DEBUG_ORDER) System.out.println("Building data");
2426 
2427         // check the builder first
2428         TotalOrderBuilder<String> totalOrderBuilder = new TotalOrderBuilder<>();
2429 
2430         if (false) {
2431             totalOrderBuilder.add("meter", "second").add("kilogram", "meter");
2432             totalOrderBuilder.build();
2433 
2434             totalOrderBuilder.add("meter", "second").add("kilogram", "meter").add("second", "kilogram");
2435             try {
2436                 totalOrderBuilder.build();
2437             } catch (Exception e) {
2438                 errln("Problem in TotalOrderBuilder");
2439             }
2440         }
2441         if (DEBUG_ORDER)System.out.println("Show ordering");
2442         // now all the units
2443         for (List<String> orderedUnits : unitOrdering.orderingData.asMap().keySet()) {
2444             List<String> baseUnits = new ArrayList<>();
2445             for (String orderedUnit : orderedUnits) {
2446                 baseUnits.add(unitOrdering.getId(orderedUnit, unitOrdering.rejects));
2447             }
2448             if (DEBUG_ORDER)System.out.println(orderedUnits + "\t" + baseUnits);
2449             totalOrderBuilder.add(baseUnits);
2450         }
2451         for (String simpleBaseUnit : simpleBaseUnits) {
2452             totalOrderBuilder.add(Collections.singletonList(simpleBaseUnit));
2453         }
2454         if (DEBUG_ORDER)System.out.println(totalOrderBuilder);
2455 
2456         if (DEBUG_ORDER)System.out.println("Rejects: " + unitOrdering.rejects);
2457         if (DEBUG_ORDER)System.out.println("Ordering: " + totalOrderBuilder.build());
2458 
2459 //        for (Entry<String, Collection<String>> entry : piecesToOccurences.asMap().entrySet()) {
2460 //            System.out.println(entry.getKey() + "\t" + entry.getValue());
2461 //        }
2462     }
2463 
2464     /** Checks the normalization of units found in the file, and returns the set of shortUnitIds found in the file */
checkCldrFileUnits(String title, final CLDRFile cldrFile)2465     public Set<String> checkCldrFileUnits(String title, final CLDRFile cldrFile) {
2466         Set<String> shortUnitsFound = new TreeSet<>();
2467         for (String path : cldrFile) {
2468             if (!path.startsWith("//ldml/units/unitLength")) {
2469                 continue;
2470             }
2471             XPathParts parts = XPathParts.getFrozenInstance(path);
2472             String longUnitId = parts.findAttributeValue("unit", "type");
2473             if (longUnitId == null) {
2474                 continue;
2475             }
2476             String shortUnitId = converter.getShortId(longUnitId);
2477             shortUnitsFound.add(shortUnitId);
2478             checkNormalization(title, shortUnitId);
2479         }
2480         return ImmutableSet.copyOf(shortUnitsFound);
2481     }
2482 
checkNormalization(String title, String source, String expected)2483     public void checkNormalization(String title, String source, String expected) {
2484         String oldExpected = normalizationCache.get(source);
2485         if (oldExpected != null) {
2486             if (!oldExpected.equals(expected)) {
2487                 assertEquals(title + ", consistent expected results for " + source, oldExpected, expected);
2488             }
2489             return;
2490         }
2491         normalizationCache.put(source, expected);
2492         UnitId unitId = converter.createUnitId(source);
2493         assertEquals(title + ", unit order", expected, unitId.toString());
2494     }
2495 
checkNormalization(String title, String source)2496     public void checkNormalization(String title, String source) {
2497         checkNormalization(title, source, source);
2498     }
2499 
2500     static class UnitOrdering {
2501         boolean SKIP_POWERS = true;
2502         Set<String> SKIP_UNITS = ImmutableSet.of(
2503             "kilogram-per-pascal-second-square-meter",
2504             "kilogram-per-pascal-second-meter"
2505             );
2506 
2507         final Set<String> SUFFIXES = ImmutableSet.of(
2508             "0c",
2509             "15c",
2510             "20c",
2511             "23c",
2512             "32f",
2513             "365",
2514             "392f",
2515             "39f",
2516             "4c",
2517             "59f",
2518             "60f",
2519             "survey",
2520             "assay",
2521             "imperial",
2522             "long",
2523             "of",
2524             "capacitance",
2525             "inductance",
2526             "current",
2527             "electric",
2528             "potential",
2529             "electric",
2530             "inductance,",
2531             "resistance",
2532             "water",
2533             "troy",
2534             "tnt",
2535             "sidereal",
2536             "unitth",
2537             "unitit",
2538             "mean",
2539             "nutrition",
2540             "tropical",
2541             "pole",
2542             "boiler",
2543             "mil",
2544             "force",
2545             "printer",
2546             "refrigeration",
2547             "register",
2548             "technical",
2549             "thermal",
2550             "metric",
2551             "dry"
2552             );
2553 
2554         final Set<String> POWERS = ImmutableSet.of(
2555             "square",
2556             "cubic",
2557             "pow4");
2558         // mil-inch, perm-inch
2559 
2560 
2561         Set<String> seen = new HashSet<>();
2562         Multimap<String, String> piecesToOccurences = TreeMultimap.create();
2563         Multimap<String, Continuation> continuations = converter.getContinuations();
2564         TreeMultimap<List<String>, String> orderingData = TreeMultimap.create(Comparators.lexicographical(Ordering.natural()), Ordering.natural());
2565         TreeSet<String> rejects = new TreeSet<>();
2566 
add(String unitId)2567         void add(String unitId) {
2568             if (!unitId.contains("-")
2569                 || !seen.add(unitId)
2570                 || SKIP_UNITS.contains(unitId)) {
2571                 return;
2572             }
2573             if (unitId.contains("square-meter-kilogram")) {
2574                 int debug = 0;
2575             }
2576             List<String> pieces = new ArrayList<>();
2577             ArrayList<String> orderedNumerator = new ArrayList<>();
2578             ArrayList<String> orderedDenominator = new ArrayList<>();
2579             ArrayList<String> current = orderedNumerator;
2580             for (UnitIterator it = Continuation.split(unitId, continuations).iterator(); it.hasNext();) {
2581                 String unit = it.next();
2582                 if (unit.equals("per")) {
2583                     if (current == orderedDenominator) {
2584                         throw new IllegalArgumentException();
2585                     }
2586                     handleOrdering(current, unitId);
2587                     current = orderedDenominator;
2588                     continue;
2589                 }
2590                 if (POWERS.contains(unit)) {
2591                     if (SKIP_POWERS) {
2592                         continue;
2593                     }
2594                     String nextUnit = it.next();
2595                     nextUnit = UnitConverter.stripPrefix(nextUnit, null);
2596                     unit += "-" + nextUnit; // should never overrun
2597                 } else {
2598                     unit = UnitConverter.stripPrefix(unit, null);
2599                 }
2600                 String peek = it.peek();
2601                 while (peek != null && SUFFIXES.contains(peek)) {
2602                     unit += "-" + peek;
2603                     it.next();
2604                     peek = it.peek();
2605                 }
2606                 current.add(unit);
2607                 pieces.add(unit);
2608                 piecesToOccurences.put(unit, unitId);
2609             }
2610             handleOrdering(current, unitId);
2611             //System.out.println(pieces + "\t=>\t" + data.target);
2612         }
2613 
2614 
2615         Map<String, String> EXTRA_BASES = ImmutableMap.<String,String>builder()
2616             .put("british-thermal-unitit", "joule")
2617             .put("british-thermal-unitth", "joule")
2618             .put("centimeter", "meter")
2619             .put("circular-mil", "meter")
2620             //.put("dry", "???")
2621             .put("dyne", "newton")
2622             .put("foot-survey", "meter")
2623             .put("inch-0c", "meter")
2624             .put("inch-23c", "meter")
2625             .put("kilogram-force", "newton")
2626             .put("kilowatt", "watt")
2627             //.put("mil", "???")
2628             .put("millimeter", "meter")
2629             .put("ofhg-0c", "ofhg")
2630             .put("ofhg-32f", "ofhg")
2631             .put("ofhg-60f", "ofhg")
2632             .put("ounce-force", "newton")
2633             .put("perm", "kilogram-per-second-per-square-meter-per-pascal")
2634             .put("poundal", "newton")
2635             .put("rankine", "celcius")
2636             .build();
2637 
getId(String orderedUnit, Set<String> rejects)2638         public String getId(String orderedUnit, Set<String> rejects) {
2639             String result = converter.getStandardUnit(orderedUnit);
2640             if (result == null) {
2641                 result = EXTRA_BASES.get(orderedUnit);
2642                 if (result == null) {
2643                     rejects.add(orderedUnit);
2644                     return "???";
2645                 }
2646             }
2647             return result;
2648         }
2649 
handleOrdering(ArrayList<String> current, String source)2650         private void handleOrdering(ArrayList<String> current, String source) {
2651             if (current.size() < 2) {
2652                 return;
2653             }
2654             orderingData.put(current, source);
2655         }
2656     }
2657 
TestElectricConsumption()2658     public void TestElectricConsumption() {
2659         String inputUnit = "kilowatt-hour-per-100-kilometer";
2660         String outputUnit = "kilogram-meter-per-square-second";
2661         Rational result = converter.convert(Rational.ONE, inputUnit, outputUnit, DEBUG);
2662         assertEquals("kWh-per-100k", Rational.of(36), result);
2663     }
2664 
TestEnglishDisplayNames()2665     public void TestEnglishDisplayNames() {
2666         CLDRFile en = CLDRConfig.getInstance().getEnglish();
2667         ImmutableSet<String> unitSkips = ImmutableSet.of("temperature-generic", "graphics-em");
2668         for (String path : en) {
2669             if (path.startsWith("//ldml/units/unitLength[@type=\"long\"]") && path.endsWith("/displayName")) {
2670                 if (path.contains("coordinateUnit")) {
2671                     continue;
2672                 }
2673                 XPathParts parts = XPathParts.getFrozenInstance(path);
2674                 final String longUnitId = parts.getAttributeValue(3, "type");
2675                 if (unitSkips.contains(longUnitId)) {
2676                     continue;
2677                 }
2678                 final String width = parts.getAttributeValue(2, "type");
2679                 //ldml/units/unitLength[@type="long"]/unit[@type="duration-decade"]/displayName
2680                 String displayName = en.getStringValue(path);
2681 
2682                 //ldml/units/unitLength[@type="long"]/unit[@type="duration-decade"]/unitPattern[@count="other"]
2683                 String pluralFormPath = path.substring(0,path.length()-"/displayName".length()) + "/unitPattern[@count=\"other\"]";
2684                 String pluralForm = en.getStringValue(pluralFormPath);
2685                 if (pluralForm == null) {
2686                     errln("Have display name but no plural: " + pluralFormPath);
2687                 } else {
2688                     String cleaned = pluralForm.replace("{0}", "").trim();
2689                     assertEquals("Unit display name should correspond to plural in English " + width + ", " + longUnitId,
2690                         cleaned, displayName);
2691                 }
2692             }
2693         }
2694     }
2695 
2696     enum TranslationStatus {has_grammar_M, has_grammar_X, add_grammar, skip_grammar, skip_trans}
2697 
2698     /** Check which units are enabled for translation.
2699      * If -v, then generates lines for spreadsheet checks.
2700      */
TestUnitsToTranslate()2701     public void TestUnitsToTranslate() {
2702         Set<String> toTranslate = GrammarInfo.getUnitsToAddGrammar();
2703         final CLDRConfig config = CLDRConfig.getInstance();
2704         final UnitConverter converter = config.getSupplementalDataInfo().getUnitConverter();
2705         Map<String, TranslationStatus> shortUnitToTranslationStatus40 = new TreeMap<>();
2706         for (String longUnit : Validity.getInstance().getStatusToCodes(LstrType.unit).get(Status.regular)) {
2707             String shortUnit = converter.getShortId(longUnit);
2708             shortUnitToTranslationStatus40.put(shortUnit, TranslationStatus.skip_trans);
2709         }
2710         for (String path : With.in(config.getRoot().iterator("//ldml/units/unitLength[@type=\"short\"]/unit"))) {
2711             XPathParts parts = XPathParts.getFrozenInstance(path);
2712             String longUnit = parts.getAttributeValue(3, "type");
2713             // Add simple units
2714             String shortUnit = converter.getShortId(longUnit);
2715             Set<UnitSystem> systems = converter.getSystemsEnum(shortUnit);
2716 
2717             boolean siOrMetric = !Collections.disjoint(systems, UnitSystem.SiOrMetric);
2718 
2719             TranslationStatus status =
2720                 toTranslate.contains(longUnit) ? (siOrMetric ? TranslationStatus.has_grammar_M : TranslationStatus.has_grammar_X)
2721                     : siOrMetric ? TranslationStatus.add_grammar : TranslationStatus.skip_grammar;
2722             shortUnitToTranslationStatus40.put(shortUnit, status);
2723         }
2724         for (Entry<String, TranslationStatus> entry : shortUnitToTranslationStatus40.entrySet()) {
2725             String shortUnit = entry.getKey();
2726             TranslationStatus status40 = entry.getValue();
2727             if (isVerbose()) System.out.println(shortUnit
2728                 + "\t" + converter.getQuantityFromUnit(shortUnit, false)
2729                 + "\t" + converter.getSystemsEnum(shortUnit)
2730                 + "\t" + (converter.isSimple(shortUnit) ? "simple" : "complex")
2731                 + "\t" + status40
2732                 );
2733         }
2734     }
2735 
2736     static final String marker = "➗";
2737 
TestValidUnitIdComponents()2738     public void TestValidUnitIdComponents() {
2739         for (String longUnit : VALID_REGULAR_UNITS) {
2740             String shortUnit = SDI.getUnitConverter().getShortId(longUnit);
2741             checkShortUnit(shortUnit);
2742         }
2743     }
2744 
TestDeprecatedUnitIdComponents()2745     public void TestDeprecatedUnitIdComponents() {
2746         for (String longUnit : DEPRECATED_REGULAR_UNITS) {
2747             String shortUnit = SDI.getUnitConverter().getShortId(longUnit);
2748             checkShortUnit(shortUnit);
2749         }
2750     }
2751 
TestSelectedUnitIdComponents()2752     public void TestSelectedUnitIdComponents() {
2753         checkShortUnit("curr-chf");
2754     }
2755 
2756 
checkShortUnit(String shortUnit)2757     public void checkShortUnit(String shortUnit) {
2758         List<String> parts = SPLIT_DASH.splitToList(shortUnit);
2759         List<String> simpleUnit = new ArrayList<>();
2760         UnitIdComponentType lastType = null;
2761         // structure is (prefix* base* suffix*) per ((prefix* base* suffix*)
2762 
2763         for (String part : parts) {
2764             UnitIdComponentType type = getUnitIdComponentType(part);
2765             switch(type) {
2766             case prefix:
2767                 if (lastType != UnitIdComponentType.prefix && !simpleUnit.isEmpty()) {
2768                     simpleUnit.add(marker);
2769                 }
2770                 break;
2771             case base:
2772                 if (lastType != UnitIdComponentType.prefix && !simpleUnit.isEmpty()) {
2773                     simpleUnit.add(marker);
2774                 }
2775                 break;
2776             case suffix:
2777                 if (!(lastType == UnitIdComponentType.base || lastType == UnitIdComponentType.suffix)) {
2778                     if ("metric".equals(part)) { // backward compatibility for metric ton; only needed if deprecated ids are allowed
2779                         lastType = UnitIdComponentType.prefix;
2780                     } else {
2781                         errln(simpleUnit + "/" + part + "; suffix only after base or suffix: " + false);
2782                     }
2783                 }
2784                 break;
2785                 // could add more conditions on these
2786             case and:
2787                 assertNotNull(simpleUnit + "/" + part + "; not at start", lastType);
2788                 // fall through
2789             case power:
2790             case per:
2791                 assertNotEquals(simpleUnit + "/" + part + "; illegal after prefix", UnitIdComponentType.prefix, lastType);
2792                 if (!simpleUnit.isEmpty()) {
2793                     simpleUnit.add(marker);
2794                 }
2795                 break;
2796             }
2797             simpleUnit.add(part + "*" + type.toShortId());
2798             lastType = type;
2799         }
2800         assertTrue(simpleUnit + ": last item must be base or suffix",
2801             lastType == UnitIdComponentType.base || lastType == UnitIdComponentType.suffix);
2802         logln("\t" + shortUnit + "\t" + simpleUnit.toString());
2803     }
2804 
getUnitIdComponentType(String part)2805     public UnitIdComponentType getUnitIdComponentType(String part) {
2806         return SDI.getUnitIdComponentType(part);
2807     }
2808 
TestMetricTon()2809     public void TestMetricTon() {
2810         assertTrue("metric-ton is deprecated", DEPRECATED_REGULAR_UNITS.contains("mass-metric-ton"));
2811         assertEquals("metric-ton is deprecated", "tonne", SDI.getUnitConverter().fixDenormalized("metric-ton"));
2812         assertEquals("to short", "metric-ton", SDI.getUnitConverter().getShortId("mass-metric-ton"));
2813         //assertEquals("to long", "mass-metric-ton", SDI.getUnitConverter().getLongId("metric-ton"));
2814     }
2815 
TestUnitParser()2816     public void TestUnitParser() {
2817         UnitParser up = new UnitParser();
2818         for (String longUnit : VALID_REGULAR_UNITS) {
2819             String shortUnit = SDI.getUnitConverter().getShortId(longUnit);
2820             checkParse(up, shortUnit);
2821         }
2822     }
2823 
checkParse(UnitParser up, String shortUnit)2824     private List<Pair<String, UnitIdComponentType>> checkParse(UnitParser up, String shortUnit) {
2825         up.set(shortUnit);
2826         List<Pair<String, UnitIdComponentType>> results = new ArrayList<>();
2827        Output<UnitIdComponentType> type = new Output<>();
2828         while (true) {
2829             String result = up.nextParse(type);
2830             if (result == null) {
2831                 break;
2832             }
2833             results.add(new Pair<>(result, type.value));
2834         }
2835         logln(shortUnit + "\t" + results);
2836         return results;
2837     }
2838 
TestUnitParserSelected()2839     public void TestUnitParserSelected() {
2840         UnitParser up = new UnitParser();
2841         String[][] tests = {
2842             // unit, exception, resultList
2843             {"british-force", "Unit suffix must follow base: british ❌ force"}, // prefix-suffix
2844             {"force", "Unit suffix must follow base: null ❌ force"}, // suffix
2845             {"british-and-french", "Unit prefix must be followed with base: british ❌ and"}, // prefix-and
2846             {"british", "Unit prefix must be followed with base: british ❌ null"}, // prefix
2847             {"g-force-light-year", null, "[(g-force,base), (light-year,base)]"}, // suffix
2848         };
2849         for (String[] test : tests) {
2850             String shortUnit = test[0];
2851             String expectedError = test[1];
2852             String expectedResult = test.length <= 2 ? null : test[2];
2853 
2854             String actualError = null;
2855             List<Pair<String, UnitIdComponentType>> actualResult  = null;
2856             try {
2857                 actualResult  = checkParse(up, shortUnit);
2858             } catch (Exception e) {
2859                 actualError = e.getMessage();
2860             }
2861             assertEquals(shortUnit + " exception", expectedError, actualError);
2862             assertEquals(shortUnit + " result", expectedResult, actualResult == null ? null : actualResult.toString());
2863         }
2864     }
2865 
TestUnitParserAgainstContinuations()2866     public void TestUnitParserAgainstContinuations() {
2867         UnitParser up = new UnitParser();
2868         UnitConverter uc = SDI.getUnitConverter();
2869         Multimap<String, Continuation> continuations = uc.getContinuations();
2870         Output<UnitIdComponentType> type = new Output<>();
2871         for (String longUnit : VALID_REGULAR_UNITS) {
2872             String shortUnit = SDI.getUnitConverter().getShortId(longUnit);
2873             if (shortUnit.contains("100")) {
2874                 logKnownIssue("CLDR-15929", "Code doesn't handle 100");
2875                 continue;
2876             }
2877             up.set(shortUnit);
2878             UnitIterator x = UnitConverter.Continuation.split(shortUnit, continuations);
2879 
2880             int count = 0;
2881             while (true) {
2882                 String upSegment = up.nextParse(type);
2883                 String continuationSegment = x.hasNext() ? x.next() : null;
2884                 if (upSegment == null || continuationSegment == null) {
2885                     assertEquals(count + ") " + shortUnit
2886                         + " Same number of segments ", continuationSegment == null, upSegment == null);
2887                     break;
2888                 }
2889                 assertTrue("type is never suffix or prefix", UnitIdComponentType.suffix != type.value &&  UnitIdComponentType.prefix != type.value);
2890                 ++count;
2891                 if (!assertEquals(count + ") " + shortUnit
2892                     + " Segments equal ", continuationSegment, upSegment)) {
2893                     break; // stop at first difference
2894                 }
2895             }
2896         }
2897     }
2898 }
2899