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