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