• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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