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