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