1 package org.unicode.cldr.util; 2 3 import com.google.common.base.Joiner; 4 import com.google.common.base.Splitter; 5 import com.google.common.collect.BiMap; 6 import com.google.common.collect.ImmutableBiMap; 7 import com.google.common.collect.ImmutableList; 8 import com.google.common.collect.ImmutableMap; 9 import com.google.common.collect.ImmutableMultimap; 10 import com.google.common.collect.ImmutableSet; 11 import com.google.common.collect.ImmutableSet.Builder; 12 import com.google.common.collect.LinkedHashMultimap; 13 import com.google.common.collect.Multimap; 14 import com.google.common.collect.Sets; 15 import com.google.common.collect.TreeMultimap; 16 import com.ibm.icu.impl.Row; 17 import com.ibm.icu.impl.Row.R2; 18 import com.ibm.icu.impl.Row.R4; 19 import com.ibm.icu.lang.UCharacter; 20 import com.ibm.icu.number.UnlocalizedNumberFormatter; 21 import com.ibm.icu.text.PluralRules; 22 import com.ibm.icu.util.Freezable; 23 import com.ibm.icu.util.Output; 24 import com.ibm.icu.util.ULocale; 25 import java.math.BigDecimal; 26 import java.math.BigInteger; 27 import java.math.MathContext; 28 import java.text.MessageFormat; 29 import java.util.ArrayList; 30 import java.util.Arrays; 31 import java.util.Collection; 32 import java.util.Collections; 33 import java.util.Comparator; 34 import java.util.EnumSet; 35 import java.util.HashSet; 36 import java.util.Iterator; 37 import java.util.LinkedHashMap; 38 import java.util.LinkedHashSet; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.Map.Entry; 42 import java.util.Objects; 43 import java.util.Set; 44 import java.util.TreeMap; 45 import java.util.TreeSet; 46 import java.util.concurrent.ConcurrentHashMap; 47 import java.util.function.Function; 48 import java.util.regex.Matcher; 49 import java.util.regex.Pattern; 50 import java.util.stream.Collectors; 51 import org.unicode.cldr.util.GrammarDerivation.CompoundUnitStructure; 52 import org.unicode.cldr.util.GrammarDerivation.Values; 53 import org.unicode.cldr.util.GrammarInfo.GrammaticalFeature; 54 import org.unicode.cldr.util.Rational.FormatStyle; 55 import org.unicode.cldr.util.Rational.RationalParser; 56 import org.unicode.cldr.util.StandardCodes.LstrType; 57 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo; 58 import org.unicode.cldr.util.SupplementalDataInfo.UnitIdComponentType; 59 import org.unicode.cldr.util.Validity.Status; 60 61 public class UnitConverter implements Freezable<UnitConverter> { 62 public static boolean DEBUG = false; 63 public static final Integer INTEGER_ONE = 1; 64 65 static final Splitter BAR_SPLITTER = Splitter.on('-'); 66 static final Splitter SPACE_SPLITTER = Splitter.on(' ').trimResults().omitEmptyStrings(); 67 68 public static final Set<String> UNTRANSLATED_UNIT_NAMES = 69 ImmutableSet.of("portion", "ofglucose", "100-kilometer", "ofhg"); 70 71 public static final Set<String> HACK_SKIP_UNIT_NAMES = 72 ImmutableSet.of( 73 // skip dot because pixel is preferred 74 "dot-per-centimeter", 75 "dot-per-inch", 76 // skip because a component is not translated 77 "liter-per-100-kilometer", 78 "millimeter-ofhg", 79 "inch-ofhg"); 80 81 final RationalParser rationalParser; 82 final Function<String, UnitIdComponentType> componentTypeData; 83 84 private Map<String, String> baseUnitToQuantity = new LinkedHashMap<>(); 85 private Map<String, String> baseUnitToStatus = new LinkedHashMap<>(); 86 private Map<String, TargetInfo> sourceToTargetInfo = new LinkedHashMap<>(); 87 private Map<String, String> sourceToStandard; 88 private Multimap<String, String> quantityToSimpleUnits = LinkedHashMultimap.create(); 89 private Multimap<String, UnitSystem> sourceToSystems = TreeMultimap.create(); 90 private Set<String> baseUnits; 91 private MapComparator<String> quantityComparator; 92 93 private Map<String, String> fixDenormalized; 94 private ImmutableMap<String, UnitId> idToUnitId; 95 96 public final BiMap<String, String> SHORT_TO_LONG_ID = Units.LONG_TO_SHORT.inverse(); 97 public final Set<String> LONG_PREFIXES = Units.TYPE_TO_CORE.keySet(); 98 99 private boolean frozen = false; 100 101 public TargetInfoComparator targetInfoComparator; 102 103 private final MapComparator<String> LongUnitIdOrder = new MapComparator<>(); 104 private final MapComparator<String> ShortUnitIdOrder = new MapComparator<>(); 105 getLongUnitIdComparator()106 public Comparator<String> getLongUnitIdComparator() { 107 return LongUnitIdOrder; 108 } 109 getShortUnitIdComparator()110 public Comparator<String> getShortUnitIdComparator() { 111 return ShortUnitIdOrder; 112 } 113 114 /** Warning: ordering is important; determines the normalized output */ 115 public static final Set<String> BASE_UNITS = 116 ImmutableSet.of( 117 "candela", 118 "kilogram", 119 "meter", 120 "second", 121 "ampere", 122 "kelvin", 123 // non-SI 124 "year", 125 "bit", 126 "item", 127 "pixel", 128 "em", 129 "revolution", 130 "portion", 131 "night"); 132 addQuantityInfo(String baseUnit, String quantity, String status)133 public void addQuantityInfo(String baseUnit, String quantity, String status) { 134 if (baseUnitToQuantity.containsKey(baseUnit)) { 135 throw new IllegalArgumentException( 136 "base unit " 137 + baseUnit 138 + " already defined for quantity " 139 + quantity 140 + " with status " 141 + status); 142 } 143 baseUnitToQuantity.put(baseUnit, quantity); 144 if (status != null) { 145 baseUnitToStatus.put(baseUnit, status); 146 } 147 quantityToSimpleUnits.put(quantity, baseUnit); 148 } 149 150 public static final Set<String> BASE_UNIT_PARTS = 151 ImmutableSet.<String>builder() 152 .add("per") 153 .add("square") 154 .add("cubic") 155 .add("pow4") 156 .addAll(BASE_UNITS) 157 .build(); 158 159 public static final Pattern PLACEHOLDER = 160 Pattern.compile( 161 "[ \\u00A0\\u200E\\u200F\\u202F]*\\{0\\}[ \\u00A0\\u200E\\u200F\\u202F]*"); 162 public static final boolean HACK = true; 163 164 @Override isFrozen()165 public boolean isFrozen() { 166 return frozen; 167 } 168 169 @Override freeze()170 public UnitConverter freeze() { 171 if (!frozen) { 172 frozen = true; 173 rationalParser.freeze(); 174 sourceToTargetInfo = ImmutableMap.copyOf(sourceToTargetInfo); 175 sourceToStandard = buildSourceToStandard(); 176 quantityToSimpleUnits = ImmutableMultimap.copyOf(quantityToSimpleUnits); 177 quantityComparator = getQuantityComparator(baseUnitToQuantity, baseUnitToStatus); 178 179 sourceToSystems = ImmutableMultimap.copyOf(sourceToSystems); 180 // other fields are frozen earlier in processing 181 Builder<String> builder = ImmutableSet.<String>builder().addAll(BASE_UNITS); 182 for (TargetInfo s : sourceToTargetInfo.values()) { 183 builder.add(s.target); 184 } 185 baseUnits = builder.build(); 186 targetInfoComparator = new TargetInfoComparator(); 187 188 buildMapComparators(); 189 190 // must be after building comparators 191 idToUnitId = ImmutableMap.copyOf(buildIdToUnitId()); 192 } 193 return this; 194 } 195 buildMapComparators()196 public void buildMapComparators() { 197 Set<R4<Integer, UnitSystem, Rational, String>> all = new TreeSet<>(); 198 Set<String> baseSeen = new HashSet<>(); 199 if (DEBUG) { 200 UnitParser up = new UnitParser(componentTypeData); 201 Output<UnitIdComponentType> uict = new Output<>(); 202 203 for (String longUnit : 204 Validity.getInstance().getStatusToCodes(LstrType.unit).get(Status.regular)) { 205 String shortUnit = getShortId(longUnit); 206 up.set(shortUnit); 207 List<String> items = new ArrayList<>(); 208 String msg = "\t"; 209 try { 210 while (true) { 211 String item = up.nextParse(uict); 212 if (item == null) break; 213 items.add(item); 214 // items.add(uict.value.toString()); 215 } 216 } catch (Exception e) { 217 msg = e.getMessage() + "\t"; 218 } 219 System.out.println(shortUnit + "\t" + Joiner.on('\t').join(items)); 220 } 221 } 222 for (String longUnit : 223 Validity.getInstance().getStatusToCodes(LstrType.unit).get(Status.regular)) { 224 Output<String> base = new Output<>(); 225 String shortUnit = getShortId(longUnit); 226 ConversionInfo conversionInfo = parseUnitId(shortUnit, base, false); 227 if (base.value == null) { 228 int debug = 0; 229 } 230 if (conversionInfo == null) { 231 if (longUnit.equals("temperature-generic")) { 232 conversionInfo = parseUnitId("kelvin", base, false); 233 } 234 } 235 String quantity; 236 Integer quantityNumericOrder = null; 237 try { 238 quantity = getQuantityFromUnit(base.value, false); 239 quantityNumericOrder = quantityComparator.getNumericOrder(quantity); 240 } catch (Exception e) { 241 System.out.println( 242 "Failed " 243 + shortUnit 244 + ", " 245 + base 246 + ", " 247 + quantityNumericOrder 248 + ", " 249 + e); 250 continue; 251 } 252 if (quantityNumericOrder == null) { // try the inverse 253 if (base.value.equals("meter-per-cubic-meter")) { // HACK 254 quantityNumericOrder = quantityComparator.getNumericOrder("consumption"); 255 } 256 if (quantityNumericOrder == null) { 257 throw new IllegalArgumentException( 258 "Missing quantity for: " + base.value + ", " + shortUnit); 259 } 260 } 261 262 final EnumSet<UnitSystem> systems = EnumSet.copyOf(getSystemsEnum(shortUnit)); 263 264 // to sort the right items together items together, put together a sort key 265 UnitSystem sortingSystem = systems.iterator().next(); 266 switch (sortingSystem) { 267 case metric: 268 case si: 269 case si_acceptable: 270 case astronomical: 271 case metric_adjacent: 272 case person_age: 273 sortingSystem = UnitSystem.metric; 274 break; 275 // country specific 276 case other: 277 case ussystem: 278 case uksystem: 279 case jpsystem: 280 sortingSystem = UnitSystem.other; 281 break; 282 default: 283 throw new IllegalArgumentException( 284 "Add new unitSystem to a grouping: " + sortingSystem); 285 } 286 R4<Integer, UnitSystem, Rational, String> sortKey = 287 Row.of(quantityNumericOrder, sortingSystem, conversionInfo.factor, shortUnit); 288 all.add(sortKey); 289 } 290 LongUnitIdOrder.setErrorOnMissing(true); 291 ShortUnitIdOrder.setErrorOnMissing(true); 292 for (R4<Integer, UnitSystem, Rational, String> item : all) { 293 String shortId = item.get3(); 294 ShortUnitIdOrder.add(shortId); 295 LongUnitIdOrder.add(getLongId(shortId)); 296 } 297 LongUnitIdOrder.freeze(); 298 ShortUnitIdOrder.freeze(); 299 } 300 buildIdToUnitId()301 public Map<String, UnitId> buildIdToUnitId() { 302 Map<String, UnitId> _idToUnitId = new TreeMap<>(); 303 for (Entry<String, String> shortAndLongId : SHORT_TO_LONG_ID.entrySet()) { 304 String shortId = shortAndLongId.getKey(); 305 String longId = shortAndLongId.getKey(); 306 UnitId uid; 307 try { 308 uid = createUnitId(shortId).freeze(); 309 } catch (Exception e) { 310 System.out.println("Failed with " + shortId); 311 continue; 312 } 313 boolean doTest = false; 314 Output<Rational> deprefix = new Output<>(); 315 for (Entry<String, Integer> entry : uid.numUnitsToPowers.entrySet()) { 316 final String unitPart = entry.getKey(); 317 UnitConverter.stripPrefix(unitPart, deprefix); 318 if (!deprefix.value.equals(Rational.ONE) || !entry.getValue().equals(INTEGER_ONE)) { 319 doTest = true; 320 break; 321 } 322 } 323 if (!doTest) { 324 for (Entry<String, Integer> entry : uid.denUnitsToPowers.entrySet()) { 325 final String unitPart = entry.getKey(); 326 UnitConverter.stripPrefix(unitPart, deprefix); 327 if (!deprefix.value.equals(Rational.ONE)) { 328 doTest = true; 329 break; 330 } 331 } 332 } 333 if (doTest) { 334 _idToUnitId.put(shortId, uid); 335 _idToUnitId.put(longId, uid); 336 } 337 } 338 return ImmutableMap.copyOf(_idToUnitId); 339 } 340 341 /** 342 * Return the 'standard unit' for the source. 343 * 344 * @return 345 */ buildSourceToStandard()346 private Map<String, String> buildSourceToStandard() { 347 Map<String, String> unitToStandard = new TreeMap<>(); 348 for (Entry<String, TargetInfo> entry : sourceToTargetInfo.entrySet()) { 349 String source = entry.getKey(); 350 TargetInfo targetInfo = entry.getValue(); 351 if (targetInfo.unitInfo.factor.equals(Rational.ONE) 352 && targetInfo.unitInfo.offset.equals(Rational.ZERO)) { 353 final String target = targetInfo.target; 354 String old = unitToStandard.get(target); 355 if (old == null) { 356 unitToStandard.put(target, source); 357 if (DEBUG) System.out.println(target + " ⟹ " + source); 358 } else if (old.length() > source.length()) { 359 unitToStandard.put(target, source); 360 if (DEBUG) 361 System.out.println( 362 "TWO STANDARDS: " + target + " ⟹ " + source + "; was " + old); 363 } else { 364 if (DEBUG) 365 System.out.println( 366 "TWO STANDARDS: " + target + " ⟹ " + old + ", was " + source); 367 } 368 } 369 } 370 return ImmutableMap.copyOf(unitToStandard); 371 } 372 373 @Override cloneAsThawed()374 public UnitConverter cloneAsThawed() { 375 throw new UnsupportedOperationException(); 376 } 377 378 public static final class ConversionInfo implements Comparable<ConversionInfo> { 379 public final Rational factor; 380 public final Rational offset; 381 public String special; 382 public boolean specialInverse; // only used with special 383 384 static final ConversionInfo IDENTITY = new ConversionInfo(Rational.ONE, Rational.ZERO); 385 ConversionInfo(Rational factor, Rational offset)386 public ConversionInfo(Rational factor, Rational offset) { 387 this.factor = factor; 388 this.offset = offset; 389 this.special = null; 390 this.specialInverse = false; 391 } 392 ConversionInfo(String special, boolean inverse)393 public ConversionInfo(String special, boolean inverse) { 394 this.factor = Rational.ZERO; // if ONE it will be treated as a base unit 395 this.offset = Rational.ZERO; 396 this.special = special; 397 this.specialInverse = inverse; 398 } 399 convert(Rational source)400 public Rational convert(Rational source) { 401 if (special != null) { 402 if (special.equals("beaufort")) { 403 return (specialInverse) 404 ? baseToScale(source, minMetersPerSecForBeaufort) 405 : scaleToBase(source, minMetersPerSecForBeaufort); 406 } 407 return source; 408 } 409 return source.multiply(factor).add(offset); 410 } 411 convertBackwards(Rational source)412 public Rational convertBackwards(Rational source) { 413 if (special != null) { 414 if (special.equals("beaufort")) { 415 return (specialInverse) 416 ? scaleToBase(source, minMetersPerSecForBeaufort) 417 : baseToScale(source, minMetersPerSecForBeaufort); 418 } 419 return source; 420 } 421 return source.subtract(offset).divide(factor); 422 } 423 424 private static final Rational[] minMetersPerSecForBeaufort = { 425 // minimum m/s values for each Bft value, plus an extra artificial value 426 // from table in Wikipedia, except for artificial value 427 // since 0 based, max Beaufort value is thus array dimension minus 2 428 Rational.of("0.0"), // 0 Bft 429 Rational.of("0.3"), // 1 430 Rational.of("1.6"), // 2 431 Rational.of("3.4"), // 3 432 Rational.of("5.5"), // 4 433 Rational.of("8.0"), // 5 434 Rational.of("10.8"), // 6 435 Rational.of("13.9"), // 7 436 Rational.of("17.2"), // 8 437 Rational.of("20.8"), // 9 438 Rational.of("24.5"), // 10 439 Rational.of("28.5"), // 11 440 Rational.of("32.7"), // 12 441 Rational.of("36.9"), // 13 442 Rational.of("41.4"), // 14 443 Rational.of("46.1"), // 15 444 Rational.of("51.1"), // 16 445 Rational.of("55.8"), // 17 446 Rational.of("61.4"), // artificial end of range 17 to give reasonable midpoint 447 }; 448 scaleToBase(Rational scaleValue, Rational[] minBaseForScaleValues)449 private Rational scaleToBase(Rational scaleValue, Rational[] minBaseForScaleValues) { 450 BigInteger scaleRound = scaleValue.abs().add(Rational.of(1, 2)).floor(); 451 BigInteger scaleMax = BigInteger.valueOf(minBaseForScaleValues.length - 2); 452 if (scaleRound.compareTo(scaleMax) > 0) { 453 scaleRound = scaleMax; 454 } 455 int scaleIndex = scaleRound.intValue(); 456 // Return midpont of range (the final range uses an articial end to produce reasonable 457 // midpoint) 458 return minBaseForScaleValues[scaleIndex] 459 .add(minBaseForScaleValues[scaleIndex + 1]) 460 .divide(Rational.TWO); 461 } 462 baseToScale(Rational baseValue, Rational[] minBaseForScaleValues)463 private Rational baseToScale(Rational baseValue, Rational[] minBaseForScaleValues) { 464 int scaleIndex = Arrays.binarySearch(minBaseForScaleValues, baseValue.abs()); 465 if (scaleIndex < 0) { 466 // since out first array entry is 0, this value will always be -2 or less 467 scaleIndex = -scaleIndex - 2; 468 } 469 int scaleMax = minBaseForScaleValues.length - 2; 470 if (scaleIndex > scaleMax) { 471 scaleIndex = scaleMax; 472 } 473 return Rational.of(scaleIndex); 474 } 475 invert()476 public ConversionInfo invert() { 477 if (special != null) { 478 return new ConversionInfo(special, !specialInverse); 479 } 480 Rational factor2 = factor.reciprocal(); 481 Rational offset2 = 482 offset.equals(Rational.ZERO) ? Rational.ZERO : offset.divide(factor).negate(); 483 return new ConversionInfo(factor2, offset2); 484 // TODO fix reciprocal 485 } 486 487 @Override toString()488 public String toString() { 489 return toString("x"); 490 } 491 toString(String unit)492 public String toString(String unit) { 493 if (special != null) { 494 return "special" + (specialInverse ? "inv" : "") + ":" + special + "(" + unit + ")"; 495 } 496 return factor.toString(FormatStyle.formatted) 497 + " * " 498 + unit 499 + (offset.equals(Rational.ZERO) 500 ? "" 501 : (offset.compareTo(Rational.ZERO) < 0 ? " - " : " + ") 502 + offset.abs().toString(FormatStyle.formatted)); 503 } 504 toDecimal()505 public String toDecimal() { 506 return toDecimal("x"); 507 } 508 toDecimal(String unit)509 public String toDecimal(String unit) { 510 if (special != null) { 511 return "special" + (specialInverse ? "inv" : "") + ":" + special + "(" + unit + ")"; 512 } 513 return factor.toBigDecimal(MathContext.DECIMAL64) 514 + " * " 515 + unit 516 + (offset.equals(Rational.ZERO) 517 ? "" 518 : (offset.compareTo(Rational.ZERO) < 0 ? " - " : " + ") 519 + offset.toBigDecimal(MathContext.DECIMAL64).abs()); 520 } 521 522 @Override compareTo(ConversionInfo o)523 public int compareTo(ConversionInfo o) { 524 // All specials sort at the end 525 int diff; 526 if (special != null) { 527 if (o.special == null) { 528 return 1; // This is special, other is not 529 } 530 // Both are special check names 531 if (0 != (diff = special.compareTo(o.special))) { 532 return diff; 533 } 534 // Among specials with the same name, inverses sort later 535 if (specialInverse != o.specialInverse) { 536 return (specialInverse) ? 1 : -1; 537 } 538 return 0; 539 } 540 if (o.special != null) { 541 return -1; // This is not special, other is 542 } 543 // Neither this nor other is special 544 if (0 != (diff = factor.compareTo(o.factor))) { 545 return diff; 546 } 547 return offset.compareTo(o.offset); 548 } 549 550 @Override equals(Object obj)551 public boolean equals(Object obj) { 552 return 0 == compareTo((ConversionInfo) obj); 553 } 554 555 @Override hashCode()556 public int hashCode() { 557 return Objects.hash(factor, offset, (special == null) ? "" : special); 558 } 559 } 560 561 public static class Continuation implements Comparable<Continuation> { 562 public final List<String> remainder; 563 public final String result; 564 addIfNeeded(String source, Multimap<String, Continuation> data)565 public static void addIfNeeded(String source, Multimap<String, Continuation> data) { 566 List<String> sourceParts = BAR_SPLITTER.splitToList(source); 567 if (sourceParts.size() > 1) { 568 Continuation continuation = 569 new Continuation( 570 ImmutableList.copyOf(sourceParts.subList(1, sourceParts.size())), 571 source); 572 data.put(sourceParts.get(0), continuation); 573 } 574 } 575 Continuation(List<String> remainder, String source)576 public Continuation(List<String> remainder, String source) { 577 this.remainder = remainder; 578 this.result = source; 579 } 580 581 /** 582 * The ordering is designed to have longest continuation first so that matching works. 583 * Otherwise the ordering doesn't matter, so we just use the result. 584 */ 585 @Override compareTo(Continuation other)586 public int compareTo(Continuation other) { 587 int diff = other.remainder.size() - remainder.size(); 588 if (diff != 0) { 589 return diff; 590 } 591 return result.compareTo(other.result); 592 } 593 match(List<String> parts, final int startIndex)594 public boolean match(List<String> parts, final int startIndex) { 595 if (remainder.size() > parts.size() - startIndex) { 596 return false; 597 } 598 int i = startIndex; 599 for (String unitPart : remainder) { 600 if (!unitPart.equals(parts.get(i++))) { 601 return false; 602 } 603 } 604 return true; 605 } 606 607 @Override toString()608 public String toString() { 609 return remainder + " " + result; 610 } 611 } 612 UnitConverter( RationalParser rationalParser, Validity validity, Function<String, UnitIdComponentType> componentTypeData)613 public UnitConverter( 614 RationalParser rationalParser, 615 Validity validity, 616 Function<String, UnitIdComponentType> componentTypeData) { 617 this.rationalParser = rationalParser; 618 this.componentTypeData = componentTypeData; 619 620 // // we need to pass in the validity so it is for the same CLDR version as the 621 // converter 622 // Set<String> VALID_UNITS = 623 // validity.getStatusToCodes(LstrType.unit).get(Status.regular); 624 // Map<String,String> _SHORT_TO_LONG_ID = new LinkedHashMap<>(); 625 // for (String longUnit : VALID_UNITS) { 626 // int dashPos = longUnit.indexOf('-'); 627 // String coreUnit = longUnit.substring(dashPos+1); 628 // _SHORT_TO_LONG_ID.put(coreUnit, longUnit); 629 // } 630 // SHORT_TO_LONG_ID = ImmutableBiMap.copyOf(_SHORT_TO_LONG_ID); 631 } 632 addRaw( String source, String target, String factor, String offset, String special, String systems)633 public void addRaw( 634 String source, 635 String target, 636 String factor, 637 String offset, 638 String special, 639 String systems) { 640 ConversionInfo info; 641 if (special != null) { 642 info = new ConversionInfo(special, false); 643 if (factor != null || offset != null) { 644 throw new IllegalArgumentException( 645 "Cannot have factor or offset with special=" + special); 646 } 647 } else { 648 info = 649 new ConversionInfo( 650 factor == null ? Rational.ONE : rationalParser.parse(factor), 651 offset == null ? Rational.ZERO : rationalParser.parse(offset)); 652 } 653 Map<String, String> args = new LinkedHashMap<>(); 654 if (factor != null) { 655 args.put("factor", factor); 656 } 657 if (offset != null) { 658 args.put("offset", offset); 659 } 660 if (special != null) { 661 args.put("special", special); 662 } 663 664 addToSourceToTarget(source, target, info, args, systems); 665 } 666 667 public static class TargetInfo { 668 public final String target; 669 public final ConversionInfo unitInfo; 670 public final Map<String, String> inputParameters; 671 TargetInfo( String target, ConversionInfo unitInfo, Map<String, String> inputParameters)672 public TargetInfo( 673 String target, ConversionInfo unitInfo, Map<String, String> inputParameters) { 674 this.target = target; 675 this.unitInfo = unitInfo; 676 this.inputParameters = ImmutableMap.copyOf(inputParameters); 677 } 678 679 @Override toString()680 public String toString() { 681 return unitInfo + " (" + target + ")"; 682 } 683 formatOriginalSource(String source)684 public String formatOriginalSource(String source) { 685 StringBuilder result = 686 new StringBuilder() 687 .append("<convertUnit source='") 688 .append(source) 689 .append("' baseUnit='") 690 .append(target) 691 .append("'"); 692 for (Entry<String, String> entry : inputParameters.entrySet()) { 693 if (entry.getValue() != null) { 694 result.append(" " + entry.getKey() + "='" + entry.getValue() + "'"); 695 } 696 } 697 result.append("/>"); 698 // if (unitInfo.equals(UnitInfo.IDENTITY)) { 699 // result.append("\t<!-- IDENTICAL -->"); 700 // } else { 701 // result.append("\t<!-- ~") 702 // .append(unitInfo.toDecimal(target)) 703 // .append(" -->"); 704 // } 705 return result.toString(); 706 } 707 } 708 709 public class TargetInfoComparator implements Comparator<TargetInfo> { 710 @Override compare(TargetInfo o1, TargetInfo o2)711 public int compare(TargetInfo o1, TargetInfo o2) { 712 String quality1 = baseUnitToQuantity.get(o1.target); 713 String quality2 = baseUnitToQuantity.get(o2.target); 714 int diff; 715 if (0 != (diff = quantityComparator.compare(quality1, quality2))) { 716 return diff; 717 } 718 if (0 != (diff = o1.unitInfo.compareTo(o2.unitInfo))) { 719 return diff; 720 } 721 return o1.target.compareTo(o2.target); 722 } 723 } 724 addToSourceToTarget( String source, String target, ConversionInfo info, Map<String, String> inputParameters, String systems)725 private void addToSourceToTarget( 726 String source, 727 String target, 728 ConversionInfo info, 729 Map<String, String> inputParameters, 730 String systems) { 731 if (sourceToTargetInfo.isEmpty()) { 732 baseUnitToQuantity = ImmutableBiMap.copyOf(baseUnitToQuantity); 733 baseUnitToStatus = ImmutableMap.copyOf(baseUnitToStatus); 734 } else if (sourceToTargetInfo.containsKey(source)) { 735 throw new IllegalArgumentException("Duplicate source: " + source + ", " + target); 736 } 737 sourceToTargetInfo.put(source, new TargetInfo(target, info, inputParameters)); 738 String targetQuantity = baseUnitToQuantity.get(target); 739 if (targetQuantity == null) { 740 throw new IllegalArgumentException("No quantity for baseUnit: " + target); 741 } 742 quantityToSimpleUnits.put(targetQuantity, source); 743 if (systems != null) { 744 SPACE_SPLITTER 745 .splitToList(systems) 746 .forEach(x -> sourceToSystems.put(source, UnitSystem.valueOf(x))); 747 } 748 } 749 getQuantityComparator( Map<String, String> baseUnitToQuantity2, Map<String, String> baseUnitToStatus2)750 private MapComparator<String> getQuantityComparator( 751 Map<String, String> baseUnitToQuantity2, Map<String, String> baseUnitToStatus2) { 752 // We want to sort all the quantities so that we have a natural ordering within compound 753 // units. So kilowatt-hour, not hour-kilowatt. 754 Collection<String> values; 755 if (true) { 756 values = baseUnitToQuantity2.values(); 757 } else { 758 // For simple quantities, just use the ordering from baseUnitToStatus 759 MapComparator<String> simpleBaseUnitComparator = 760 new MapComparator<>(baseUnitToStatus2.keySet()).freeze(); 761 // For non-symbol quantities, use the ordering of the UnitIds 762 Map<UnitId, String> unitIdToQuantity = new TreeMap<>(); 763 for (Entry<String, String> buq : baseUnitToQuantity2.entrySet()) { 764 UnitId uid = 765 new UnitId(simpleBaseUnitComparator).add(buq.getKey(), true, 1).freeze(); 766 unitIdToQuantity.put(uid, buq.getValue()); 767 } 768 // System.out.println(Joiner.on("\n").join(unitIdToQuantity.values())); 769 values = unitIdToQuantity.values(); 770 } 771 if (DEBUG) System.out.println(values); 772 return new MapComparator<>(values).freeze(); 773 } 774 canConvertBetween(String unit)775 public Set<String> canConvertBetween(String unit) { 776 TargetInfo targetInfo = sourceToTargetInfo.get(unit); 777 if (targetInfo == null) { 778 return Collections.emptySet(); 779 } 780 String quantity = baseUnitToQuantity.get(targetInfo.target); 781 return getSimpleUnits(quantity); 782 } 783 getSimpleUnits(String quantity)784 public Set<String> getSimpleUnits(String quantity) { 785 return ImmutableSet.copyOf(quantityToSimpleUnits.get(quantity)); 786 } 787 canConvert()788 public Set<String> canConvert() { 789 return sourceToTargetInfo.keySet(); 790 } 791 792 /** Converts between units, but ONLY if they are both base units */ convertDirect(Rational source, String sourceUnit, String targetUnit)793 public Rational convertDirect(Rational source, String sourceUnit, String targetUnit) { 794 if (sourceUnit.equals(targetUnit)) { 795 return source; 796 } 797 TargetInfo toPivotInfo = sourceToTargetInfo.get(sourceUnit); 798 if (toPivotInfo == null) { 799 return Rational.NaN; 800 } 801 TargetInfo fromPivotInfo = sourceToTargetInfo.get(targetUnit); 802 if (fromPivotInfo == null) { 803 return Rational.NaN; 804 } 805 if (!toPivotInfo.target.equals(fromPivotInfo.target)) { 806 return Rational.NaN; 807 } 808 Rational toPivot = toPivotInfo.unitInfo.convert(source); 809 Rational fromPivot = fromPivotInfo.unitInfo.convertBackwards(toPivot); 810 return fromPivot; 811 } 812 813 // TODO fix to guarantee single mapping 814 getUnitInfo(String sourceUnit, Output<String> baseUnit)815 public ConversionInfo getUnitInfo(String sourceUnit, Output<String> baseUnit) { 816 if (isBaseUnit(sourceUnit)) { 817 baseUnit.value = sourceUnit; 818 return ConversionInfo.IDENTITY; 819 } 820 TargetInfo targetToInfo = sourceToTargetInfo.get(sourceUnit); 821 if (targetToInfo == null) { 822 return null; 823 } 824 baseUnit.value = targetToInfo.target; 825 return targetToInfo.unitInfo; 826 } 827 getBaseUnit(String simpleUnit)828 public String getBaseUnit(String simpleUnit) { 829 TargetInfo targetToInfo = sourceToTargetInfo.get(simpleUnit); 830 if (targetToInfo == null) { 831 return null; 832 } 833 return targetToInfo.target; 834 } 835 836 /** 837 * Return the standard unit, eg newton for kilogram-meter-per-square-second 838 * 839 * @param simpleUnit 840 * @return 841 */ getStandardUnit(String unit)842 public String getStandardUnit(String unit) { 843 Output<String> metricUnit = new Output<>(); 844 parseUnitId(unit, metricUnit, false); 845 String result = sourceToStandard.get(metricUnit.value); 846 if (result == null) { 847 UnitId mUnit = createUnitId(metricUnit.value); 848 mUnit = mUnit.resolve(); 849 result = sourceToStandard.get(mUnit.toString()); 850 if (result == null) { 851 mUnit = mUnit.getReciprocal(); 852 result = sourceToStandard.get(mUnit.toString()); 853 if (result != null) { 854 result = "per-" + result; 855 } 856 } 857 } 858 return result == null ? metricUnit.value : result; 859 } 860 861 /** 862 * Reduces a unit, eg square-meter-per-meter-second ==> meter-per-second 863 * 864 * @param unit 865 * @return 866 */ getReducedUnit(String unit)867 public String getReducedUnit(String unit) { 868 UnitId mUnit = createUnitId(unit); 869 mUnit = mUnit.resolve(); 870 return mUnit.toString(); 871 } 872 getSpecialBaseUnit(String quantity, Set<UnitSystem> unitSystem)873 public String getSpecialBaseUnit(String quantity, Set<UnitSystem> unitSystem) { 874 if (unitSystem.contains(UnitSystem.ussystem) || unitSystem.contains(UnitSystem.uksystem)) { 875 switch (quantity) { 876 case "volume": 877 return unitSystem.contains(UnitSystem.uksystem) ? "gallon-imperial" : "gallon"; 878 case "mass": 879 return "pound"; 880 case "length": 881 return "foot"; 882 case "area": 883 return "square-foot"; 884 } 885 } 886 return null; 887 } 888 889 // unit constants are positive integers, optionally using positive exponents 890 static final Pattern CONSTANT = Pattern.compile("[0-9]+([eE][0-9]+)?"); 891 892 /** 893 * Takes a derived unit id, and produces the equivalent derived base unit id and UnitInfo to 894 * convert to it 895 * 896 * @author markdavis 897 * @param showYourWork TODO 898 */ parseUnitId( String derivedUnit, Output<String> metricUnit, boolean showYourWork)899 public ConversionInfo parseUnitId( 900 String derivedUnit, Output<String> metricUnit, boolean showYourWork) { 901 // First check whether we are dealing with a special mapping 902 Output<String> testBaseUnit = new Output<>(); 903 ConversionInfo testInfo = getUnitInfo(derivedUnit, testBaseUnit); 904 if (testInfo != null && testInfo.special != null) { 905 metricUnit.value = testBaseUnit.value; 906 return new ConversionInfo(testInfo.special, testInfo.specialInverse); 907 } 908 // Not a special mapping, proceed as usual 909 metricUnit.value = null; 910 911 UnitId outputUnit = new UnitId(UNIT_COMPARATOR); 912 Rational numerator = Rational.ONE; 913 Rational denominator = Rational.ONE; 914 boolean inNumerator = true; 915 int power = 1; 916 917 Output<Rational> deprefix = new Output<>(); 918 Rational offset = Rational.ZERO; 919 int countUnits = 0; 920 // We need to pass in componentTypeData because we may be called while reading 921 // the data for SupplementalDataInfo; 922 UnitParser up = new UnitParser(componentTypeData).set(derivedUnit); 923 Matcher constantMatcher = CONSTANT.matcher(""); 924 925 for (Iterator<String> upi = With.toIterator(up); upi.hasNext(); ) { 926 String unit = upi.next(); 927 ++countUnits; 928 if (constantMatcher.reset(unit).matches()) { 929 Rational constant = 930 Rational.of(new BigDecimal(unit)); // guaranteed to have denominator = ONE 931 if (inNumerator) { 932 numerator = numerator.multiply(constant); 933 } else { 934 denominator = denominator.multiply(constant); 935 } 936 } else if (unit.equals("square")) { 937 if (power != 1) { 938 throw new IllegalArgumentException("Can't have power of " + unit); 939 } 940 power = 2; 941 if (showYourWork) 942 System.out.println( 943 showRational("\t " + unit + ": ", Rational.of(power), "power")); 944 } else if (unit.equals("cubic")) { 945 if (power != 1) { 946 throw new IllegalArgumentException("Can't have power of " + unit); 947 } 948 power = 3; 949 if (showYourWork) 950 System.out.println( 951 showRational("\t " + unit + ": ", Rational.of(power), "power")); 952 } else if (unit.startsWith("pow")) { 953 if (power != 1) { 954 throw new IllegalArgumentException("Can't have power of " + unit); 955 } 956 power = Integer.parseInt(unit.substring(3)); 957 if (showYourWork) 958 System.out.println( 959 showRational("\t " + unit + ": ", Rational.of(power), "power")); 960 } else if (unit.equals("per")) { 961 if (power != 1) { 962 throw new IllegalArgumentException("Can't have power of per"); 963 } 964 if (showYourWork && inNumerator) System.out.println("\tper"); 965 inNumerator = false; // ignore multiples 966 // } else if ('9' >= unit.charAt(0)) { 967 // if (power != 1) { 968 // throw new IllegalArgumentException("Can't have power of " + 969 // unit); 970 // } 971 // Rational factor = Rational.of(Integer.parseInt(unit)); 972 // if (inNumerator) { 973 // numerator = numerator.multiply(factor); 974 // } else { 975 // denominator = denominator.multiply(factor); 976 // } 977 } else { 978 // kilo etc. 979 unit = stripPrefix(unit, deprefix); 980 if (showYourWork) { 981 if (!deprefix.value.equals(Rational.ONE)) { 982 System.out.println(showRational("\tprefix: ", deprefix.value, unit)); 983 } else { 984 System.out.println("\t" + unit); 985 } 986 } 987 988 Rational value = deprefix.value; 989 if (!isSimpleBaseUnit(unit)) { 990 TargetInfo info = sourceToTargetInfo.get(unit); 991 if (info == null) { 992 if (showYourWork) System.out.println("\t⟹ no conversion for: " + unit); 993 return null; // can't convert 994 } 995 String baseUnit = info.target; 996 997 value = 998 (info.unitInfo.special == null) 999 ? info.unitInfo.factor.multiply(value) 1000 : info.unitInfo.convert(value); 1001 // if (showYourWork && !info.unitInfo.factor.equals(Rational.ONE)) 1002 // System.out.println(showRational("\tfactor: ", info.unitInfo.factor, 1003 // baseUnit)); 1004 // Special handling for offsets. We disregard them if there are any other units. 1005 if (countUnits == 1 && !upi.hasNext()) { 1006 offset = info.unitInfo.offset; 1007 if (showYourWork && !info.unitInfo.offset.equals(Rational.ZERO)) 1008 System.out.println( 1009 showRational("\toffset: ", info.unitInfo.offset, baseUnit)); 1010 } 1011 unit = baseUnit; 1012 } 1013 for (int p = 1; p <= power; ++p) { 1014 String title = ""; 1015 if (value.equals(Rational.ONE)) { 1016 if (showYourWork) System.out.println("\t(already base unit)"); 1017 continue; 1018 } else if (inNumerator) { 1019 numerator = numerator.multiply(value); 1020 title = "\t× "; 1021 } else { 1022 denominator = denominator.multiply(value); 1023 title = "\t÷ "; 1024 } 1025 if (showYourWork) 1026 System.out.println( 1027 showRational("\t× ", value, " ⟹ " + unit) 1028 + "\t" 1029 + numerator.divide(denominator) 1030 + "\t" 1031 + numerator.divide(denominator).doubleValue()); 1032 } 1033 // create cleaned up target unitid 1034 outputUnit.add(unit, inNumerator, power); 1035 power = 1; 1036 } 1037 } 1038 metricUnit.value = outputUnit.toString(); 1039 return new ConversionInfo(numerator.divide(denominator), offset); 1040 } 1041 1042 /** Only for use for simple base unit comparison */ 1043 // Thus we do not need to handle specials here 1044 private class UnitComparator implements Comparator<String> { 1045 // TODO, use order in units.xml 1046 1047 @Override compare(String o1, String o2)1048 public int compare(String o1, String o2) { 1049 if (o1.equals(o2)) { 1050 return 0; 1051 } 1052 Output<Rational> deprefix1 = new Output<>(); 1053 o1 = stripPrefix(o1, deprefix1); 1054 TargetInfo targetAndInfo1 = sourceToTargetInfo.get(o1); 1055 String quantity1 = baseUnitToQuantity.get(targetAndInfo1.target); 1056 1057 Output<Rational> deprefix2 = new Output<>(); 1058 o2 = stripPrefix(o2, deprefix2); 1059 TargetInfo targetAndInfo2 = sourceToTargetInfo.get(o2); 1060 String quantity2 = baseUnitToQuantity.get(targetAndInfo2.target); 1061 1062 int diff; 1063 if (0 != (diff = quantityComparator.compare(quantity1, quantity2))) { 1064 return diff; 1065 } 1066 Rational factor1 = targetAndInfo1.unitInfo.factor.multiply(deprefix1.value); 1067 Rational factor2 = targetAndInfo2.unitInfo.factor.multiply(deprefix2.value); 1068 if (0 != (diff = factor1.compareTo(factor2))) { 1069 return diff; 1070 } 1071 return o1.compareTo(o2); 1072 } 1073 } 1074 1075 Comparator<String> UNIT_COMPARATOR = new UnitComparator(); 1076 static final Pattern TRAILING_ZEROS = Pattern.compile("0+$"); 1077 1078 /** Only handles the canonical units; no kilo-, only normalized, etc. */ 1079 // Thus we do not need to handle specials here 1080 // TODO: optimize 1081 // • the comparators don't have to be fields in this class; 1082 // it is not a static class, so they can be on the converter. 1083 // • We can cache the frozen UnitIds, avoiding the parse times 1084 1085 public class UnitId implements Freezable<UnitId>, Comparable<UnitId> { 1086 public Map<String, Integer> numUnitsToPowers; 1087 public Map<String, Integer> denUnitsToPowers; 1088 public EntrySetComparator<String, Integer> entrySetComparator; 1089 public Comparator<String> comparator; 1090 public Rational factor = Rational.ONE; 1091 1092 private boolean frozen = false; 1093 UnitId(Comparator<String> comparator)1094 private UnitId(Comparator<String> comparator) { 1095 this.comparator = comparator; 1096 numUnitsToPowers = new TreeMap<>(comparator); 1097 denUnitsToPowers = new TreeMap<>(comparator); 1098 entrySetComparator = 1099 new EntrySetComparator<String, Integer>(comparator, Comparator.naturalOrder()); 1100 } // 1101 getReciprocal()1102 public UnitId getReciprocal() { 1103 UnitId result = new UnitId(comparator); 1104 result.entrySetComparator = entrySetComparator; 1105 result.numUnitsToPowers = denUnitsToPowers; 1106 result.denUnitsToPowers = numUnitsToPowers; 1107 result.factor = factor.reciprocal(); 1108 return result; 1109 } 1110 add(String compoundUnit, boolean groupInNumerator, int groupPower)1111 private UnitId add(String compoundUnit, boolean groupInNumerator, int groupPower) { 1112 if (frozen) { 1113 throw new UnsupportedOperationException("Object is frozen."); 1114 } 1115 boolean inNumerator = true; 1116 int power = 1; 1117 // We need to pass in componentTypeData because we may be called while reading 1118 // the data for SupplementalDataInfo; 1119 UnitParser up = new UnitParser(componentTypeData).set(compoundUnit); 1120 Matcher constantMatcher = CONSTANT.matcher(""); 1121 1122 for (String unitPart : With.toIterable(up)) { 1123 switch (unitPart) { 1124 case "square": 1125 power = 2; 1126 break; 1127 case "cubic": 1128 power = 3; 1129 break; 1130 case "per": 1131 inNumerator = false; 1132 break; // sticky, ignore multiples 1133 default: 1134 if (constantMatcher.reset(unitPart).matches()) { 1135 Rational constant = 1136 Rational.of( 1137 new BigDecimal( 1138 unitPart)); // guaranteed to have denominator = 1139 // ONE 1140 if (inNumerator) { 1141 factor = factor.multiply(constant); 1142 } else { 1143 factor = factor.divide(constant); 1144 } 1145 } else if (unitPart.startsWith("pow")) { 1146 power = Integer.parseInt(unitPart.substring(3)); 1147 } else { 1148 Map<String, Integer> target = 1149 inNumerator == groupInNumerator 1150 ? numUnitsToPowers 1151 : denUnitsToPowers; 1152 Integer oldPower; 1153 try { 1154 oldPower = target.get(unitPart); 1155 } catch (Exception e) { 1156 throw new IllegalArgumentException( 1157 "Can't parse unitPart " + unitPart + " in " + compoundUnit, 1158 e); 1159 } 1160 // we multiply powers, so that weight-square-volume => 1161 // weight-pow4-length 1162 int newPower = groupPower * power + (oldPower == null ? 0 : oldPower); 1163 target.put(unitPart, newPower); 1164 power = 1; 1165 } 1166 } 1167 } 1168 return this; 1169 } 1170 1171 @Override toString()1172 public String toString() { 1173 StringBuilder builder = new StringBuilder(); 1174 boolean firstDenominator = true; 1175 for (int i = 1; i >= 0; --i) { // two passes, numerator then den. 1176 boolean positivePass = i > 0; 1177 if (positivePass && !factor.numerator.equals(BigInteger.ONE)) { 1178 builder.append(shortConstant(factor.numerator)); 1179 } 1180 1181 Map<String, Integer> target = positivePass ? numUnitsToPowers : denUnitsToPowers; 1182 for (Entry<String, Integer> entry : target.entrySet()) { 1183 String unit = entry.getKey(); 1184 int power = entry.getValue(); 1185 // NOTE: zero (eg one-per-one) gets counted twice 1186 if (builder.length() != 0) { 1187 builder.append('-'); 1188 } 1189 if (!positivePass) { 1190 if (firstDenominator) { 1191 firstDenominator = false; 1192 builder.append("per-"); 1193 if (!factor.denominator.equals(BigInteger.ONE)) { 1194 builder.append(shortConstant(factor.denominator)).append('-'); 1195 } 1196 } 1197 } 1198 switch (power) { 1199 case 1: 1200 break; 1201 case 2: 1202 builder.append("square-"); 1203 break; 1204 case 3: 1205 builder.append("cubic-"); 1206 break; 1207 default: 1208 if (power > 3) { 1209 builder.append("pow" + power + "-"); 1210 } else { 1211 throw new IllegalArgumentException("Unhandled power: " + power); 1212 } 1213 break; 1214 } 1215 builder.append(unit); 1216 } 1217 if (!positivePass 1218 && firstDenominator 1219 && !factor.denominator.equals(BigInteger.ONE)) { 1220 builder.append("-per-").append(shortConstant(factor.denominator)); 1221 } 1222 } 1223 return builder.toString(); 1224 } 1225 1226 /** 1227 * Return a string format. If larger than 7 digits, use 1eN format. 1228 * 1229 * @param source 1230 * @return 1231 */ shortConstant(BigInteger source)1232 public String shortConstant(BigInteger source) { 1233 // don't bother optimizing 1234 String result = source.toString(); 1235 if (result.length() < 8) { 1236 return result; 1237 } 1238 Matcher matcher = TRAILING_ZEROS.matcher(result); 1239 if (matcher.find()) { 1240 int zeroCount = matcher.group().length(); 1241 return result.substring(0, result.length() - zeroCount) + "e" + zeroCount; 1242 } 1243 return result; 1244 } 1245 toString( LocaleStringProvider resolvedFile, String width, String _pluralCategory, String caseVariant, Multimap<UnitPathType, String> partsUsed, boolean maximal)1246 public String toString( 1247 LocaleStringProvider resolvedFile, 1248 String width, 1249 String _pluralCategory, 1250 String caseVariant, 1251 Multimap<UnitPathType, String> partsUsed, 1252 boolean maximal) { 1253 if (partsUsed != null) { 1254 partsUsed.clear(); 1255 } 1256 // TODO handle factor!! 1257 String result = null; 1258 String numerator = null; 1259 String timesPattern = null; 1260 String placeholderPattern = null; 1261 Output<Integer> deprefix = new Output<>(); 1262 1263 PlaceholderLocation placeholderPosition = PlaceholderLocation.missing; 1264 Matcher placeholderMatcher = PLACEHOLDER.matcher(""); 1265 Output<String> unitPatternOut = new Output<>(); 1266 1267 PluralInfo pluralInfo = 1268 CLDRConfig.getInstance() 1269 .getSupplementalDataInfo() 1270 .getPlurals(resolvedFile.getLocaleID()); 1271 PluralRules pluralRules = pluralInfo.getPluralRules(); 1272 String singularPluralCategory = pluralRules.select(1d); 1273 final ULocale locale = new ULocale(resolvedFile.getLocaleID()); 1274 String fullPerPattern = null; 1275 int negCount = 0; 1276 1277 for (int i = 1; i >= 0; --i) { // two passes, numerator then den. 1278 boolean positivePass = i > 0; 1279 if (!positivePass) { 1280 switch (locale.toString()) { 1281 case "de": 1282 caseVariant = "accusative"; 1283 break; // German pro rule 1284 } 1285 numerator = result; // from now on, result ::= denominator 1286 result = null; 1287 } 1288 1289 Map<String, Integer> target = positivePass ? numUnitsToPowers : denUnitsToPowers; 1290 int unitsLeft = target.size(); 1291 for (Entry<String, Integer> entry : target.entrySet()) { 1292 String possiblyPrefixedUnit = entry.getKey(); 1293 String unit = stripPrefixPower(possiblyPrefixedUnit, deprefix); 1294 String genderVariant = 1295 UnitPathType.gender.getTrans( 1296 resolvedFile, "long", unit, null, null, null, partsUsed); 1297 1298 int power = entry.getValue(); 1299 unitsLeft--; 1300 String pluralCategory = 1301 unitsLeft == 0 && positivePass 1302 ? _pluralCategory 1303 : singularPluralCategory; 1304 1305 if (!positivePass) { 1306 if (maximal && 0 == negCount++) { // special case exact match for per form, 1307 // and no previous result 1308 if (true) { 1309 throw new UnsupportedOperationException( 1310 "not yet implemented fully"); 1311 } 1312 String fullUnit; 1313 switch (power) { 1314 case 1: 1315 fullUnit = unit; 1316 break; 1317 case 2: 1318 fullUnit = "square-" + unit; 1319 break; 1320 case 3: 1321 fullUnit = "cubic-" + unit; 1322 break; 1323 default: 1324 throw new IllegalArgumentException("powers > 3 not supported"); 1325 } 1326 fullPerPattern = 1327 UnitPathType.perUnit.getTrans( 1328 resolvedFile, 1329 width, 1330 fullUnit, 1331 _pluralCategory, 1332 caseVariant, 1333 genderVariant, 1334 partsUsed); 1335 // if there is a special form, we'll use it 1336 if (fullPerPattern != null) { 1337 continue; 1338 } 1339 } 1340 } 1341 1342 // handle prefix, like kilo- 1343 String prefixPattern = null; 1344 if (deprefix.value != 1) { 1345 prefixPattern = 1346 UnitPathType.prefix.getTrans( 1347 resolvedFile, 1348 width, 1349 "10p" + deprefix.value, 1350 _pluralCategory, 1351 caseVariant, 1352 genderVariant, 1353 partsUsed); 1354 } 1355 1356 // get the core pattern. Detect and remove the the placeholder (and surrounding 1357 // spaces) 1358 String unitPattern = 1359 UnitPathType.unit.getTrans( 1360 resolvedFile, 1361 width, 1362 unit, 1363 pluralCategory, 1364 caseVariant, 1365 genderVariant, 1366 partsUsed); 1367 if (unitPattern == null) { 1368 return null; // unavailable 1369 } 1370 // we are set up for 2 kinds of placeholder patterns for units. {0}\s?stuff or 1371 // stuff\s?{0}, or nothing(Eg Arabic) 1372 placeholderPosition = 1373 extractUnit(placeholderMatcher, unitPattern, unitPatternOut); 1374 if (placeholderPosition == PlaceholderLocation.middle) { 1375 return null; // signal we can't handle, but shouldn't happen with 1376 // well-formed data. 1377 } else if (placeholderPosition != PlaceholderLocation.missing) { 1378 unitPattern = unitPatternOut.value; 1379 placeholderPattern = placeholderMatcher.group(); 1380 } 1381 1382 // we have all the pieces, so build it up 1383 if (prefixPattern != null) { 1384 unitPattern = combineLowercasing(locale, width, prefixPattern, unitPattern); 1385 } 1386 1387 String powerPattern = null; 1388 switch (power) { 1389 case 1: 1390 break; 1391 case 2: 1392 powerPattern = 1393 UnitPathType.power.getTrans( 1394 resolvedFile, 1395 width, 1396 "power2", 1397 pluralCategory, 1398 caseVariant, 1399 genderVariant, 1400 partsUsed); 1401 break; 1402 case 3: 1403 powerPattern = 1404 UnitPathType.power.getTrans( 1405 resolvedFile, 1406 width, 1407 "power3", 1408 pluralCategory, 1409 caseVariant, 1410 genderVariant, 1411 partsUsed); 1412 break; 1413 default: 1414 throw new IllegalArgumentException("No power pattern > 3: " + this); 1415 } 1416 1417 if (powerPattern != null) { 1418 unitPattern = combineLowercasing(locale, width, powerPattern, unitPattern); 1419 } 1420 1421 if (result != null) { 1422 if (timesPattern == null) { 1423 timesPattern = getTimesPattern(resolvedFile, width); 1424 } 1425 result = MessageFormat.format(timesPattern, result, unitPattern); 1426 } else { 1427 result = unitPattern; 1428 } 1429 } 1430 } 1431 1432 // if there is a fullPerPattern, then we use it instead of per pattern + first 1433 // denominator element 1434 if (fullPerPattern != null) { 1435 if (numerator != null) { 1436 numerator = MessageFormat.format(fullPerPattern, numerator); 1437 } else { 1438 numerator = fullPerPattern; 1439 placeholderPattern = null; 1440 } 1441 if (result != null) { 1442 if (timesPattern == null) { 1443 timesPattern = getTimesPattern(resolvedFile, width); 1444 } 1445 numerator = MessageFormat.format(timesPattern, numerator, result); 1446 } 1447 result = numerator; 1448 } else { 1449 // glue the two parts together, if we have two of them 1450 if (result == null) { 1451 result = numerator; 1452 } else { 1453 String perPattern = 1454 UnitPathType.per.getTrans( 1455 resolvedFile, 1456 width, 1457 null, 1458 _pluralCategory, 1459 caseVariant, 1460 null, 1461 partsUsed); 1462 if (numerator == null) { 1463 result = MessageFormat.format(perPattern, "", result).trim(); 1464 } else { 1465 result = MessageFormat.format(perPattern, numerator, result); 1466 } 1467 } 1468 } 1469 return addPlaceholder(result, placeholderPattern, placeholderPosition); 1470 } 1471 getTimesPattern( LocaleStringProvider resolvedFile, String width)1472 public String getTimesPattern( 1473 LocaleStringProvider resolvedFile, String width) { // TODO fix hack! 1474 if (HACK && "en".equals(resolvedFile.getLocaleID())) { 1475 return "{0}-{1}"; 1476 } 1477 String timesPatternPath = 1478 "//ldml/units/unitLength[@type=\"" 1479 + width 1480 + "\"]/compoundUnit[@type=\"times\"]/compoundUnitPattern"; 1481 return resolvedFile.getStringValue(timesPatternPath); 1482 } 1483 1484 @Override equals(Object obj)1485 public boolean equals(Object obj) { 1486 UnitId other = (UnitId) obj; 1487 return factor.equals(other.factor) & numUnitsToPowers.equals(other.numUnitsToPowers) 1488 && denUnitsToPowers.equals(other.denUnitsToPowers); 1489 } 1490 1491 @Override hashCode()1492 public int hashCode() { 1493 return Objects.hash(factor, numUnitsToPowers, denUnitsToPowers); 1494 } 1495 1496 @Override isFrozen()1497 public boolean isFrozen() { 1498 return frozen; 1499 } 1500 1501 @Override freeze()1502 public UnitId freeze() { 1503 frozen = true; 1504 numUnitsToPowers = ImmutableMap.copyOf(numUnitsToPowers); 1505 denUnitsToPowers = ImmutableMap.copyOf(denUnitsToPowers); 1506 return this; 1507 } 1508 1509 @Override cloneAsThawed()1510 public UnitId cloneAsThawed() { 1511 throw new UnsupportedOperationException(); 1512 } 1513 resolve()1514 public UnitId resolve() { 1515 UnitId result = new UnitId(UNIT_COMPARATOR); 1516 result.numUnitsToPowers.putAll(numUnitsToPowers); 1517 result.denUnitsToPowers.putAll(denUnitsToPowers); 1518 for (Entry<String, Integer> entry : numUnitsToPowers.entrySet()) { 1519 final String key = entry.getKey(); 1520 Integer denPower = denUnitsToPowers.get(key); 1521 if (denPower == null) { 1522 continue; 1523 } 1524 int power = entry.getValue() - denPower; 1525 if (power > 0) { 1526 result.numUnitsToPowers.put(key, power); 1527 result.denUnitsToPowers.remove(key); 1528 } else if (power < 0) { 1529 result.numUnitsToPowers.remove(key); 1530 result.denUnitsToPowers.put(key, -power); 1531 } else { // 0, so 1532 result.numUnitsToPowers.remove(key); 1533 result.denUnitsToPowers.remove(key); 1534 } 1535 } 1536 return result.freeze(); 1537 } 1538 1539 @Override compareTo(UnitId o)1540 public int compareTo(UnitId o) { 1541 int diff = 1542 compareEntrySets( 1543 numUnitsToPowers.entrySet(), 1544 o.numUnitsToPowers.entrySet(), 1545 entrySetComparator); 1546 if (diff != 0) return diff; 1547 diff = 1548 compareEntrySets( 1549 denUnitsToPowers.entrySet(), 1550 o.denUnitsToPowers.entrySet(), 1551 entrySetComparator); 1552 if (diff != 0) return diff; 1553 return factor.compareTo(o.factor); 1554 } 1555 1556 /** 1557 * Default rules Prefixes & powers: the gender of the whole is the same as the gender of the 1558 * operand. In pseudocode: gender(square, meter) = gender(meter) gender(kilo, meter) = 1559 * gender(meter) 1560 * 1561 * <p>Per: the gender of the whole is the gender of the numerator. If there is no numerator, 1562 * then the gender of the denominator gender(gram per meter) = gender(gram) 1563 * 1564 * <p>Times: the gender of the whole is the gender of the last operand gender(gram-meter) = 1565 * gender(gram) 1566 * 1567 * @param source 1568 * @param partsUsed 1569 * @return TODO: add parameter to short-circuit the lookup if the unit is not a compound. 1570 */ getGender( CLDRFile resolvedFile, Output<String> source, Multimap<UnitPathType, String> partsUsed)1571 public String getGender( 1572 CLDRFile resolvedFile, 1573 Output<String> source, 1574 Multimap<UnitPathType, String> partsUsed) { 1575 // will not be empty 1576 1577 GrammarDerivation gd = null; 1578 // Values power = gd.get(GrammaticalFeature.grammaticalGender, 1579 // CompoundUnitStructure.power); no data available yet 1580 // Values prefix = gd.get(GrammaticalFeature.grammaticalGender, 1581 // CompoundUnitStructure.prefix); 1582 1583 Map<String, Integer> determiner; 1584 if (numUnitsToPowers.isEmpty()) { 1585 determiner = denUnitsToPowers; 1586 } else if (denUnitsToPowers.isEmpty()) { 1587 determiner = numUnitsToPowers; 1588 } else { 1589 if (gd == null) { 1590 gd = 1591 SupplementalDataInfo.getInstance() 1592 .getGrammarDerivation(resolvedFile.getLocaleID()); 1593 } 1594 Values per = 1595 gd.get(GrammaticalFeature.grammaticalGender, CompoundUnitStructure.per); 1596 boolean useFirst = per.value0.equals("0"); 1597 determiner = 1598 useFirst 1599 ? numUnitsToPowers // otherwise use numerator if possible 1600 : denUnitsToPowers; 1601 // TODO add test that the value is 0 or 1, so that if it fails we know to upgrade 1602 // this code. 1603 } 1604 1605 Entry<String, Integer> bestMeasure; 1606 if (determiner.size() == 1) { 1607 bestMeasure = determiner.entrySet().iterator().next(); 1608 } else { 1609 if (gd == null) { 1610 gd = 1611 SupplementalDataInfo.getInstance() 1612 .getGrammarDerivation(resolvedFile.getLocaleID()); 1613 } 1614 Values times = 1615 gd.get(GrammaticalFeature.grammaticalGender, CompoundUnitStructure.times); 1616 boolean useFirst = times.value0.equals("0"); 1617 if (useFirst) { 1618 bestMeasure = determiner.entrySet().iterator().next(); 1619 } else { 1620 bestMeasure = null; // we know the determiner is not empty, but this makes the 1621 // compiler 1622 for (Entry<String, Integer> entry : determiner.entrySet()) { 1623 bestMeasure = entry; 1624 } 1625 } 1626 } 1627 String strippedUnit = stripPrefix(bestMeasure.getKey(), null); 1628 String gender = 1629 UnitPathType.gender.getTrans( 1630 resolvedFile, "long", strippedUnit, null, null, null, partsUsed); 1631 if (gender != null && source != null) { 1632 source.value = strippedUnit; 1633 } 1634 return gender; 1635 } 1636 times(UnitId id2)1637 public UnitId times(UnitId id2) { 1638 UnitId result = new UnitId(comparator); 1639 result.factor = factor.multiply(id2.factor); 1640 combine(numUnitsToPowers, id2.numUnitsToPowers, result.numUnitsToPowers); 1641 combine(denUnitsToPowers, id2.denUnitsToPowers, result.denUnitsToPowers); 1642 return result; 1643 } 1644 combine( Map<String, Integer> map1, Map<String, Integer> map2, Map<String, Integer> resultMap)1645 public void combine( 1646 Map<String, Integer> map1, 1647 Map<String, Integer> map2, 1648 Map<String, Integer> resultMap) { 1649 Set<String> units = Sets.union(map1.keySet(), map2.keySet()); 1650 for (String unit : units) { 1651 Integer int1 = map1.get(unit); 1652 Integer int2 = map2.get(unit); 1653 resultMap.put(unit, (int1 == null ? 0 : int1) + (int2 == null ? 0 : int2)); 1654 } 1655 } 1656 } 1657 1658 public enum PlaceholderLocation { 1659 before, 1660 middle, 1661 after, 1662 missing 1663 } 1664 addPlaceholder( String result, String placeholderPattern, PlaceholderLocation placeholderPosition)1665 public static String addPlaceholder( 1666 String result, String placeholderPattern, PlaceholderLocation placeholderPosition) { 1667 return placeholderPattern == null 1668 ? result 1669 : placeholderPosition == PlaceholderLocation.before 1670 ? placeholderPattern + result 1671 : result + placeholderPattern; 1672 } 1673 1674 /** 1675 * Returns the location of the placeholder. Call placeholderMatcher.group() after calling this 1676 * to get the placeholder. 1677 * 1678 * @param placeholderMatcher 1679 * @param unitPattern 1680 * @param unitPatternOut 1681 * @param before 1682 * @return 1683 */ extractUnit( Matcher placeholderMatcher, String unitPattern, Output<String> unitPatternOut)1684 public static PlaceholderLocation extractUnit( 1685 Matcher placeholderMatcher, String unitPattern, Output<String> unitPatternOut) { 1686 if (placeholderMatcher.reset(unitPattern).find()) { 1687 if (placeholderMatcher.start() == 0) { 1688 unitPatternOut.value = unitPattern.substring(placeholderMatcher.end()); 1689 return PlaceholderLocation.before; 1690 } else if (placeholderMatcher.end() == unitPattern.length()) { 1691 unitPatternOut.value = unitPattern.substring(0, placeholderMatcher.start()); 1692 return PlaceholderLocation.after; 1693 } else { 1694 unitPatternOut.value = unitPattern; 1695 return PlaceholderLocation.middle; 1696 } 1697 } else { 1698 unitPatternOut.value = unitPattern; 1699 return PlaceholderLocation.missing; 1700 } 1701 } 1702 combineLowercasing( final ULocale locale, String width, String prefixPattern, String unitPattern)1703 public static String combineLowercasing( 1704 final ULocale locale, String width, String prefixPattern, String unitPattern) { 1705 // catch special case, ZentiLiter 1706 if (width.equals("long") 1707 && !prefixPattern.contains(" {") 1708 && !prefixPattern.contains(" {")) { 1709 unitPattern = UCharacter.toLowerCase(locale, unitPattern); 1710 } 1711 unitPattern = MessageFormat.format(prefixPattern, unitPattern); 1712 return unitPattern; 1713 } 1714 1715 public static class EntrySetComparator<K extends Comparable<K>, V> 1716 implements Comparator<Entry<K, V>> { 1717 Comparator<K> kComparator; 1718 Comparator<V> vComparator; 1719 EntrySetComparator(Comparator<K> kComparator, Comparator<V> vComparator)1720 public EntrySetComparator(Comparator<K> kComparator, Comparator<V> vComparator) { 1721 this.kComparator = kComparator; 1722 this.vComparator = vComparator; 1723 } 1724 1725 @Override compare(Entry<K, V> o1, Entry<K, V> o2)1726 public int compare(Entry<K, V> o1, Entry<K, V> o2) { 1727 int diff = kComparator.compare(o1.getKey(), o2.getKey()); 1728 if (diff != 0) { 1729 return diff; 1730 } 1731 diff = vComparator.compare(o1.getValue(), o2.getValue()); 1732 if (diff != 0) { 1733 return diff; 1734 } 1735 return o1.getKey().compareTo(o2.getKey()); 1736 } 1737 } 1738 1739 public static <K extends Comparable<K>, V extends Comparable<V>, T extends Entry<K, V>> compareEntrySets(Collection<T> o1, Collection<T> o2, Comparator<T> comparator)1740 int compareEntrySets(Collection<T> o1, Collection<T> o2, Comparator<T> comparator) { 1741 Iterator<T> iterator1 = o1.iterator(); 1742 Iterator<T> iterator2 = o2.iterator(); 1743 while (true) { 1744 if (!iterator1.hasNext()) { 1745 return iterator2.hasNext() ? -1 : 0; 1746 } else if (!iterator2.hasNext()) { 1747 return 1; 1748 } 1749 T item1 = iterator1.next(); 1750 T item2 = iterator2.next(); 1751 int diff = comparator.compare(item1, item2); 1752 if (diff != 0) { 1753 return diff; 1754 } 1755 } 1756 } 1757 1758 private ConcurrentHashMap<String, UnitId> UNIT_ID = new ConcurrentHashMap<>(); 1759 1760 // TODO This is safe but should use regular cache createUnitId(String unit)1761 public final UnitId createUnitId(String unit) { 1762 UnitId result = UNIT_ID.get(unit); 1763 if (result == null) { 1764 result = new UnitId(UNIT_COMPARATOR).add(unit, true, 1).freeze(); 1765 UNIT_ID.put(unit, result); 1766 } 1767 return result; 1768 } 1769 isBaseUnit(String unit)1770 public boolean isBaseUnit(String unit) { 1771 return baseUnits.contains(unit); 1772 } 1773 isSimpleBaseUnit(String unit)1774 public boolean isSimpleBaseUnit(String unit) { 1775 return BASE_UNITS.contains(unit); 1776 } 1777 baseUnits()1778 public Set<String> baseUnits() { 1779 return baseUnits; 1780 } 1781 1782 // TODO change to TRIE if the performance isn't good enough, or restructure with regex 1783 // https://www.nist.gov/pml/owm/metric-si-prefixes 1784 public static final ImmutableMap<String, Integer> PREFIX_POWERS = 1785 ImmutableMap.<String, Integer>builder() 1786 .put("quecto", -30) 1787 .put("ronto", -27) 1788 .put("yocto", -24) 1789 .put("zepto", -21) 1790 .put("atto", -18) 1791 .put("femto", -15) 1792 .put("pico", -12) 1793 .put("nano", -9) 1794 .put("micro", -6) 1795 .put("milli", -3) 1796 .put("centi", -2) 1797 .put("deci", -1) 1798 .put("deka", 1) 1799 .put("hecto", 2) 1800 .put("kilo", 3) 1801 .put("mega", 6) 1802 .put("giga", 9) 1803 .put("tera", 12) 1804 .put("peta", 15) 1805 .put("exa", 18) 1806 .put("zetta", 21) 1807 .put("yotta", 24) 1808 .put("ronna", 27) 1809 .put("quetta", 30) 1810 .build(); 1811 1812 public static final ImmutableMap<String, Rational> PREFIXES; 1813 1814 static { 1815 Map<String, Rational> temp = new LinkedHashMap<>(); 1816 for (Entry<String, Integer> entry : PREFIX_POWERS.entrySet()) { entry.getKey()1817 temp.put(entry.getKey(), Rational.pow10(entry.getValue())); 1818 } 1819 PREFIXES = ImmutableMap.copyOf(temp); 1820 } 1821 1822 public static final Set<String> METRIC_TAKING_PREFIXES = 1823 ImmutableSet.of( 1824 "bit", "byte", "liter", "tonne", "degree", "celsius", "kelvin", "calorie", 1825 "bar"); 1826 public static final Set<String> METRIC_TAKING_BINARY_PREFIXES = ImmutableSet.of("bit", "byte"); 1827 1828 static final Set<String> SKIP_PREFIX = 1829 ImmutableSet.of("millimeter-ofhg", "kilogram", "kilogram-force"); 1830 1831 static final Rational RATIONAL1000 = Rational.of(1000); 1832 1833 /** 1834 * If there is no prefix, return the unit and ONE. If there is a prefix return the unit (with 1835 * prefix stripped) and the prefix factor 1836 */ stripPrefixCommon( String unit, Output<V> deprefix, Map<String, V> unitMap)1837 public static <V> String stripPrefixCommon( 1838 String unit, Output<V> deprefix, Map<String, V> unitMap) { 1839 if (SKIP_PREFIX.contains(unit)) { 1840 return unit; 1841 } 1842 1843 for (Entry<String, V> entry : unitMap.entrySet()) { 1844 String prefix = entry.getKey(); 1845 if (unit.startsWith(prefix)) { 1846 String result = unit.substring(prefix.length()); 1847 // We have to do a special hack for kilogram, but only for the Rational case. 1848 // The Integer case is used for name construction, so that is ok. 1849 final boolean isRational = deprefix != null && deprefix.value instanceof Rational; 1850 boolean isGramHack = isRational && result.equals("gram"); 1851 if (isGramHack) { 1852 result = "kilogram"; 1853 } 1854 if (deprefix != null) { 1855 deprefix.value = entry.getValue(); 1856 if (isGramHack) { 1857 final Rational ratValue = (Rational) deprefix.value; 1858 deprefix.value = (V) ratValue.divide(RATIONAL1000); 1859 } 1860 } 1861 return result; 1862 } 1863 } 1864 return unit; 1865 } 1866 stripPrefix(String unit, Output<Rational> deprefix)1867 public static String stripPrefix(String unit, Output<Rational> deprefix) { 1868 if (deprefix != null) { 1869 deprefix.value = Rational.ONE; 1870 } 1871 return stripPrefixCommon(unit, deprefix, PREFIXES); 1872 } 1873 stripPrefixPower(String unit, Output<Integer> deprefix)1874 public static String stripPrefixPower(String unit, Output<Integer> deprefix) { 1875 if (deprefix != null) { 1876 deprefix.value = 1; 1877 } 1878 return stripPrefixCommon(unit, deprefix, PREFIX_POWERS); 1879 } 1880 getBaseUnitToQuantity()1881 public BiMap<String, String> getBaseUnitToQuantity() { 1882 return (BiMap<String, String>) baseUnitToQuantity; 1883 } 1884 getQuantityFromUnit(String unit, boolean showYourWork)1885 public String getQuantityFromUnit(String unit, boolean showYourWork) { 1886 Output<String> metricUnit = new Output<>(); 1887 unit = fixDenormalized(unit); 1888 try { 1889 ConversionInfo unitInfo = parseUnitId(unit, metricUnit, showYourWork); 1890 return metricUnit.value == null ? null : getQuantityFromBaseUnit(metricUnit.value); 1891 } catch (Exception e) { 1892 System.out.println("Failed with " + unit + ", " + metricUnit + "\t" + e); 1893 return null; 1894 } 1895 } 1896 getQuantityFromBaseUnit(String baseUnit)1897 public String getQuantityFromBaseUnit(String baseUnit) { 1898 if (baseUnit == null) { 1899 throw new NullPointerException("baseUnit"); 1900 } 1901 String result = getQuantityFromBaseUnit2(baseUnit); 1902 if (result != null) { 1903 return result; 1904 } 1905 result = getQuantityFromBaseUnit2(reciprocalOf(baseUnit)); 1906 if (result != null) { 1907 result += "-inverse"; 1908 } 1909 return result; 1910 } 1911 getQuantityFromBaseUnit2(String baseUnit)1912 private String getQuantityFromBaseUnit2(String baseUnit) { 1913 String result = baseUnitToQuantity.get(baseUnit); 1914 if (result != null) { 1915 return result; 1916 } 1917 UnitId unitId = createUnitId(baseUnit); 1918 UnitId resolved = unitId.resolve(); 1919 return baseUnitToQuantity.get(resolved.toString()); 1920 } 1921 getSimpleUnits()1922 public Set<String> getSimpleUnits() { 1923 return sourceToTargetInfo.keySet(); 1924 } 1925 addAliases(Map<String, R2<List<String>, String>> tagToReplacement)1926 public void addAliases(Map<String, R2<List<String>, String>> tagToReplacement) { 1927 fixDenormalized = new TreeMap<>(); 1928 for (Entry<String, R2<List<String>, String>> entry : tagToReplacement.entrySet()) { 1929 final String badCode = entry.getKey(); 1930 final List<String> replacements = entry.getValue().get0(); 1931 fixDenormalized.put(badCode, replacements.iterator().next()); 1932 } 1933 fixDenormalized = ImmutableMap.copyOf(fixDenormalized); 1934 } 1935 getInternalConversionData()1936 public Map<String, TargetInfo> getInternalConversionData() { 1937 return sourceToTargetInfo; 1938 } 1939 getSourceToSystems()1940 public Multimap<String, UnitSystem> getSourceToSystems() { 1941 return sourceToSystems; 1942 } 1943 1944 public enum UnitSystem { // TODO convert getSystems and SupplementalDataInfo to use natively 1945 si, 1946 si_acceptable, 1947 metric, 1948 metric_adjacent, 1949 ussystem, 1950 uksystem, 1951 jpsystem, 1952 astronomical, 1953 person_age, 1954 other, 1955 prefixable; 1956 1957 public static final Set<UnitSystem> SiOrMetric = 1958 ImmutableSet.of( 1959 UnitSystem.metric, 1960 UnitSystem.si, 1961 UnitSystem.metric_adjacent, 1962 UnitSystem.si_acceptable); 1963 public static final Set<UnitSystem> ALL = ImmutableSet.copyOf(UnitSystem.values()); 1964 fromStringCollection(Collection<String> stringUnitSystems)1965 public static Set<UnitSystem> fromStringCollection(Collection<String> stringUnitSystems) { 1966 return stringUnitSystems.stream() 1967 .map(x -> UnitSystem.valueOf(x)) 1968 .collect(Collectors.toSet()); 1969 } 1970 1971 @Deprecated toStringSet(Collection<UnitSystem> stringUnitSystems)1972 public static Set<String> toStringSet(Collection<UnitSystem> stringUnitSystems) { 1973 return new LinkedHashSet<>( 1974 stringUnitSystems.stream().map(x -> x.toString()).collect(Collectors.toList())); 1975 } 1976 1977 private static final Joiner SLASH_JOINER = Joiner.on("/"); 1978 getSystemsDisplay(Set<UnitSystem> systems)1979 public static String getSystemsDisplay(Set<UnitSystem> systems) { 1980 List<String> result = new ArrayList<>(); 1981 for (UnitSystem system : systems) { 1982 switch (system) { 1983 case si_acceptable: 1984 case metric: 1985 case metric_adjacent: 1986 return ""; 1987 case ussystem: 1988 result.add("US"); 1989 break; 1990 case uksystem: 1991 result.add("UK"); 1992 break; 1993 case jpsystem: 1994 result.add("JP"); 1995 break; 1996 } 1997 } 1998 return result.isEmpty() ? "" : " (" + SLASH_JOINER.join(result) + ")"; 1999 } 2000 } 2001 getSystems(String unit)2002 public Set<String> getSystems(String unit) { 2003 return UnitSystem.toStringSet(getSystemsEnum(unit)); 2004 } 2005 getSystemsEnum(String unit)2006 public Set<UnitSystem> getSystemsEnum(String unit) { 2007 Set<UnitSystem> result = null; 2008 UnitId id = createUnitId(unit); 2009 2010 // we walk through all the units in the numerator and denominator, and keep the 2011 // *intersection* of the units. 2012 // So {ussystem} and {ussystem, uksystem} => ussystem 2013 // Special case: {metric_adjacent} intersect {metric} => {metric_adjacent}. 2014 // We do that by adding metric_adjacent to any set with metric, 2015 // then removing metric_adjacent if there is a metric. 2016 // Same for si_acceptable. 2017 main: 2018 for (Map<String, Integer> unitsToPowers : 2019 Arrays.asList(id.denUnitsToPowers, id.numUnitsToPowers)) { 2020 for (String rawSubunit : unitsToPowers.keySet()) { 2021 String subunit = UnitConverter.stripPrefix(rawSubunit, null); 2022 2023 Set<UnitSystem> systems = new TreeSet<>(sourceToSystems.get(subunit)); 2024 if (systems.contains(UnitSystem.metric)) { 2025 systems.add(UnitSystem.metric_adjacent); 2026 } 2027 if (systems.contains(UnitSystem.si)) { 2028 systems.add(UnitSystem.si_acceptable); 2029 } 2030 2031 if (result == null) { 2032 result = systems; // first setting 2033 if (!subunit.equals(rawSubunit)) { 2034 result.remove(UnitSystem.prefixable); 2035 } 2036 } else { 2037 result.retainAll(systems); 2038 result.remove(UnitSystem.prefixable); // remove if more than one 2039 } 2040 if (result.isEmpty()) { 2041 break main; 2042 } 2043 } 2044 } 2045 if (result == null || result.isEmpty()) { 2046 return ImmutableSet.of(UnitSystem.other); 2047 } 2048 if (result.contains(UnitSystem.metric)) { 2049 result.remove(UnitSystem.metric_adjacent); 2050 } 2051 if (result.contains(UnitSystem.si)) { 2052 result.remove(UnitSystem.si_acceptable); 2053 } 2054 2055 return ImmutableSet.copyOf(EnumSet.copyOf(result)); // the enum is to sort 2056 } 2057 2058 // private void addSystems(Set<String> result, String subunit) { 2059 // Collection<String> systems = sourceToSystems.get(subunit); 2060 // if (!systems.isEmpty()) { 2061 // result.addAll(systems); 2062 // } 2063 // } 2064 reciprocalOf(String value)2065 public String reciprocalOf(String value) { 2066 // quick version, input guaranteed to be normalized, if original is 2067 if (value.startsWith("per-")) { 2068 return value.substring(4); 2069 } 2070 int index = value.indexOf("-per-"); 2071 if (index < 0) { 2072 return "per-" + value; 2073 } 2074 return value.substring(index + 5) + "-per-" + value.substring(0, index); 2075 } 2076 parseRational(String source)2077 public Rational parseRational(String source) { 2078 return rationalParser.parse(source); 2079 } 2080 showRational(String title, Rational rational, String unit)2081 public String showRational(String title, Rational rational, String unit) { 2082 String doubleString = showRational2(rational, " = ", " ≅ "); 2083 final String endResult = title + rational + doubleString + (unit != null ? " " + unit : ""); 2084 return endResult; 2085 } 2086 showRational(Rational rational, String approximatePrefix)2087 public String showRational(Rational rational, String approximatePrefix) { 2088 String doubleString = showRational2(rational, "", approximatePrefix); 2089 return doubleString.isEmpty() ? rational.numerator.toString() : doubleString; 2090 } 2091 showRational2(Rational rational, String equalPrefix, String approximatePrefix)2092 public String showRational2(Rational rational, String equalPrefix, String approximatePrefix) { 2093 String doubleString = ""; 2094 if (!rational.denominator.equals(BigInteger.ONE)) { 2095 String doubleValue = 2096 String.valueOf(rational.toBigDecimal(MathContext.DECIMAL32).doubleValue()); 2097 Rational reverse = parseRational(doubleValue); 2098 doubleString = 2099 (reverse.equals(rational) ? equalPrefix : approximatePrefix) + doubleValue; 2100 } 2101 return doubleString; 2102 } 2103 convert( final Rational sourceValue, final String sourceUnitIn, final String targetUnit, boolean showYourWork)2104 public Rational convert( 2105 final Rational sourceValue, 2106 final String sourceUnitIn, 2107 final String targetUnit, 2108 boolean showYourWork) { 2109 if (showYourWork) { 2110 System.out.println( 2111 showRational("\nconvert:\t", sourceValue, sourceUnitIn) + " ⟹ " + targetUnit); 2112 } 2113 final String sourceUnit = fixDenormalized(sourceUnitIn); 2114 Output<String> sourceBase = new Output<>(); 2115 Output<String> targetBase = new Output<>(); 2116 ConversionInfo sourceConversionInfo = parseUnitId(sourceUnit, sourceBase, showYourWork); 2117 if (sourceConversionInfo == null) { 2118 if (showYourWork) System.out.println("! unknown unit: " + sourceUnit); 2119 return Rational.NaN; 2120 } 2121 Rational intermediateResult = sourceConversionInfo.convert(sourceValue); 2122 if (showYourWork) 2123 System.out.println( 2124 showRational("intermediate:\t", intermediateResult, sourceBase.value)); 2125 if (showYourWork) System.out.println("invert:\t" + targetUnit); 2126 ConversionInfo targetConversionInfo = parseUnitId(targetUnit, targetBase, showYourWork); 2127 if (targetConversionInfo == null) { 2128 if (showYourWork) System.out.println("! unknown unit: " + targetUnit); 2129 return Rational.NaN; 2130 } 2131 if (!sourceBase.value.equals(targetBase.value)) { 2132 // try resolving 2133 String sourceBaseFixed = createUnitId(sourceBase.value).resolve().toString(); 2134 String targetBaseFixed = createUnitId(targetBase.value).resolve().toString(); 2135 // try reciprocal 2136 if (!sourceBaseFixed.equals(targetBaseFixed)) { 2137 String reciprocalUnit = reciprocalOf(sourceBase.value); 2138 if (reciprocalUnit == null || !targetBase.value.equals(reciprocalUnit)) { 2139 if (showYourWork) 2140 System.out.println( 2141 "! incomparable units: " + sourceUnit + " and " + targetUnit); 2142 return Rational.NaN; 2143 } 2144 intermediateResult = intermediateResult.reciprocal(); 2145 if (showYourWork) 2146 System.out.println( 2147 showRational( 2148 " ⟹ 1/intermediate:\t", intermediateResult, reciprocalUnit)); 2149 } 2150 } 2151 Rational result = targetConversionInfo.convertBackwards(intermediateResult); 2152 if (showYourWork) System.out.println(showRational("target:\t", result, targetUnit)); 2153 return result; 2154 } 2155 fixDenormalized(String unit)2156 public String fixDenormalized(String unit) { 2157 String fixed = fixDenormalized.get(unit); 2158 return fixed == null ? unit : fixed; 2159 } 2160 getConstants()2161 public Map<String, Rational> getConstants() { 2162 return rationalParser.getConstants(); 2163 } 2164 getBaseUnitFromQuantity(String unitQuantity)2165 public String getBaseUnitFromQuantity(String unitQuantity) { 2166 boolean invert = false; 2167 if (unitQuantity.endsWith("-inverse")) { 2168 invert = true; 2169 unitQuantity = unitQuantity.substring(0, unitQuantity.length() - 8); 2170 } 2171 String bu = ((BiMap<String, String>) baseUnitToQuantity).inverse().get(unitQuantity); 2172 if (bu == null) { 2173 return null; 2174 } 2175 return invert ? reciprocalOf(bu) : bu; 2176 } 2177 getQuantities()2178 public Set<String> getQuantities() { 2179 return getBaseUnitToQuantity().inverse().keySet(); 2180 } 2181 2182 public enum UnitComplexity { 2183 simple, 2184 non_simple 2185 } 2186 2187 private ConcurrentHashMap<String, UnitComplexity> COMPLEXITY = new ConcurrentHashMap<>(); 2188 // TODO This is safe but should use regular cache 2189 getComplexity(String longOrShortId)2190 public UnitComplexity getComplexity(String longOrShortId) { 2191 UnitComplexity result = COMPLEXITY.get(longOrShortId); 2192 if (result == null) { 2193 String shortId; 2194 String longId = getLongId(longOrShortId); 2195 if (longId == null) { 2196 longId = longOrShortId; 2197 shortId = SHORT_TO_LONG_ID.inverse().get(longId); 2198 } else { 2199 shortId = longOrShortId; 2200 } 2201 UnitId uid = createUnitId(shortId); 2202 result = UnitComplexity.simple; 2203 2204 if (uid.numUnitsToPowers.size() != 1 || !uid.denUnitsToPowers.isEmpty()) { 2205 result = UnitComplexity.non_simple; 2206 } else { 2207 Output<Rational> deprefix = new Output<>(); 2208 for (Entry<String, Integer> entry : uid.numUnitsToPowers.entrySet()) { 2209 final String unitPart = entry.getKey(); 2210 UnitConverter.stripPrefix(unitPart, deprefix); 2211 if (!deprefix.value.equals(Rational.ONE) 2212 || !entry.getValue().equals(INTEGER_ONE)) { 2213 result = UnitComplexity.non_simple; 2214 break; 2215 } 2216 } 2217 if (result == UnitComplexity.simple) { 2218 for (Entry<String, Integer> entry : uid.denUnitsToPowers.entrySet()) { 2219 final String unitPart = entry.getKey(); 2220 UnitConverter.stripPrefix(unitPart, deprefix); 2221 if (!deprefix.value.equals(Rational.ONE)) { 2222 result = UnitComplexity.non_simple; 2223 break; 2224 } 2225 } 2226 } 2227 } 2228 COMPLEXITY.put(shortId, result); 2229 COMPLEXITY.put(longId, result); 2230 } 2231 return result; 2232 } 2233 isSimple(String x)2234 public boolean isSimple(String x) { 2235 return getComplexity(x) == UnitComplexity.simple; 2236 } 2237 getLongId(String shortUnitId)2238 public String getLongId(String shortUnitId) { 2239 return CldrUtility.ifNull(SHORT_TO_LONG_ID.get(shortUnitId), shortUnitId); 2240 } 2241 getLongIds(Iterable<String> shortUnitIds)2242 public Set<String> getLongIds(Iterable<String> shortUnitIds) { 2243 LinkedHashSet<String> result = new LinkedHashSet<>(); 2244 for (String longUnitId : shortUnitIds) { 2245 String shortId = SHORT_TO_LONG_ID.get(longUnitId); 2246 if (shortId != null) { 2247 result.add(shortId); 2248 } 2249 } 2250 return ImmutableSet.copyOf(result); 2251 } 2252 getShortId(String longUnitId)2253 public String getShortId(String longUnitId) { 2254 if (longUnitId == null) { 2255 return null; 2256 } 2257 String result = SHORT_TO_LONG_ID.inverse().get(longUnitId); 2258 if (result != null) { 2259 return result; 2260 } 2261 int dashPos = longUnitId.indexOf('-'); 2262 if (dashPos < 0) { 2263 return longUnitId; 2264 } 2265 String type = longUnitId.substring(0, dashPos); 2266 return LONG_PREFIXES.contains(type) ? longUnitId.substring(dashPos + 1) : longUnitId; 2267 } 2268 getShortIds(Iterable<String> longUnitIds)2269 public Set<String> getShortIds(Iterable<String> longUnitIds) { 2270 LinkedHashSet<String> result = new LinkedHashSet<>(); 2271 for (String longUnitId : longUnitIds) { 2272 String shortId = SHORT_TO_LONG_ID.inverse().get(longUnitId); 2273 if (shortId != null) { 2274 result.add(shortId); 2275 } 2276 } 2277 return ImmutableSet.copyOf(result); 2278 } 2279 getBaseUnitToStatus()2280 public Map<String, String> getBaseUnitToStatus() { 2281 return baseUnitToStatus; 2282 } 2283 2284 static final Rational LIMIT_UPPER_RELATED = Rational.of(10000); 2285 static final Rational LIMIT_LOWER_RELATED = LIMIT_UPPER_RELATED.reciprocal(); 2286 getRelatedExamples( String inputUnit, Set<UnitSystem> allowedSystems)2287 public Map<Rational, String> getRelatedExamples( 2288 String inputUnit, Set<UnitSystem> allowedSystems) { 2289 Set<String> others = new LinkedHashSet<>(canConvertBetween(inputUnit)); 2290 if (others.size() <= 1) { 2291 return Map.of(); 2292 } 2293 // add common units 2294 if (others.contains("meter")) { 2295 others.add("kilometer"); 2296 others.add("millimeter"); 2297 } else if (others.contains("liter")) { 2298 others.add("milliliter"); 2299 } 2300 // remove unusual units 2301 others.removeAll( 2302 Set.of( 2303 "point", 2304 "fathom", 2305 "carat", 2306 "grain", 2307 "slug", 2308 "drop", 2309 "pinch", 2310 "cup-metric", 2311 "dram", 2312 "jigger", 2313 "pint-metric", 2314 "bushel, barrel", 2315 "dunam", 2316 "rod", 2317 "chain", 2318 "furlong", 2319 "fortnight", 2320 "rankine", 2321 "kelvin", 2322 "calorie-it", 2323 "british-thermal-unit-it", 2324 "foodcalorie", 2325 "nautical-mile", 2326 "mile-scandinavian", 2327 "knot", 2328 "beaufort")); 2329 2330 Map<Rational, String> result = new TreeMap<>(Comparator.reverseOrder()); 2331 2332 // get metric 2333 Output<String> sourceBase = new Output<>(); 2334 ConversionInfo sourceConversionInfo = parseUnitId(inputUnit, sourceBase, false); 2335 String baseUnit = sourceBase.value; 2336 Rational baseUnitToInput = sourceConversionInfo.factor; 2337 2338 putIfInRange(result, baseUnit, baseUnitToInput); 2339 2340 // get similar IDs 2341 // TBD 2342 2343 // get nearby in same system, and in metric 2344 2345 for (UnitSystem system : allowedSystems) { 2346 if (system.equals(UnitSystem.si)) { 2347 continue; 2348 } 2349 String closestLess = null; 2350 Rational closestLessValue = Rational.NEGATIVE_INFINITY; 2351 String closestGreater = null; 2352 Rational closestGreaterValue = Rational.INFINITY; 2353 2354 // check all the units in this system, to find the nearest above,and the nearest below 2355 2356 for (String other : others) { 2357 if (other.equals(inputUnit) 2358 || other.endsWith("-person") 2359 || other.startsWith("100-")) { // skips 2360 continue; 2361 } 2362 Set<UnitSystem> otherSystems = getSystemsEnum(other); 2363 if (!otherSystems.contains(system)) { 2364 continue; 2365 } 2366 2367 sourceConversionInfo = parseUnitId(other, sourceBase, false); 2368 Rational otherValue = 2369 baseUnitToInput.multiply(sourceConversionInfo.factor.reciprocal()); 2370 2371 if (otherValue.compareTo(Rational.ONE) < 0) { 2372 if (otherValue.compareTo(closestLessValue) > 0) { 2373 closestLess = other; 2374 closestLessValue = otherValue; 2375 } 2376 } else { 2377 if (otherValue.compareTo(closestGreaterValue) < 0) { 2378 closestGreater = other; 2379 closestGreaterValue = otherValue; 2380 } 2381 } 2382 } 2383 putIfInRange(result, closestLess, closestLessValue); 2384 putIfInRange(result, closestGreater, closestGreaterValue); 2385 } 2386 2387 result.remove(Rational.ONE, inputUnit); // simplest to do here 2388 return result; 2389 } 2390 putIfInRange(Map<Rational, String> result, String baseUnit, Rational otherValue)2391 public void putIfInRange(Map<Rational, String> result, String baseUnit, Rational otherValue) { 2392 if (baseUnit != null 2393 && otherValue.compareTo(LIMIT_LOWER_RELATED) >= 0 2394 && otherValue.compareTo(LIMIT_UPPER_RELATED) <= 0) { 2395 if (baseUnitToQuantity.get(baseUnit) != null) { 2396 baseUnit = getStandardUnit(baseUnit); 2397 } 2398 result.put(otherValue, baseUnit); 2399 } 2400 } 2401 2402 static final Set<UnitSystem> NO_UK = 2403 Set.copyOf(Sets.difference(UnitSystem.ALL, Set.of(UnitSystem.uksystem))); 2404 static final Set<UnitSystem> NO_JP = 2405 Set.copyOf(Sets.difference(UnitSystem.ALL, Set.of(UnitSystem.jpsystem))); 2406 static final Set<UnitSystem> NO_JP_UK = 2407 Set.copyOf( 2408 Sets.difference( 2409 UnitSystem.ALL, Set.of(UnitSystem.jpsystem, UnitSystem.uksystem))); 2410 2411 /** 2412 * Customize the systems according to the locale 2413 * 2414 * @return 2415 */ getExampleUnitSystems(String locale)2416 public static Set<UnitSystem> getExampleUnitSystems(String locale) { 2417 String language = CLDRLocale.getInstance(locale).getLanguage(); 2418 switch (language) { 2419 case "ja": 2420 return NO_UK; 2421 case "en": 2422 return NO_JP; 2423 default: 2424 return NO_JP_UK; 2425 } 2426 } 2427 2428 /** 2429 * Resolve the unit if possible, eg gram-square-second-per-second ==> gram-second <br> 2430 * TODO handle complex units that don't match a simple quantity, eg 2431 * kilogram-ampere-per-meter-square-second => pascal-ampere 2432 */ resolve(String unit)2433 public String resolve(String unit) { 2434 UnitId unitId = createUnitId(unit); 2435 if (unitId == null) { 2436 return unit; 2437 } 2438 String resolved = unitId.resolve().toString(); 2439 return getStandardUnit(resolved.isBlank() ? unit : resolved); 2440 } 2441 format( final String languageTag, Rational outputAmount, final String unit, UnlocalizedNumberFormatter nf3)2442 public String format( 2443 final String languageTag, 2444 Rational outputAmount, 2445 final String unit, 2446 UnlocalizedNumberFormatter nf3) { 2447 final CLDRConfig config = CLDRConfig.getInstance(); 2448 Factory factory = config.getCldrFactory(); 2449 int pos = languageTag.indexOf("-u"); 2450 String localeBase = 2451 (pos < 0 ? languageTag : languageTag.substring(0, pos)).replace('-', '_'); 2452 CLDRFile localeFile = factory.make(localeBase, true); 2453 PluralRules pluralRules = 2454 config.getSupplementalDataInfo() 2455 .getPluralRules( 2456 localeBase, com.ibm.icu.text.PluralRules.PluralType.CARDINAL); 2457 String pluralCategory = pluralRules.select(outputAmount.doubleValue()); 2458 String path = 2459 UnitPathType.unit.getTranslationPath( 2460 localeFile, "long", unit, pluralCategory, "nominative", "neuter"); 2461 String pattern = localeFile.getStringValue(path); 2462 final ULocale uLocale = ULocale.forLanguageTag(languageTag); 2463 String cldrFormattedNumber = 2464 nf3.locale(uLocale).format(outputAmount.doubleValue()).toString(); 2465 return com.ibm.icu.text.MessageFormat.format(pattern, cldrFormattedNumber); 2466 } 2467 } 2468