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