1 package org.unicode.cldr.util; 2 3 import java.math.BigInteger; 4 import java.math.MathContext; 5 import java.text.MessageFormat; 6 import java.util.Arrays; 7 import java.util.Collection; 8 import java.util.Collections; 9 import java.util.Comparator; 10 import java.util.EnumSet; 11 import java.util.Iterator; 12 import java.util.LinkedHashMap; 13 import java.util.LinkedHashSet; 14 import java.util.List; 15 import java.util.Map; 16 import java.util.Map.Entry; 17 import java.util.Objects; 18 import java.util.Set; 19 import java.util.TreeMap; 20 import java.util.concurrent.ConcurrentHashMap; 21 import java.util.regex.Matcher; 22 import java.util.regex.Pattern; 23 import java.util.stream.Collectors; 24 25 import org.unicode.cldr.util.GrammarDerivation.CompoundUnitStructure; 26 import org.unicode.cldr.util.GrammarDerivation.Values; 27 import org.unicode.cldr.util.GrammarInfo.GrammaticalFeature; 28 import org.unicode.cldr.util.Rational.FormatStyle; 29 import org.unicode.cldr.util.Rational.RationalParser; 30 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo; 31 32 import com.google.common.base.Splitter; 33 import com.google.common.collect.BiMap; 34 import com.google.common.collect.ImmutableBiMap; 35 import com.google.common.collect.ImmutableList; 36 import com.google.common.collect.ImmutableMap; 37 import com.google.common.collect.ImmutableMultimap; 38 import com.google.common.collect.ImmutableSet; 39 import com.google.common.collect.ImmutableSet.Builder; 40 import com.google.common.collect.LinkedHashMultimap; 41 import com.google.common.collect.Multimap; 42 import com.google.common.collect.TreeMultimap; 43 import com.ibm.icu.impl.Row.R2; 44 import com.ibm.icu.lang.UCharacter; 45 import com.ibm.icu.text.PluralRules; 46 import com.ibm.icu.util.Freezable; 47 import com.ibm.icu.util.Output; 48 import com.ibm.icu.util.ULocale; 49 50 public class UnitConverter implements Freezable<UnitConverter> { 51 public static boolean DEBUG = false; 52 public static final Integer INTEGER_ONE = Integer.valueOf(1); 53 54 static final Splitter BAR_SPLITTER = Splitter.on('-'); 55 static final Splitter SPACE_SPLITTER = Splitter.on(' ').trimResults().omitEmptyStrings(); 56 57 public static final Set<String> UNTRANSLATED_UNIT_NAMES = ImmutableSet.of( 58 "portion", 59 "ofglucose", 60 "100-kilometer", 61 "ofhg"); 62 63 public static final Set<String> HACK_SKIP_UNIT_NAMES = ImmutableSet.of( 64 // skip dot because pixel is preferred 65 "dot-per-centimeter", 66 "dot-per-inch", 67 // skip because a component is not translated 68 "liter-per-100-kilometer", 69 "millimeter-ofhg", 70 "inch-ofhg"); 71 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, String> sourceToSystems = LinkedHashMultimap.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 90 private boolean frozen = false; 91 92 public TargetInfoComparator targetInfoComparator; 93 94 /** Warning: ordering is important; determines the normalized output */ 95 public static final Set<String> BASE_UNITS = ImmutableSet.of( 96 "candela", 97 "kilogram", 98 "meter", 99 "second", 100 "ampere", 101 "kelvin", 102 // non-SI 103 "year", 104 "bit", 105 "item", 106 "pixel", 107 "em", 108 "revolution", 109 "portion" 110 ); 111 addQuantityInfo(String baseUnit, String quantity, String status)112 public void addQuantityInfo(String baseUnit, String quantity, String status) { 113 if (baseUnitToQuantity.containsKey(baseUnit)) { 114 throw new IllegalArgumentException(); 115 } 116 baseUnitToQuantity.put(baseUnit, quantity); 117 if (status != null) { 118 baseUnitToStatus.put(baseUnit, status); 119 } 120 quantityToSimpleUnits.put(quantity, baseUnit); 121 } 122 123 public static final Set<String> BASE_UNIT_PARTS = ImmutableSet.<String>builder() 124 .add("per").add("square").add("cubic").addAll(BASE_UNITS) 125 .build(); 126 127 public static final Pattern PLACEHOLDER = Pattern.compile("[ \\u00A0\\u200E\\u200F\\u202F]*\\{0\\}[ \\u00A0\\u200E\\u200F\\u202F]*"); 128 public static final boolean HACK = true; 129 130 @Override isFrozen()131 public boolean isFrozen() { 132 return frozen; 133 } 134 135 @Override freeze()136 public UnitConverter freeze() { 137 if (!frozen) { 138 frozen = true; 139 rationalParser.freeze(); 140 sourceToTargetInfo = ImmutableMap.copyOf(sourceToTargetInfo); 141 sourceToStandard = buildSourceToStandard(); 142 quantityToSimpleUnits = ImmutableMultimap.copyOf(quantityToSimpleUnits); 143 quantityComparator = getQuantityComparator(baseUnitToQuantity, baseUnitToStatus); 144 145 sourceToSystems = ImmutableMultimap.copyOf(sourceToSystems); 146 // other fields are frozen earlier in processing 147 Builder<String> builder = ImmutableSet.<String>builder() 148 .addAll(BASE_UNITS); 149 for (TargetInfo s : sourceToTargetInfo.values()) { 150 builder.add(s.target); 151 } 152 baseUnits = builder.build(); 153 continuations = ImmutableMultimap.copyOf(continuations); 154 targetInfoComparator = new TargetInfoComparator(); 155 156 Map<String, UnitId> _idToUnitId = new TreeMap<>(); 157 for (Entry<String, String> shortAndLongId : SHORT_TO_LONG_ID.entrySet()) { 158 String shortId = shortAndLongId.getKey(); 159 String longId = shortAndLongId.getKey(); 160 UnitId uid = createUnitId(shortId).freeze(); 161 boolean doTest = false; 162 Output<Rational> deprefix = new Output<>(); 163 for (Entry<String, Integer> entry : uid.numUnitsToPowers.entrySet()) { 164 final String unitPart = entry.getKey(); 165 UnitConverter.stripPrefix(unitPart, deprefix ); 166 if (!deprefix.value.equals(Rational.ONE) || !entry.getValue().equals(INTEGER_ONE)) { 167 doTest = true; 168 break; 169 } 170 } 171 if (!doTest) { 172 for (Entry<String, Integer> entry : uid.denUnitsToPowers.entrySet()) { 173 final String unitPart = entry.getKey(); 174 UnitConverter.stripPrefix(unitPart, deprefix); 175 if (!deprefix.value.equals(Rational.ONE)) { 176 doTest = true; 177 break; 178 } 179 } 180 } 181 if (doTest) { 182 _idToUnitId.put(shortId, uid); 183 _idToUnitId.put(longId, uid); 184 } 185 } 186 idToUnitId = ImmutableMap.copyOf(_idToUnitId); 187 } 188 return this; 189 } 190 191 /** 192 * Return the 'standard unit' for the source. 193 * @return 194 */ buildSourceToStandard()195 private Map<String, String> buildSourceToStandard() { 196 Map<String, String> unitToStandard = new TreeMap<>(); 197 for (Entry<String, TargetInfo> entry : sourceToTargetInfo.entrySet()) { 198 String source = entry.getKey(); 199 TargetInfo targetInfo = entry.getValue(); 200 if (targetInfo.unitInfo.factor.equals(Rational.ONE) && targetInfo.unitInfo.offset.equals(Rational.ZERO)) { 201 final String target = targetInfo.target; 202 String old = unitToStandard.get(target); 203 if (old == null) { 204 unitToStandard.put(target, source); 205 if (DEBUG) System.out.println(target + " ⟹ " + source); 206 } else if (old.length() > source.length()) { 207 unitToStandard.put(target, source); 208 if (DEBUG) System.out.println("TWO STANDARDS: " + target + " ⟹ " + source + "; was " + old); 209 } else { 210 if (DEBUG) System.out.println("TWO STANDARDS: " + target + " ⟹ " + old + ", was " + source); 211 } 212 } 213 } 214 return ImmutableMap.copyOf(unitToStandard); 215 } 216 217 @Override cloneAsThawed()218 public UnitConverter cloneAsThawed() { 219 throw new UnsupportedOperationException(); 220 } 221 222 223 public static final class ConversionInfo implements Comparable<ConversionInfo> { 224 public final Rational factor; 225 public final Rational offset; 226 227 static final ConversionInfo IDENTITY = new ConversionInfo(Rational.ONE, Rational.ZERO); 228 ConversionInfo(Rational factor, Rational offset)229 public ConversionInfo(Rational factor, Rational offset) { 230 this.factor = factor; 231 this.offset = offset; 232 } 233 convert(Rational source)234 public Rational convert(Rational source) { 235 return source.multiply(factor).add(offset); 236 } 237 convertBackwards(Rational source)238 public Rational convertBackwards(Rational source) { 239 return source.subtract(offset).divide(factor); 240 } 241 invert()242 public ConversionInfo invert() { 243 Rational factor2 = factor.reciprocal(); 244 Rational offset2 = offset.equals(Rational.ZERO) ? Rational.ZERO : offset.divide(factor).negate(); 245 return new ConversionInfo(factor2, offset2); 246 // TODO fix reciprocal 247 } 248 249 @Override toString()250 public String toString() { 251 return toString("x"); 252 } toString(String unit)253 public String toString(String unit) { 254 return factor.toString(FormatStyle.simple) 255 + " * " + unit 256 + (offset.equals(Rational.ZERO) ? "" : 257 (offset.compareTo(Rational.ZERO) < 0 ? " - " : " - ") 258 + offset.abs().toString(FormatStyle.simple)); 259 } 260 toDecimal()261 public String toDecimal() { 262 return toDecimal("x"); 263 } toDecimal(String unit)264 public String toDecimal(String unit) { 265 return factor.toBigDecimal(MathContext.DECIMAL64) 266 + " * " + unit 267 + (offset.equals(Rational.ZERO) ? "" : 268 (offset.compareTo(Rational.ZERO) < 0 ? " - " : " - ") 269 + offset.toBigDecimal(MathContext.DECIMAL64).abs()); 270 } 271 272 @Override compareTo(ConversionInfo o)273 public int compareTo(ConversionInfo o) { 274 int diff; 275 if (0 != (diff = factor.compareTo(o.factor))) { 276 return diff; 277 } 278 return offset.compareTo(o.offset); 279 } 280 @Override equals(Object obj)281 public boolean equals(Object obj) { 282 return 0 == compareTo((ConversionInfo)obj); 283 } 284 @Override hashCode()285 public int hashCode() { 286 return Objects.hash(factor, offset); 287 } 288 } 289 290 public static class Continuation implements Comparable<Continuation> { 291 public final List<String> remainder; 292 public final String result; 293 addIfNeeded(String source, Multimap<String, Continuation> data)294 public static void addIfNeeded(String source, Multimap<String, Continuation> data) { 295 List<String> sourceParts = BAR_SPLITTER.splitToList(source); 296 if (sourceParts.size() > 1) { 297 Continuation continuation = new Continuation(ImmutableList.copyOf(sourceParts.subList(1, sourceParts.size())), source); 298 data.put(sourceParts.get(0), continuation); 299 } 300 } Continuation(List<String> remainder, String source)301 public Continuation(List<String> remainder, String source) { 302 this.remainder = remainder; 303 this.result = source; 304 } 305 /** 306 * The ordering is designed to have longest continuation first so that matching works. 307 * Otherwise the ordering doesn't matter, so we just use the result. 308 */ 309 @Override compareTo(Continuation other)310 public int compareTo(Continuation other) { 311 int diff = other.remainder.size() - remainder.size(); 312 if (diff != 0) { 313 return diff; 314 } 315 return result.compareTo(other.result); 316 } 317 match(List<String> parts, final int startIndex)318 public boolean match(List<String> parts, final int startIndex) { 319 if (remainder.size() > parts.size() - startIndex) { 320 return false; 321 } 322 int i = startIndex; 323 for (String unitPart : remainder) { 324 if (!unitPart.equals(parts.get(i++))) { 325 return false; 326 } 327 } 328 return true; 329 } 330 331 @Override toString()332 public String toString() { 333 return remainder + " " + result; 334 } 335 split(String derivedUnit, Multimap<String, Continuation> continuations)336 public static UnitIterator split(String derivedUnit, Multimap<String, Continuation> continuations) { 337 return new UnitIterator(derivedUnit, continuations); 338 } 339 340 public static class UnitIterator implements Iterable<String>, Iterator<String> { 341 final List<String> parts; 342 final Multimap<String, Continuation> continuations; 343 int nextIndex = 0; 344 UnitIterator(String derivedUnit, Multimap<String, Continuation> continuations)345 public UnitIterator(String derivedUnit, Multimap<String, Continuation> continuations) { 346 parts = BAR_SPLITTER.splitToList(derivedUnit); 347 this.continuations = continuations; 348 } 349 350 @Override hasNext()351 public boolean hasNext() { 352 return nextIndex < parts.size(); 353 } 354 peek()355 public String peek() { 356 return parts.size() <= nextIndex ? null : parts.get(nextIndex); 357 } 358 359 @Override next()360 public String next() { 361 String result = parts.get(nextIndex++); 362 Collection<Continuation> continuationOptions = continuations.get(result); 363 for (Continuation option : continuationOptions) { 364 if (option.match(parts, nextIndex)) { 365 nextIndex += option.remainder.size(); 366 return option.result; 367 } 368 } 369 return result; 370 } 371 372 @Override iterator()373 public UnitIterator iterator() { 374 return this; 375 } 376 377 } 378 } 379 UnitConverter(RationalParser rationalParser, Validity validity)380 public UnitConverter(RationalParser rationalParser, Validity validity) { 381 this.rationalParser = rationalParser; 382 // // we need to pass in the validity so it is for the same CLDR version as the converter 383 // Set<String> VALID_UNITS = validity.getStatusToCodes(LstrType.unit).get(Status.regular); 384 // Map<String,String> _SHORT_TO_LONG_ID = new LinkedHashMap<>(); 385 // for (String longUnit : VALID_UNITS) { 386 // int dashPos = longUnit.indexOf('-'); 387 // String coreUnit = longUnit.substring(dashPos+1); 388 // _SHORT_TO_LONG_ID.put(coreUnit, longUnit); 389 // } 390 // SHORT_TO_LONG_ID = ImmutableBiMap.copyOf(_SHORT_TO_LONG_ID); 391 } 392 addRaw(String source, String target, String factor, String offset, String systems)393 public void addRaw(String source, String target, String factor, String offset, String systems) { 394 ConversionInfo info = new ConversionInfo( 395 factor == null ? Rational.ONE : rationalParser.parse(factor), 396 offset == null ? Rational.ZERO : rationalParser.parse(offset)); 397 Map<String, String> args = new LinkedHashMap<>(); 398 if (factor != null) { 399 args.put("factor", factor); 400 } 401 if (offset != null) { 402 args.put("offset", offset); 403 } 404 405 addToSourceToTarget(source, target, info, args, systems); 406 Continuation.addIfNeeded(source, continuations); 407 } 408 409 public static class TargetInfo{ 410 public final String target; 411 public final ConversionInfo unitInfo; 412 public final Map<String, String> inputParameters; TargetInfo(String target, ConversionInfo unitInfo, Map<String, String> inputParameters)413 public TargetInfo(String target, ConversionInfo unitInfo, Map<String, String> inputParameters) { 414 this.target = target; 415 this.unitInfo = unitInfo; 416 this.inputParameters = ImmutableMap.copyOf(inputParameters); 417 } 418 @Override toString()419 public String toString() { 420 return unitInfo + " (" + target + ")"; 421 } formatOriginalSource(String source)422 public String formatOriginalSource(String source) { 423 StringBuilder result = new StringBuilder() 424 .append("<convertUnit source='") 425 .append(source) 426 .append("' baseUnit='") 427 .append(target) 428 .append("'") 429 ; 430 for (Entry<String, String> entry : inputParameters.entrySet()) { 431 if (entry.getValue() != null) { 432 result.append(" " + entry.getKey() + "='" + entry.getValue() + "'"); 433 } 434 } 435 result.append("/>"); 436 // if (unitInfo.equals(UnitInfo.IDENTITY)) { 437 // result.append("\t<!-- IDENTICAL -->"); 438 // } else { 439 // result.append("\t<!-- ~") 440 // .append(unitInfo.toDecimal(target)) 441 // .append(" -->"); 442 // } 443 return result.toString(); 444 } 445 } 446 public class TargetInfoComparator implements Comparator<TargetInfo> { 447 @Override compare(TargetInfo o1, TargetInfo o2)448 public int compare(TargetInfo o1, TargetInfo o2) { 449 String quality1 = baseUnitToQuantity.get(o1.target); 450 String quality2 = baseUnitToQuantity.get(o2.target); 451 int diff; 452 if (0 != (diff = quantityComparator.compare(quality1, quality2))) { 453 return diff; 454 } 455 if (0 != (diff = o1.unitInfo.compareTo(o2.unitInfo))) { 456 return diff; 457 } 458 return o1.target.compareTo(o2.target); 459 } 460 } 461 addToSourceToTarget(String source, String target, ConversionInfo info, Map<String, String> inputParameters, String systems)462 private void addToSourceToTarget(String source, String target, ConversionInfo info, 463 Map<String, String> inputParameters, String systems) { 464 if (sourceToTargetInfo.isEmpty()) { 465 baseUnitToQuantity = ImmutableBiMap.copyOf(baseUnitToQuantity); 466 baseUnitToStatus = ImmutableMap.copyOf(baseUnitToStatus); 467 } else if (sourceToTargetInfo.containsKey(source)) { 468 throw new IllegalArgumentException("Duplicate source: " + source + ", " + target); 469 } 470 sourceToTargetInfo.put(source, new TargetInfo(target, info, inputParameters)); 471 String targetQuantity = baseUnitToQuantity.get(target); 472 if (targetQuantity == null) { 473 throw new IllegalArgumentException("No quantity for baseUnit: " + target); 474 } 475 quantityToSimpleUnits.put(targetQuantity, source); 476 if (systems != null) { 477 sourceToSystems.putAll(source, SPACE_SPLITTER.split(systems)); 478 } 479 } 480 getQuantityComparator(Map<String, String> baseUnitToQuantity2, Map<String, String> baseUnitToStatus2)481 private Comparator<String> getQuantityComparator(Map<String, String> baseUnitToQuantity2, Map<String, String> baseUnitToStatus2) { 482 // We want to sort all the quantities so that we have a natural ordering within compound units. So kilowatt-hour, not hour-kilowatt. 483 Collection<String> values; 484 if (true) { 485 values = baseUnitToQuantity2.values(); 486 } else { 487 // For simple quantities, just use the ordering from baseUnitToStatus 488 MapComparator<String> simpleBaseUnitComparator = new MapComparator<>(baseUnitToStatus2.keySet()).freeze(); 489 // For non-symbol quantities, use the ordering of the UnitIds 490 Map<UnitId, String> unitIdToQuantity = new TreeMap<>(); 491 for (Entry<String, String> buq : baseUnitToQuantity2.entrySet()) { 492 UnitId uid = new UnitId(simpleBaseUnitComparator).add(continuations, buq.getKey(), true, 1).freeze(); 493 unitIdToQuantity.put(uid, buq.getValue()); 494 } 495 // System.out.println(Joiner.on("\n").join(unitIdToQuantity.values())); 496 values = unitIdToQuantity.values(); 497 } 498 if (DEBUG) System.out.println(values); 499 return new MapComparator<>(values).freeze(); 500 } 501 canConvertBetween(String unit)502 public Set<String> canConvertBetween(String unit) { 503 TargetInfo targetInfo = sourceToTargetInfo.get(unit); 504 if (targetInfo == null) { 505 return Collections.emptySet(); 506 } 507 String quantity = baseUnitToQuantity.get(targetInfo.target); 508 return ImmutableSet.copyOf(quantityToSimpleUnits.get(quantity)); 509 } 510 canConvert()511 public Set<String> canConvert() { 512 return sourceToTargetInfo.keySet(); 513 } 514 515 /** 516 * Converts between units, but ONLY if they are both base units 517 */ convertDirect(Rational source, String sourceUnit, String targetUnit)518 public Rational convertDirect(Rational source, String sourceUnit, String targetUnit) { 519 if (sourceUnit.equals(targetUnit)) { 520 return source; 521 } 522 TargetInfo toPivotInfo = sourceToTargetInfo.get(sourceUnit); 523 if (toPivotInfo == null) { 524 return Rational.NaN; 525 } 526 TargetInfo fromPivotInfo = sourceToTargetInfo.get(targetUnit); 527 if (fromPivotInfo == null) { 528 return Rational.NaN; 529 } 530 if (!toPivotInfo.target.equals(fromPivotInfo.target)) { 531 return Rational.NaN; 532 } 533 Rational toPivot = toPivotInfo.unitInfo.convert(source); 534 Rational fromPivot = fromPivotInfo.unitInfo.convertBackwards(toPivot); 535 return fromPivot; 536 } 537 538 // TODO fix to guarantee single mapping 539 getUnitInfo(String sourceUnit, Output<String> baseUnit)540 public ConversionInfo getUnitInfo(String sourceUnit, Output<String> baseUnit) { 541 if (isBaseUnit(sourceUnit)) { 542 baseUnit.value = sourceUnit; 543 return ConversionInfo.IDENTITY; 544 } 545 TargetInfo targetToInfo = sourceToTargetInfo.get(sourceUnit); 546 if (targetToInfo == null) { 547 return null; 548 } 549 baseUnit.value = targetToInfo.target; 550 return targetToInfo.unitInfo; 551 } 552 getBaseUnit(String simpleUnit)553 public String getBaseUnit(String simpleUnit) { 554 TargetInfo targetToInfo = sourceToTargetInfo.get(simpleUnit); 555 if (targetToInfo == null) { 556 return null; 557 } 558 return targetToInfo.target; 559 } 560 561 /** 562 * Return the standard unit, eg newton for kilogram-meter-per-square-second 563 * @param simpleUnit 564 * @return 565 */ getStandardUnit(String unit)566 public String getStandardUnit(String unit) { 567 Output<String> metricUnit = new Output<>(); 568 parseUnitId(unit, metricUnit, false); 569 String result = sourceToStandard.get(metricUnit.value); 570 if (result == null) { 571 UnitId mUnit = createUnitId(metricUnit.value); 572 mUnit = mUnit.resolve(); 573 result = sourceToStandard.get(mUnit.toString()); 574 } 575 return result == null ? metricUnit.value : result; 576 } 577 getSpecialBaseUnit(String quantity, Set<UnitSystem> unitSystem)578 public String getSpecialBaseUnit(String quantity, Set<UnitSystem> unitSystem) { 579 if (unitSystem.contains(UnitSystem.ussystem) || unitSystem.contains(UnitSystem.uksystem)) { 580 switch(quantity) { 581 case "volume": return unitSystem.contains(UnitSystem.uksystem) ? "gallon-imperial" : "gallon"; 582 case "mass": return "pound"; 583 case "length": return "foot"; 584 case "area": return "square-foot"; 585 } 586 } 587 return null; 588 } 589 590 /** 591 * Takes a derived unit id, and produces the equivalent derived base unit id and UnitInfo to convert to it 592 * @author markdavis 593 * @param showYourWork TODO 594 * 595 */ parseUnitId(String derivedUnit, Output<String> metricUnit, boolean showYourWork)596 public ConversionInfo parseUnitId (String derivedUnit, Output<String> metricUnit, boolean showYourWork) { 597 metricUnit.value = null; 598 599 UnitId outputUnit = new UnitId(UNIT_COMPARATOR); 600 Rational numerator = Rational.ONE; 601 Rational denominator = Rational.ONE; 602 boolean inNumerator = true; 603 int power = 1; 604 605 Output<Rational> deprefix = new Output<>(); 606 Rational offset = Rational.ZERO; 607 int countUnits = 0; 608 for (Iterator<String> it = Continuation.split(derivedUnit, continuations).iterator(); it.hasNext();) { 609 String unit = it.next(); 610 ++countUnits; 611 if (unit.equals("square")) { 612 if (power != 1) { 613 throw new IllegalArgumentException("Can't have power of " + unit); 614 } 615 power = 2; 616 if (showYourWork) System.out.println(showRational("\t " + unit + ": ", Rational.of(power), "power")); 617 } else if (unit.equals("cubic")) { 618 if (power != 1) { 619 throw new IllegalArgumentException("Can't have power of " + unit); 620 } 621 power = 3; 622 if (showYourWork) System.out.println(showRational("\t " + unit + ": ", Rational.of(power), "power")); 623 } else if (unit.startsWith("pow")) { 624 if (power != 1) { 625 throw new IllegalArgumentException("Can't have power of " + unit); 626 } 627 power = Integer.parseInt(unit.substring(3)); 628 if (showYourWork) System.out.println(showRational("\t " + unit + ": ", Rational.of(power), "power")); 629 } else if (unit.equals("per")) { 630 if (power != 1) { 631 throw new IllegalArgumentException("Can't have power of per"); 632 } 633 if (showYourWork && inNumerator) System.out.println("\tper"); 634 inNumerator = false; // ignore multiples 635 // } else if ('9' >= unit.charAt(0)) { 636 // if (power != 1) { 637 // throw new IllegalArgumentException("Can't have power of " + unit); 638 // } 639 // Rational factor = Rational.of(Integer.parseInt(unit)); 640 // if (inNumerator) { 641 // numerator = numerator.multiply(factor); 642 // } else { 643 // denominator = denominator.multiply(factor); 644 // } 645 } else { 646 // kilo etc. 647 unit = stripPrefix(unit, deprefix); 648 if (showYourWork) { 649 if (!deprefix.value.equals(Rational.ONE)) { 650 System.out.println(showRational("\tprefix: ", deprefix.value, unit)); 651 } else { 652 System.out.println("\t" + unit); 653 } 654 } 655 656 Rational value = deprefix.value; 657 if (!isSimpleBaseUnit(unit)) { 658 TargetInfo info = sourceToTargetInfo.get(unit); 659 if (info == null) { 660 if (showYourWork) System.out.println("\t⟹ no conversion for: " + unit); 661 return null; // can't convert 662 } 663 String baseUnit = info.target; 664 665 value = info.unitInfo.factor.multiply(value); 666 //if (showYourWork && !info.unitInfo.factor.equals(Rational.ONE)) System.out.println(showRational("\tfactor: ", info.unitInfo.factor, baseUnit)); 667 // Special handling for offsets. We disregard them if there are any other units. 668 if (countUnits == 1 && !it.hasNext()) { 669 offset = info.unitInfo.offset; 670 if (showYourWork && !info.unitInfo.offset.equals(Rational.ZERO)) System.out.println(showRational("\toffset: ", info.unitInfo.offset, baseUnit)); 671 } 672 unit = baseUnit; 673 } 674 for (int p = 1; p <= power; ++p) { 675 String title = ""; 676 if (value.equals(Rational.ONE)) { 677 if (showYourWork) System.out.println("\t(already base unit)"); 678 continue; 679 } else if (inNumerator) { 680 numerator = numerator.multiply(value); 681 title = "\t× "; 682 } else { 683 denominator = denominator.multiply(value); 684 title = "\t÷ "; 685 } 686 if (showYourWork) System.out.println(showRational("\t× ", value, " ⟹ " + unit) + "\t" + numerator.divide(denominator) + "\t" + numerator.divide(denominator).doubleValue()); 687 } 688 // create cleaned up target unitid 689 outputUnit.add(continuations, unit, inNumerator, power); 690 power = 1; 691 } 692 } 693 metricUnit.value = outputUnit.toString(); 694 return new ConversionInfo(numerator.divide(denominator), offset); 695 } 696 697 698 /** Only for use for simple base unit comparison */ 699 private class UnitComparator implements Comparator<String>{ 700 // TODO, use order in units.xml 701 702 @Override compare(String o1, String o2)703 public int compare(String o1, String o2) { 704 if (o1.equals(o2)) { 705 return 0; 706 } 707 Output<Rational> deprefix1 = new Output<>(); 708 o1 = stripPrefix(o1, deprefix1); 709 TargetInfo targetAndInfo1 = sourceToTargetInfo.get(o1); 710 String quantity1 = baseUnitToQuantity.get(targetAndInfo1.target); 711 712 Output<Rational> deprefix2 = new Output<>(); 713 o2 = stripPrefix(o2, deprefix2); 714 TargetInfo targetAndInfo2 = sourceToTargetInfo.get(o2); 715 String quantity2 = baseUnitToQuantity.get(targetAndInfo2.target); 716 717 int diff; 718 if (0 != (diff = quantityComparator.compare(quantity1, quantity2))) { 719 return diff; 720 } 721 Rational factor1 = targetAndInfo1.unitInfo.factor.multiply(deprefix1.value); 722 Rational factor2 = targetAndInfo2.unitInfo.factor.multiply(deprefix2.value); 723 if (0 != (diff = factor1.compareTo(factor2))) { 724 return diff; 725 } 726 return o1.compareTo(o2); 727 } 728 } 729 730 Comparator<String> UNIT_COMPARATOR = new UnitComparator(); 731 732 /** 733 * Only handles the canonical units; no kilo-, only normalized, etc. 734 * @author markdavis 735 * 736 */ 737 public class UnitId implements Freezable<UnitId>, Comparable<UnitId> { 738 public Map<String, Integer> numUnitsToPowers; 739 public Map<String, Integer> denUnitsToPowers; 740 public EntrySetComparator<String, Integer> entrySetComparator; 741 private boolean frozen = false; 742 UnitId(Comparator<String> comparator)743 private UnitId(Comparator<String> comparator) { 744 numUnitsToPowers = new TreeMap<>(comparator); 745 denUnitsToPowers = new TreeMap<>(comparator); 746 entrySetComparator = new EntrySetComparator<String, Integer>(comparator, Comparator.naturalOrder()); 747 } // 748 add(Multimap<String, Continuation> continuations, String compoundUnit, boolean groupInNumerator, int groupPower)749 private UnitId add(Multimap<String, Continuation> continuations, String compoundUnit, boolean groupInNumerator, int groupPower) { 750 if (frozen) { 751 throw new UnsupportedOperationException("Object is frozen."); 752 } 753 boolean inNumerator = true; 754 int power = 1; 755 // maybe refactor common parts with above code. 756 for (String unitPart : Continuation.split(compoundUnit, continuations)) { 757 switch (unitPart) { 758 case "square": power = 2; break; 759 case "cubic": power = 3; break; 760 case "per": inNumerator = false; break; // sticky, ignore multiples 761 default: 762 if (unitPart.startsWith("pow")) { 763 power = Integer.parseInt(unitPart.substring(3)); 764 } else { 765 Map<String, Integer> target = inNumerator == groupInNumerator ? numUnitsToPowers : denUnitsToPowers; 766 Integer oldPower = target.get(unitPart); 767 // we multiply powers, so that weight-square-volume => weight-pow4-length 768 int newPower = groupPower * power + (oldPower == null ? 0 : oldPower); 769 target.put(unitPart, newPower); 770 power = 1; 771 } 772 } 773 } 774 return this; 775 } 776 @Override toString()777 public String toString() { 778 StringBuilder builder = new StringBuilder(); 779 boolean firstDenominator = true; 780 for (int i = 1; i >= 0; --i) { // two passes, numerator then den. 781 boolean positivePass = i > 0; 782 Map<String, Integer> target = positivePass ? numUnitsToPowers : denUnitsToPowers; 783 for (Entry<String, Integer> entry : target.entrySet()) { 784 String unit = entry.getKey(); 785 int power = entry.getValue(); 786 // NOTE: zero (eg one-per-one) gets counted twice 787 if (builder.length() != 0) { 788 builder.append('-'); 789 } 790 if (!positivePass) { 791 if (firstDenominator) { 792 firstDenominator = false; 793 builder.append("per-"); 794 } 795 } 796 switch (power) { 797 case 1: 798 break; 799 case 2: 800 builder.append("square-"); break; 801 case 3: 802 builder.append("cubic-"); break; 803 default: 804 if (power > 3) { 805 builder.append("pow" + power + "-"); 806 } else { 807 throw new IllegalArgumentException("Unhandled power: " + power); 808 } 809 break; 810 } 811 builder.append(unit); 812 813 } 814 } 815 return builder.toString(); 816 } 817 toString(LocaleStringProvider resolvedFile, String width, String _pluralCategory, String caseVariant, Multimap<UnitPathType, String> partsUsed, boolean maximal)818 public String toString(LocaleStringProvider resolvedFile, String width, String _pluralCategory, String caseVariant, Multimap<UnitPathType, String> partsUsed, boolean maximal) { 819 if (partsUsed != null) { 820 partsUsed.clear(); 821 } 822 String result = null; 823 String numerator = null; 824 String timesPattern = null; 825 String placeholderPattern = null; 826 Output<Integer> deprefix = new Output<>(); 827 828 PlaceholderLocation placeholderPosition = PlaceholderLocation.missing; 829 Matcher placeholderMatcher = PLACEHOLDER.matcher(""); 830 Output<String> unitPatternOut = new Output<>(); 831 832 PluralInfo pluralInfo = CLDRConfig.getInstance().getSupplementalDataInfo().getPlurals(resolvedFile.getLocaleID()); 833 PluralRules pluralRules = pluralInfo.getPluralRules(); 834 String singularPluralCategory = pluralRules.select(1d); 835 final ULocale locale = new ULocale(resolvedFile.getLocaleID()); 836 String fullPerPattern = null; 837 int negCount = 0; 838 839 for (int i = 1; i >= 0; --i) { // two passes, numerator then den. 840 boolean positivePass = i > 0; 841 if (!positivePass) { 842 switch(locale.toString()) { 843 case "de": caseVariant = "accusative"; break; // German pro rule 844 } 845 numerator = result; // from now on, result ::= denominator 846 result = null; 847 } 848 849 Map<String, Integer> target = positivePass ? numUnitsToPowers : denUnitsToPowers; 850 int unitsLeft = target.size(); 851 for (Entry<String, Integer> entry : target.entrySet()) { 852 String possiblyPrefixedUnit = entry.getKey(); 853 String unit = stripPrefixPower(possiblyPrefixedUnit, deprefix); 854 String genderVariant = UnitPathType.gender.getTrans(resolvedFile, "long", unit, null, null, null, partsUsed); 855 856 int power = entry.getValue(); 857 unitsLeft--; 858 String pluralCategory = unitsLeft == 0 && positivePass ? _pluralCategory : singularPluralCategory; 859 860 if (!positivePass) { 861 if (maximal && 0 == negCount++) { // special case exact match for per form, and no previous result 862 if (true) { 863 throw new UnsupportedOperationException("not yet implemented fully"); 864 } 865 String fullUnit; 866 switch(power) { 867 case 1: fullUnit = unit; break; 868 case 2: fullUnit = "square-" + unit; break; 869 case 3: fullUnit = "cubic-" + unit; break; 870 default: throw new IllegalArgumentException("powers > 3 not supported"); 871 } 872 fullPerPattern = UnitPathType.perUnit.getTrans(resolvedFile, width, fullUnit, _pluralCategory, caseVariant, genderVariant, partsUsed); 873 // if there is a special form, we'll use it 874 if (fullPerPattern != null) { 875 continue; 876 } 877 } 878 } 879 880 // handle prefix, like kilo- 881 String prefixPattern = null; 882 if (deprefix.value != 1) { 883 prefixPattern = UnitPathType.prefix.getTrans(resolvedFile, width, "10p" + deprefix.value, _pluralCategory, caseVariant, genderVariant, partsUsed); 884 } 885 886 // get the core pattern. Detect and remove the the placeholder (and surrounding spaces) 887 String unitPattern = UnitPathType.unit.getTrans(resolvedFile, width, unit, pluralCategory, caseVariant, genderVariant, partsUsed); 888 if (unitPattern == null) { 889 return null; // unavailable 890 } 891 // we are set up for 2 kinds of placeholder patterns for units. {0}\s?stuff or stuff\s?{0}, or nothing(Eg Arabic) 892 placeholderPosition = extractUnit(placeholderMatcher, unitPattern, unitPatternOut); 893 if (placeholderPosition == PlaceholderLocation.middle) { 894 return null; // signal we can't handle, but shouldn't happen with well-formed data. 895 } else if (placeholderPosition != PlaceholderLocation.missing) { 896 unitPattern = unitPatternOut.value; 897 placeholderPattern = placeholderMatcher.group(); 898 } 899 900 // we have all the pieces, so build it up 901 if (prefixPattern != null) { 902 unitPattern = combineLowercasing(locale, width, prefixPattern, unitPattern); 903 } 904 905 String powerPattern = null; 906 switch (power) { 907 case 1: 908 break; 909 case 2: 910 powerPattern = UnitPathType.power.getTrans(resolvedFile, width, "power2", pluralCategory, caseVariant, genderVariant, partsUsed); 911 break; 912 case 3: 913 powerPattern = UnitPathType.power.getTrans(resolvedFile, width, "power3", pluralCategory, caseVariant, genderVariant, partsUsed); 914 break; 915 default: 916 throw new IllegalArgumentException("No power pattern > 3: " + this); 917 } 918 919 if (powerPattern != null) { 920 unitPattern = combineLowercasing(locale, width, powerPattern, unitPattern); 921 } 922 923 if (result != null) { 924 if (timesPattern == null) { 925 timesPattern = getTimesPattern(resolvedFile, width); 926 } 927 result = MessageFormat.format(timesPattern, result, unitPattern); 928 } else { 929 result = unitPattern; 930 } 931 } 932 } 933 934 // if there is a fullPerPattern, then we use it instead of per pattern + first denominator element 935 if (fullPerPattern != null) { 936 if (numerator != null) { 937 numerator = MessageFormat.format(fullPerPattern, numerator); 938 } else { 939 numerator = fullPerPattern; 940 placeholderPattern = null; 941 } 942 if (result != null) { 943 if (timesPattern == null) { 944 timesPattern = getTimesPattern(resolvedFile, width); 945 } 946 numerator = MessageFormat.format(timesPattern, numerator, result); 947 } 948 result = numerator; 949 } else { 950 // glue the two parts together, if we have two of them 951 if (result == null) { 952 result = numerator; 953 } else { 954 String perPattern = UnitPathType.per.getTrans(resolvedFile, width, null, _pluralCategory, caseVariant, null, partsUsed); 955 if (numerator == null) { 956 result = MessageFormat.format(perPattern, "", result).trim(); 957 } else { 958 result = MessageFormat.format(perPattern, numerator, result); 959 } 960 } 961 } 962 return addPlaceholder(result, placeholderPattern, placeholderPosition); 963 } 964 getTimesPattern(LocaleStringProvider resolvedFile, String width)965 public String getTimesPattern(LocaleStringProvider resolvedFile, String width) { // TODO fix hack! 966 if (HACK && "en".equals(resolvedFile.getLocaleID())) { 967 return "{0}-{1}"; 968 } 969 String timesPatternPath = "//ldml/units/unitLength[@type=\"" + width + "\"]/compoundUnit[@type=\"times\"]/compoundUnitPattern"; 970 return resolvedFile.getStringValue(timesPatternPath); 971 } 972 973 @Override equals(Object obj)974 public boolean equals(Object obj) { 975 UnitId other = (UnitId) obj; 976 return numUnitsToPowers.equals(other.numUnitsToPowers) 977 && denUnitsToPowers.equals(other.denUnitsToPowers); 978 } 979 @Override hashCode()980 public int hashCode() { 981 return Objects.hash(numUnitsToPowers, denUnitsToPowers); 982 } 983 @Override isFrozen()984 public boolean isFrozen() { 985 return frozen; 986 } 987 @Override freeze()988 public UnitId freeze() { 989 frozen = true; 990 numUnitsToPowers = ImmutableMap.copyOf(numUnitsToPowers); 991 denUnitsToPowers = ImmutableMap.copyOf(denUnitsToPowers); 992 return this; 993 } 994 @Override cloneAsThawed()995 public UnitId cloneAsThawed() { 996 throw new UnsupportedOperationException(); 997 } 998 resolve()999 public UnitId resolve() { 1000 UnitId result = new UnitId(UNIT_COMPARATOR); 1001 result.numUnitsToPowers.putAll(numUnitsToPowers); 1002 result.denUnitsToPowers.putAll(denUnitsToPowers); 1003 for (Entry<String, Integer> entry : numUnitsToPowers.entrySet()) { 1004 final String key = entry.getKey(); 1005 Integer denPower = denUnitsToPowers.get(key); 1006 if (denPower == null) { 1007 continue; 1008 } 1009 int power = entry.getValue() - denPower; 1010 if (power > 0) { 1011 result.numUnitsToPowers.put(key, power); 1012 result.denUnitsToPowers.remove(key); 1013 } else if (power < 0) { 1014 result.numUnitsToPowers.remove(key); 1015 result.denUnitsToPowers.put(key, -power); 1016 } else { // 0, so 1017 result.numUnitsToPowers.remove(key); 1018 result.denUnitsToPowers.remove(key); 1019 } 1020 } 1021 return result.freeze(); 1022 } 1023 1024 @Override compareTo(UnitId o)1025 public int compareTo(UnitId o) { 1026 int diff = compareEntrySets(numUnitsToPowers.entrySet(), o.numUnitsToPowers.entrySet(), entrySetComparator); 1027 if (diff != 0) return diff; 1028 return compareEntrySets(denUnitsToPowers.entrySet(), o.denUnitsToPowers.entrySet(), entrySetComparator); 1029 } 1030 1031 /** 1032 * Default rules 1033 * Prefixes & powers: the gender of the whole is the same as the gender of the operand. In pseudocode: 1034 gender(square, meter) = gender(meter) 1035 gender(kilo, meter) = gender(meter) 1036 1037 * Per: the gender of the whole is the gender of the numerator. If there is no numerator, then the gender of the denominator 1038 gender(gram per meter) = gender(gram) 1039 1040 * Times: the gender of the whole is the gender of the last operand 1041 gender(gram-meter) = gender(gram) 1042 * @param source 1043 * @param partsUsed 1044 1045 * @return 1046 * TODO: add parameter to short-circuit the lookup if the unit is not a compound. 1047 */ getGender(CLDRFile resolvedFile, Output<String> source, Multimap<UnitPathType, String> partsUsed)1048 public String getGender(CLDRFile resolvedFile, Output<String> source, Multimap<UnitPathType, String> partsUsed) { 1049 // will not be empty 1050 1051 GrammarDerivation gd = null; 1052 //Values power = gd.get(GrammaticalFeature.grammaticalGender, CompoundUnitStructure.power); no data available yet 1053 //Values prefix = gd.get(GrammaticalFeature.grammaticalGender, CompoundUnitStructure.prefix); 1054 1055 1056 Map<String,Integer> determiner; 1057 if (numUnitsToPowers.isEmpty()) { 1058 determiner = denUnitsToPowers; 1059 } else if (denUnitsToPowers.isEmpty()) { 1060 determiner = numUnitsToPowers; 1061 } else { 1062 if (gd == null) { 1063 gd = SupplementalDataInfo.getInstance().getGrammarDerivation(resolvedFile.getLocaleID()); 1064 } 1065 Values per = gd.get(GrammaticalFeature.grammaticalGender, CompoundUnitStructure.per); 1066 boolean useFirst = per.value0.equals("0"); 1067 determiner = useFirst ? numUnitsToPowers // otherwise use numerator if possible 1068 : denUnitsToPowers; 1069 // TODO add test that the value is 0 or 1, so that if it fails we know to upgrade this code. 1070 } 1071 1072 Entry<String, Integer> bestMeasure; 1073 if (determiner.size() == 1) { 1074 bestMeasure = determiner.entrySet().iterator().next(); 1075 } else { 1076 if (gd == null) { 1077 gd = SupplementalDataInfo.getInstance().getGrammarDerivation(resolvedFile.getLocaleID()); 1078 } 1079 Values times = gd.get(GrammaticalFeature.grammaticalGender, CompoundUnitStructure.times); 1080 boolean useFirst = times.value0.equals("0"); 1081 if (useFirst) { 1082 bestMeasure = determiner.entrySet().iterator().next(); 1083 } else { 1084 bestMeasure = null; // we know the determiner is not empty, but this makes the compiler 1085 for (Entry<String, Integer> entry : determiner.entrySet()) { 1086 bestMeasure = entry; 1087 } 1088 } 1089 } 1090 String strippedUnit = stripPrefix(bestMeasure.getKey(), null); 1091 String gender = UnitPathType.gender.getTrans(resolvedFile, "long", strippedUnit, null, null, null, partsUsed); 1092 if (gender != null && source != null) { 1093 source.value = strippedUnit; 1094 } 1095 return gender; 1096 } 1097 1098 } 1099 1100 public enum PlaceholderLocation {before, middle, after, missing} 1101 addPlaceholder(String result, String placeholderPattern, PlaceholderLocation placeholderPosition)1102 public static String addPlaceholder(String result, String placeholderPattern, PlaceholderLocation placeholderPosition) { 1103 return placeholderPattern == null ? result 1104 : placeholderPosition == PlaceholderLocation.before ? placeholderPattern + result 1105 : result + placeholderPattern; 1106 } 1107 1108 /** 1109 * Returns the location of the placeholder. Call placeholderMatcher.group() after calling this to get the placeholder. 1110 * @param placeholderMatcher 1111 * @param unitPattern 1112 * @param unitPatternOut 1113 * @param before 1114 * @return 1115 */ extractUnit(Matcher placeholderMatcher, String unitPattern, Output<String> unitPatternOut)1116 public static PlaceholderLocation extractUnit(Matcher placeholderMatcher, String unitPattern, Output<String> unitPatternOut) { 1117 if (placeholderMatcher.reset(unitPattern).find()) { 1118 if (placeholderMatcher.start() == 0) { 1119 unitPatternOut.value = unitPattern.substring(placeholderMatcher.end()); 1120 return PlaceholderLocation.before; 1121 } else if (placeholderMatcher.end() == unitPattern.length()) { 1122 unitPatternOut.value = unitPattern.substring(0, placeholderMatcher.start()); 1123 return PlaceholderLocation.after; 1124 } else { 1125 unitPatternOut.value = unitPattern; 1126 return PlaceholderLocation.middle; 1127 } 1128 } else { 1129 unitPatternOut.value = unitPattern; 1130 return PlaceholderLocation.missing; 1131 } 1132 } 1133 combineLowercasing(final ULocale locale, String width, String prefixPattern, String unitPattern)1134 public static String combineLowercasing(final ULocale locale, String width, String prefixPattern, String unitPattern) { 1135 // catch special case, ZentiLiter 1136 if (width.equals("long") && !prefixPattern.contains(" {") && !prefixPattern.contains(" {")) { 1137 unitPattern = UCharacter.toLowerCase(locale, unitPattern); 1138 } 1139 unitPattern = MessageFormat.format(prefixPattern, unitPattern); 1140 return unitPattern; 1141 } 1142 1143 public static class EntrySetComparator<K extends Comparable<K>,V> implements Comparator<Entry<K, V>> { 1144 Comparator<K> kComparator; 1145 Comparator<V> vComparator; EntrySetComparator(Comparator<K> kComparator, Comparator<V> vComparator)1146 public EntrySetComparator(Comparator<K> kComparator, Comparator<V> vComparator) { 1147 this.kComparator = kComparator; 1148 this.vComparator = vComparator; 1149 } 1150 @Override compare(Entry<K, V> o1, Entry<K, V> o2)1151 public int compare(Entry<K, V> o1, Entry<K, V> o2) { 1152 int diff = kComparator.compare(o1.getKey(), o2.getKey()); 1153 if (diff != 0) { 1154 return diff; 1155 } 1156 diff = vComparator.compare(o1.getValue(), o2.getValue()); 1157 if (diff != 0) { 1158 return diff; 1159 } 1160 return o1.getKey().compareTo(o2.getKey()); 1161 } 1162 } 1163 compareEntrySets(Collection<T> o1, Collection<T> o2, Comparator<T> comparator)1164 public static <K extends Comparable<K>, V extends Comparable<V>, T extends Entry<K, V>> int compareEntrySets(Collection<T> o1, Collection<T> o2, Comparator<T> comparator) { 1165 Iterator<T> iterator1 = o1.iterator(); 1166 Iterator<T> iterator2 = o2.iterator(); 1167 while (true) { 1168 if (!iterator1.hasNext()) { 1169 return iterator2.hasNext() ? -1 : 0; 1170 } else if (!iterator2.hasNext()) { 1171 return 1; 1172 } 1173 T item1 = iterator1.next(); 1174 T item2 = iterator2.next(); 1175 int diff = comparator.compare(item1, item2); 1176 if (diff != 0) { 1177 return diff; 1178 } 1179 } 1180 } 1181 1182 private ConcurrentHashMap<String, UnitId> UNIT_ID = new ConcurrentHashMap<>(); 1183 // TODO This is safe but should use regular cache createUnitId(String unit)1184 public final UnitId createUnitId(String unit) { 1185 UnitId result = UNIT_ID.get(unit); 1186 if (result == null) { 1187 result = new UnitId(UNIT_COMPARATOR).add(continuations, unit, true, 1).freeze(); 1188 UNIT_ID.put(unit, result); 1189 } 1190 return result; 1191 } 1192 isBaseUnit(String unit)1193 public boolean isBaseUnit(String unit) { 1194 return baseUnits.contains(unit); 1195 } 1196 isSimpleBaseUnit(String unit)1197 public boolean isSimpleBaseUnit(String unit) { 1198 return BASE_UNITS.contains(unit); 1199 } 1200 baseUnits()1201 public Set<String> baseUnits() { 1202 return baseUnits; 1203 } 1204 1205 // TODO change to TRIE if the performance isn't good enough, or restructure with regex 1206 public static final ImmutableMap<String, Integer> PREFIX_POWERS = ImmutableMap.<String, Integer>builder() 1207 .put("yocto", -24) 1208 .put("zepto", -21) 1209 .put("atto", -18) 1210 .put("femto", -15) 1211 .put("pico", -12) 1212 .put("nano", -9) 1213 .put("micro", -6) 1214 .put("milli", -3) 1215 .put("centi", -2) 1216 .put("deci", -1) 1217 .put("deka", 1) 1218 .put("hecto", 2) 1219 .put("kilo", 3) 1220 .put("mega", 6) 1221 .put("giga", 9) 1222 .put("tera", 12) 1223 .put("peta", 15) 1224 .put("exa", 18) 1225 .put("zetta", 21) 1226 .put("yotta", 24) 1227 .build(); 1228 1229 public static final ImmutableMap<String, Rational> PREFIXES; 1230 static { 1231 Map<String, Rational> temp = new LinkedHashMap<>(); 1232 for (Entry<String, Integer> entry : PREFIX_POWERS.entrySet()) { entry.getKey()1233 temp.put(entry.getKey(), Rational.pow10(entry.getValue())); 1234 } 1235 PREFIXES = ImmutableMap.copyOf(temp); 1236 } 1237 1238 static final Set<String> SKIP_PREFIX = ImmutableSet.of( 1239 "millimeter-ofhg", 1240 "kilogram" 1241 ); 1242 1243 static final Rational RATIONAL1000 = Rational.of(1000); 1244 /** 1245 * If there is no prefix, return the unit and ONE. 1246 * If there is a prefix return the unit (with prefix stripped) and the prefix factor 1247 * */ stripPrefixCommon(String unit, Output<V> deprefix, Map<String, V> unitMap)1248 private static <V> String stripPrefixCommon(String unit, Output<V> deprefix, Map<String, V> unitMap) { 1249 if (SKIP_PREFIX.contains(unit)) { 1250 return unit; 1251 } 1252 1253 for (Entry<String, V> entry : unitMap.entrySet()) { 1254 String prefix = entry.getKey(); 1255 if (unit.startsWith(prefix)) { 1256 String result = unit.substring(prefix.length()); 1257 // We have to do a special hack for kilogram, but only for the Rational case. 1258 // The Integer case is used for name construction, so that is ok. 1259 final boolean isRational = deprefix != null && deprefix.value instanceof Rational; 1260 boolean isGramHack = isRational && result.equals("gram"); 1261 if (isGramHack) { 1262 result = "kilogram"; 1263 } 1264 if (deprefix != null) { 1265 deprefix.value = entry.getValue(); 1266 if (isGramHack) { 1267 final Rational ratValue = (Rational) deprefix.value; 1268 deprefix.value = (V) ratValue.divide(RATIONAL1000); 1269 } 1270 } 1271 return result; 1272 } 1273 } 1274 return unit; 1275 } 1276 stripPrefix(String unit, Output<Rational> deprefix)1277 public static String stripPrefix(String unit, Output<Rational> deprefix) { 1278 if (deprefix != null) { 1279 deprefix.value = Rational.ONE; 1280 } 1281 return stripPrefixCommon(unit, deprefix, PREFIXES); 1282 } 1283 stripPrefixPower(String unit, Output<Integer> deprefix)1284 public static String stripPrefixPower(String unit, Output<Integer> deprefix) { 1285 if (deprefix != null) { 1286 deprefix.value = Integer.valueOf(1); 1287 } 1288 return stripPrefixCommon(unit, deprefix, PREFIX_POWERS); 1289 } 1290 getBaseUnitToQuantity()1291 public BiMap<String, String> getBaseUnitToQuantity() { 1292 return (BiMap<String, String>) baseUnitToQuantity; 1293 } 1294 getQuantityFromUnit(String unit, boolean showYourWork)1295 public String getQuantityFromUnit(String unit, boolean showYourWork) { 1296 Output<String> metricUnit = new Output<>(); 1297 unit = fixDenormalized(unit); 1298 ConversionInfo unitInfo = parseUnitId(unit, metricUnit, showYourWork); 1299 return metricUnit.value == null ? null : getQuantityFromBaseUnit(metricUnit.value); 1300 } 1301 getQuantityFromBaseUnit(String baseUnit)1302 public String getQuantityFromBaseUnit(String baseUnit) { 1303 if (baseUnit == null) { 1304 throw new NullPointerException("baseUnit"); 1305 } 1306 String result = getQuantityFromBaseUnit2(baseUnit); 1307 if (result != null) { 1308 return result; 1309 } 1310 result = getQuantityFromBaseUnit2(reciprocalOf(baseUnit)); 1311 if (result != null) { 1312 result += "-inverse"; 1313 } 1314 return result; 1315 } 1316 getQuantityFromBaseUnit2(String baseUnit)1317 private String getQuantityFromBaseUnit2(String baseUnit) { 1318 String result = baseUnitToQuantity.get(baseUnit); 1319 if (result != null) { 1320 return result; 1321 } 1322 UnitId unitId = createUnitId(baseUnit); 1323 UnitId resolved = unitId.resolve(); 1324 return baseUnitToQuantity.get(resolved.toString()); 1325 } 1326 getSimpleUnits()1327 public Set<String> getSimpleUnits() { 1328 return sourceToTargetInfo.keySet(); 1329 } 1330 addAliases(Map<String, R2<List<String>, String>> tagToReplacement)1331 public void addAliases(Map<String, R2<List<String>, String>> tagToReplacement) { 1332 fixDenormalized = new TreeMap<>(); 1333 for (Entry<String, R2<List<String>, String>> entry : tagToReplacement.entrySet()) { 1334 final String badCode = entry.getKey(); 1335 final List<String> replacements = entry.getValue().get0(); 1336 fixDenormalized.put(badCode, replacements.iterator().next()); 1337 } 1338 fixDenormalized = ImmutableMap.copyOf(fixDenormalized); 1339 } 1340 getInternalConversionData()1341 public Map<String, TargetInfo> getInternalConversionData() { 1342 return sourceToTargetInfo; 1343 } 1344 getSourceToSystems()1345 public Multimap<String, String> getSourceToSystems() { 1346 return sourceToSystems; 1347 } 1348 1349 public enum UnitSystem { // TODO convert getSystems and SupplementalDataInfo to use natively 1350 si, 1351 metric, 1352 ussystem, 1353 uksystem, 1354 other; 1355 1356 public static final Set<UnitSystem> SiOrMetric = ImmutableSet.of(UnitSystem.metric, UnitSystem.si); 1357 fromStringCollection(Collection<String> stringUnitSystems)1358 public static Set<UnitSystem> fromStringCollection(Collection<String> stringUnitSystems) { 1359 return stringUnitSystems.stream().map(x -> UnitSystem.valueOf(x)).collect(Collectors.toSet()); 1360 } toStringSet(Collection<UnitSystem> stringUnitSystems)1361 public static Set<String> toStringSet(Collection<UnitSystem> stringUnitSystems) { 1362 return stringUnitSystems.stream().map(x -> x.toString()).collect(Collectors.toSet()); 1363 } 1364 } 1365 getSystems(String unit)1366 public Set<String> getSystems(String unit) { 1367 return UnitSystem.toStringSet(getSystemsEnum(unit)); 1368 } 1369 getSystemsEnum(String unit)1370 public Set<UnitSystem> getSystemsEnum(String unit) { 1371 Set<UnitSystem> result = null; 1372 UnitId id = createUnitId(unit); 1373 1374 // we walk through all the units in the numerator and denominator, and keep the *intersection* of 1375 // the units. So {ussystem} and {ussystem, uksystem} => ussystem 1376 // Special case: {dmetric} intersect {metric} => {dmetric}. We do that by adding dmetric to any set with metric, then removing dmetric if there is a metric 1377 main: 1378 for (Map<String, Integer> unitsToPowers : Arrays.asList(id.denUnitsToPowers, id.numUnitsToPowers)) { 1379 for (String subunit : unitsToPowers.keySet()) { 1380 subunit = UnitConverter.stripPrefix(subunit, null); 1381 Set<UnitSystem> systems = UnitSystem.fromStringCollection(sourceToSystems.get(subunit)); 1382 1383 if (result == null) { 1384 result = systems; 1385 } else { 1386 result.retainAll(systems); 1387 } 1388 if (result.isEmpty()) { 1389 break main; 1390 } 1391 } 1392 } 1393 return result == null || result.isEmpty() ? ImmutableSet.of(UnitSystem.other) : ImmutableSet.copyOf(EnumSet.copyOf(result)); 1394 } 1395 1396 addSystems(Set<String> result, String subunit)1397 private void addSystems(Set<String> result, String subunit) { 1398 Collection<String> systems = sourceToSystems.get(subunit); 1399 if (!systems.isEmpty()) { 1400 result.addAll(systems); 1401 } 1402 } 1403 reciprocalOf(String value)1404 public String reciprocalOf(String value) { 1405 // quick version, input guarantteed to be normalized 1406 int index = value.indexOf("-per-"); 1407 if (index < 0) { 1408 return null; 1409 } 1410 return value.substring(index+5) + "-per-" + value.substring(0, index); 1411 } 1412 parseRational(String source)1413 public Rational parseRational(String source) { 1414 return rationalParser.parse(source); 1415 } 1416 showRational(String title, Rational rational, String unit)1417 public String showRational(String title, Rational rational, String unit) { 1418 String doubleString = showRational2(rational, " = ", " ≅ "); 1419 final String endResult = title + rational + doubleString + (unit != null ? " " + unit: ""); 1420 return endResult; 1421 } 1422 showRational(Rational rational, String approximatePrefix)1423 public String showRational(Rational rational, String approximatePrefix) { 1424 String doubleString = showRational2(rational, "", approximatePrefix); 1425 return doubleString.isEmpty() ? rational.numerator.toString() : doubleString; 1426 } 1427 showRational2(Rational rational, String equalPrefix, String approximatePrefix)1428 public String showRational2(Rational rational, String equalPrefix, String approximatePrefix) { 1429 String doubleString = ""; 1430 if (!rational.denominator.equals(BigInteger.ONE)) { 1431 String doubleValue = String.valueOf(rational.toBigDecimal(MathContext.DECIMAL32).doubleValue()); 1432 Rational reverse = parseRational(doubleValue); 1433 doubleString = (reverse.equals(rational) ? equalPrefix : approximatePrefix) + doubleValue; 1434 } 1435 return doubleString; 1436 } 1437 convert(Rational sourceValue, String sourceUnit, final String targetUnit, boolean showYourWork)1438 public Rational convert(Rational sourceValue, String sourceUnit, final String targetUnit, boolean showYourWork) { 1439 if (showYourWork) { 1440 System.out.println(showRational("\nconvert:\t", sourceValue, sourceUnit) + " ⟹ " + targetUnit); 1441 } 1442 sourceUnit = fixDenormalized(sourceUnit); 1443 Output<String> sourceBase = new Output<>(); 1444 Output<String> targetBase = new Output<>(); 1445 ConversionInfo sourceConversionInfo = parseUnitId(sourceUnit, sourceBase, showYourWork); 1446 if (sourceConversionInfo == null) { 1447 if (showYourWork) System.out.println("! unknown unit: " + sourceUnit); 1448 return Rational.NaN; 1449 } 1450 Rational intermediateResult = sourceConversionInfo.convert(sourceValue); 1451 if (showYourWork) System.out.println(showRational("intermediate:\t", intermediateResult, sourceBase.value)); 1452 if (showYourWork) System.out.println("invert:\t" + targetUnit); 1453 ConversionInfo targetConversionInfo = parseUnitId(targetUnit, targetBase, showYourWork); 1454 if (targetConversionInfo == null) { 1455 if (showYourWork) System.out.println("! unknown unit: " + targetUnit); 1456 return Rational.NaN; 1457 } 1458 if (!sourceBase.value.equals(targetBase.value)) { 1459 // try resolving 1460 String sourceBaseFixed = createUnitId(sourceBase.value).resolve().toString(); 1461 String targetBaseFixed = createUnitId(targetBase.value).resolve().toString(); 1462 // try reciprocal 1463 if (!sourceBaseFixed.equals(targetBaseFixed)) { 1464 String reciprocalUnit = reciprocalOf(sourceBase.value); 1465 if (reciprocalUnit == null || !targetBase.value.equals(reciprocalUnit)) { 1466 if (showYourWork) System.out.println("! incomparable units: " + sourceUnit + " and " + targetUnit); 1467 return Rational.NaN; 1468 } 1469 intermediateResult = intermediateResult.reciprocal(); 1470 if (showYourWork) System.out.println(showRational(" ⟹ 1/intermediate:\t", intermediateResult, reciprocalUnit)); 1471 } 1472 } 1473 Rational result = targetConversionInfo.convertBackwards(intermediateResult); 1474 if (showYourWork) System.out.println(showRational("target:\t", result, targetUnit)); 1475 return result; 1476 } 1477 fixDenormalized(String unit)1478 public String fixDenormalized(String unit) { 1479 String fixed = fixDenormalized.get(unit); 1480 return fixed == null ? unit : fixed; 1481 } 1482 getConstants()1483 public Map<String, Rational> getConstants() { 1484 return rationalParser.getConstants(); 1485 } 1486 getBaseUnitFromQuantity(String unitQuantity)1487 public String getBaseUnitFromQuantity(String unitQuantity) { 1488 boolean invert = false; 1489 if (unitQuantity.endsWith("-inverse")) { 1490 invert = true; 1491 unitQuantity = unitQuantity.substring(0,unitQuantity.length()-8); 1492 } 1493 String bu = ((BiMap<String,String>) baseUnitToQuantity).inverse().get(unitQuantity); 1494 if (bu == null) { 1495 return null; 1496 } 1497 return invert ? reciprocalOf(bu) : bu; 1498 } 1499 getQuantities()1500 public Set<String> getQuantities() { 1501 return getBaseUnitToQuantity().inverse().keySet(); 1502 } 1503 1504 public enum UnitComplexity {simple, non_simple} 1505 1506 private ConcurrentHashMap<String, UnitComplexity> COMPLEXITY = new ConcurrentHashMap<>(); 1507 // TODO This is safe but should use regular cache 1508 getComplexity(String longOrShortId)1509 public UnitComplexity getComplexity (String longOrShortId){ 1510 UnitComplexity result = COMPLEXITY.get(longOrShortId); 1511 if (result == null) { 1512 String shortId; 1513 String longId = getLongId(longOrShortId); 1514 if (longId == null) { 1515 longId = longOrShortId; 1516 shortId = SHORT_TO_LONG_ID.inverse().get(longId); 1517 } else { 1518 shortId = longOrShortId; 1519 } 1520 UnitId uid = createUnitId(shortId); 1521 result = UnitComplexity.simple; 1522 1523 if (uid.numUnitsToPowers.size() != 1 || !uid.denUnitsToPowers.isEmpty()) { 1524 result = UnitComplexity.non_simple; 1525 } else { 1526 Output<Rational> deprefix = new Output<>(); 1527 for (Entry<String, Integer> entry : uid.numUnitsToPowers.entrySet()) { 1528 final String unitPart = entry.getKey(); 1529 UnitConverter.stripPrefix(unitPart, deprefix ); 1530 if (!deprefix.value.equals(Rational.ONE) || !entry.getValue().equals(INTEGER_ONE)) { 1531 result = UnitComplexity.non_simple; 1532 break; 1533 } 1534 } 1535 if (result == UnitComplexity.simple) { 1536 for (Entry<String, Integer> entry : uid.denUnitsToPowers.entrySet()) { 1537 final String unitPart = entry.getKey(); 1538 UnitConverter.stripPrefix(unitPart, deprefix); 1539 if (!deprefix.value.equals(Rational.ONE)) { 1540 result = UnitComplexity.non_simple; 1541 break; 1542 } 1543 } 1544 } 1545 } 1546 COMPLEXITY.put(shortId, result); 1547 COMPLEXITY.put(longId, result); 1548 } 1549 return result; 1550 } 1551 isSimple(String x)1552 public boolean isSimple(String x) { 1553 return getComplexity(x) == UnitComplexity.simple; 1554 } 1555 getLongId(String shortUnitId)1556 public String getLongId(String shortUnitId) { 1557 return CldrUtility.ifNull(SHORT_TO_LONG_ID.get(shortUnitId), shortUnitId); 1558 } 1559 getLongIds(Iterable<String> shortUnitIds)1560 public Set<String> getLongIds(Iterable<String> shortUnitIds) { 1561 LinkedHashSet<String> result = new LinkedHashSet<>(); 1562 for (String longUnitId : shortUnitIds) { 1563 String shortId = SHORT_TO_LONG_ID.get(longUnitId); 1564 if (shortId != null) { 1565 result.add(shortId); 1566 } 1567 } 1568 return ImmutableSet.copyOf(result); 1569 } 1570 getShortId(String longUnitId)1571 public String getShortId(String longUnitId) { 1572 return CldrUtility.ifNull(SHORT_TO_LONG_ID.inverse().get(longUnitId), longUnitId); 1573 } 1574 getShortIds(Iterable<String> longUnitIds)1575 public Set<String> getShortIds(Iterable<String> longUnitIds) { 1576 LinkedHashSet<String> result = new LinkedHashSet<>(); 1577 for (String longUnitId : longUnitIds) { 1578 String shortId = SHORT_TO_LONG_ID.inverse().get(longUnitId); 1579 if (shortId != null) { 1580 result.add(shortId); 1581 } 1582 } 1583 return ImmutableSet.copyOf(result); 1584 } 1585 getContinuations()1586 public Multimap<String, Continuation> getContinuations() { 1587 return continuations; 1588 } 1589 getBaseUnitToStatus()1590 public Map<String, String> getBaseUnitToStatus() { 1591 return baseUnitToStatus; 1592 } 1593 } 1594