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