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