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