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