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