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