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