xref: /aosp_15_r20/external/cldr/tools/cldr-code/src/main/java/org/unicode/cldr/util/UnitConverter.java (revision 912701f9769bb47905792267661f0baf2b85bed5)
1 package org.unicode.cldr.util;
2 
3 import com.google.common.base.Joiner;
4 import com.google.common.base.Splitter;
5 import com.google.common.collect.BiMap;
6 import com.google.common.collect.ImmutableBiMap;
7 import com.google.common.collect.ImmutableList;
8 import com.google.common.collect.ImmutableMap;
9 import com.google.common.collect.ImmutableMultimap;
10 import com.google.common.collect.ImmutableSet;
11 import com.google.common.collect.ImmutableSet.Builder;
12 import com.google.common.collect.LinkedHashMultimap;
13 import com.google.common.collect.Multimap;
14 import com.google.common.collect.Sets;
15 import com.google.common.collect.TreeMultimap;
16 import com.ibm.icu.impl.Row.R2;
17 import com.ibm.icu.lang.UCharacter;
18 import com.ibm.icu.number.UnlocalizedNumberFormatter;
19 import com.ibm.icu.text.PluralRules;
20 import com.ibm.icu.util.Freezable;
21 import com.ibm.icu.util.Output;
22 import com.ibm.icu.util.ULocale;
23 import java.math.BigInteger;
24 import java.math.MathContext;
25 import java.text.MessageFormat;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.Comparator;
31 import java.util.EnumSet;
32 import java.util.Iterator;
33 import java.util.LinkedHashMap;
34 import java.util.LinkedHashSet;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Map.Entry;
38 import java.util.Objects;
39 import java.util.Set;
40 import java.util.TreeMap;
41 import java.util.TreeSet;
42 import java.util.concurrent.ConcurrentHashMap;
43 import java.util.regex.Matcher;
44 import java.util.regex.Pattern;
45 import java.util.stream.Collectors;
46 import org.unicode.cldr.util.GrammarDerivation.CompoundUnitStructure;
47 import org.unicode.cldr.util.GrammarDerivation.Values;
48 import org.unicode.cldr.util.GrammarInfo.GrammaticalFeature;
49 import org.unicode.cldr.util.Rational.FormatStyle;
50 import org.unicode.cldr.util.Rational.RationalParser;
51 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo;
52 
53 public class UnitConverter implements Freezable<UnitConverter> {
54     public static boolean DEBUG = false;
55     public static final Integer INTEGER_ONE = 1;
56 
57     static final Splitter BAR_SPLITTER = Splitter.on('-');
58     static final Splitter SPACE_SPLITTER = Splitter.on(' ').trimResults().omitEmptyStrings();
59 
60     public static final Set<String> UNTRANSLATED_UNIT_NAMES =
61             ImmutableSet.of("portion", "ofglucose", "100-kilometer", "ofhg");
62 
63     public static final Set<String> HACK_SKIP_UNIT_NAMES =
64             ImmutableSet.of(
65                     // skip dot because pixel is preferred
66                     "dot-per-centimeter",
67                     "dot-per-inch",
68                     // skip because a component is not translated
69                     "liter-per-100-kilometer",
70                     "millimeter-ofhg",
71                     "inch-ofhg");
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, UnitSystem> sourceToSystems = TreeMultimap.create();
81     private Set<String> baseUnits;
82     private Multimap<String, Continuation> continuations = TreeMultimap.create();
83     private Comparator<String> quantityComparator;
84 
85     private Map<String, String> fixDenormalized;
86     private ImmutableMap<String, UnitId> idToUnitId;
87 
88     public final BiMap<String, String> SHORT_TO_LONG_ID = Units.LONG_TO_SHORT.inverse();
89     public final Set<String> LONG_PREFIXES = Units.TYPE_TO_CORE.keySet();
90 
91     private boolean frozen = false;
92 
93     public TargetInfoComparator targetInfoComparator;
94 
95     /** Warning: ordering is important; determines the normalized output */
96     public static final Set<String> BASE_UNITS =
97             ImmutableSet.of(
98                     "candela",
99                     "kilogram",
100                     "meter",
101                     "second",
102                     "ampere",
103                     "kelvin",
104                     // non-SI
105                     "year",
106                     "bit",
107                     "item",
108                     "pixel",
109                     "em",
110                     "revolution",
111                     "portion");
112 
addQuantityInfo(String baseUnit, String quantity, String status)113     public void addQuantityInfo(String baseUnit, String quantity, String status) {
114         if (baseUnitToQuantity.containsKey(baseUnit)) {
115             throw new IllegalArgumentException(
116                     "base unit "
117                             + baseUnit
118                             + " already defined for quantity "
119                             + quantity
120                             + " with status "
121                             + status);
122         }
123         baseUnitToQuantity.put(baseUnit, quantity);
124         if (status != null) {
125             baseUnitToStatus.put(baseUnit, status);
126         }
127         quantityToSimpleUnits.put(quantity, baseUnit);
128     }
129 
130     public static final Set<String> BASE_UNIT_PARTS =
131             ImmutableSet.<String>builder()
132                     .add("per")
133                     .add("square")
134                     .add("cubic")
135                     .add("pow4")
136                     .addAll(BASE_UNITS)
137                     .build();
138 
139     public static final Pattern PLACEHOLDER =
140             Pattern.compile(
141                     "[ \\u00A0\\u200E\\u200F\\u202F]*\\{0\\}[ \\u00A0\\u200E\\u200F\\u202F]*");
142     public static final boolean HACK = true;
143 
144     @Override
isFrozen()145     public boolean isFrozen() {
146         return frozen;
147     }
148 
149     @Override
freeze()150     public UnitConverter freeze() {
151         if (!frozen) {
152             frozen = true;
153             rationalParser.freeze();
154             sourceToTargetInfo = ImmutableMap.copyOf(sourceToTargetInfo);
155             sourceToStandard = buildSourceToStandard();
156             quantityToSimpleUnits = ImmutableMultimap.copyOf(quantityToSimpleUnits);
157             quantityComparator = getQuantityComparator(baseUnitToQuantity, baseUnitToStatus);
158 
159             sourceToSystems = ImmutableMultimap.copyOf(sourceToSystems);
160             // other fields are frozen earlier in processing
161             Builder<String> builder = ImmutableSet.<String>builder().addAll(BASE_UNITS);
162             for (TargetInfo s : sourceToTargetInfo.values()) {
163                 builder.add(s.target);
164             }
165             baseUnits = builder.build();
166             continuations = ImmutableMultimap.copyOf(continuations);
167             targetInfoComparator = new TargetInfoComparator();
168 
169             Map<String, UnitId> _idToUnitId = new TreeMap<>();
170             for (Entry<String, String> shortAndLongId : SHORT_TO_LONG_ID.entrySet()) {
171                 String shortId = shortAndLongId.getKey();
172                 String longId = shortAndLongId.getKey();
173                 UnitId uid = createUnitId(shortId).freeze();
174                 boolean doTest = false;
175                 Output<Rational> deprefix = new Output<>();
176                 for (Entry<String, Integer> entry : uid.numUnitsToPowers.entrySet()) {
177                     final String unitPart = entry.getKey();
178                     UnitConverter.stripPrefix(unitPart, deprefix);
179                     if (!deprefix.value.equals(Rational.ONE)
180                             || !entry.getValue().equals(INTEGER_ONE)) {
181                         doTest = true;
182                         break;
183                     }
184                 }
185                 if (!doTest) {
186                     for (Entry<String, Integer> entry : uid.denUnitsToPowers.entrySet()) {
187                         final String unitPart = entry.getKey();
188                         UnitConverter.stripPrefix(unitPart, deprefix);
189                         if (!deprefix.value.equals(Rational.ONE)) {
190                             doTest = true;
191                             break;
192                         }
193                     }
194                 }
195                 if (doTest) {
196                     _idToUnitId.put(shortId, uid);
197                     _idToUnitId.put(longId, uid);
198                 }
199             }
200             idToUnitId = ImmutableMap.copyOf(_idToUnitId);
201         }
202         return this;
203     }
204 
205     /**
206      * Return the 'standard unit' for the source.
207      *
208      * @return
209      */
buildSourceToStandard()210     private Map<String, String> buildSourceToStandard() {
211         Map<String, String> unitToStandard = new TreeMap<>();
212         for (Entry<String, TargetInfo> entry : sourceToTargetInfo.entrySet()) {
213             String source = entry.getKey();
214             TargetInfo targetInfo = entry.getValue();
215             if (targetInfo.unitInfo.factor.equals(Rational.ONE)
216                     && targetInfo.unitInfo.offset.equals(Rational.ZERO)) {
217                 final String target = targetInfo.target;
218                 String old = unitToStandard.get(target);
219                 if (old == null) {
220                     unitToStandard.put(target, source);
221                     if (DEBUG) System.out.println(target + " ⟹ " + source);
222                 } else if (old.length() > source.length()) {
223                     unitToStandard.put(target, source);
224                     if (DEBUG)
225                         System.out.println(
226                                 "TWO STANDARDS: " + target + " ⟹ " + source + "; was " + old);
227                 } else {
228                     if (DEBUG)
229                         System.out.println(
230                                 "TWO STANDARDS: " + target + " ⟹ " + old + ", was " + source);
231                 }
232             }
233         }
234         return ImmutableMap.copyOf(unitToStandard);
235     }
236 
237     @Override
cloneAsThawed()238     public UnitConverter cloneAsThawed() {
239         throw new UnsupportedOperationException();
240     }
241 
242     public static final class ConversionInfo implements Comparable<ConversionInfo> {
243         public final Rational factor;
244         public final Rational offset;
245         public String special;
246         public boolean specialInverse; // only used with special
247 
248         static final ConversionInfo IDENTITY = new ConversionInfo(Rational.ONE, Rational.ZERO);
249 
ConversionInfo(Rational factor, Rational offset)250         public ConversionInfo(Rational factor, Rational offset) {
251             this.factor = factor;
252             this.offset = offset;
253             this.special = null;
254             this.specialInverse = false;
255         }
256 
ConversionInfo(String special, boolean inverse)257         public ConversionInfo(String special, boolean inverse) {
258             this.factor = Rational.ZERO; // if ONE it will be treated as a base unit
259             this.offset = Rational.ZERO;
260             this.special = special;
261             this.specialInverse = inverse;
262         }
263 
convert(Rational source)264         public Rational convert(Rational source) {
265             if (special != null) {
266                 if (special.equals("beaufort")) {
267                     return (specialInverse)
268                             ? baseToScale(source, minMetersPerSecForBeaufort)
269                             : scaleToBase(source, minMetersPerSecForBeaufort);
270                 }
271                 return source;
272             }
273             return source.multiply(factor).add(offset);
274         }
275 
convertBackwards(Rational source)276         public Rational convertBackwards(Rational source) {
277             if (special != null) {
278                 if (special.equals("beaufort")) {
279                     return (specialInverse)
280                             ? scaleToBase(source, minMetersPerSecForBeaufort)
281                             : baseToScale(source, minMetersPerSecForBeaufort);
282                 }
283                 return source;
284             }
285             return source.subtract(offset).divide(factor);
286         }
287 
288         private static final Rational[] minMetersPerSecForBeaufort = {
289             // minimum m/s values for each Bft value, plus an extra artificial value
290             // from table in Wikipedia, except for artificial value
291             // since 0 based, max Beaufort value is thus array dimension minus 2
292             Rational.of("0.0"), // 0 Bft
293             Rational.of("0.3"), // 1
294             Rational.of("1.6"), // 2
295             Rational.of("3.4"), // 3
296             Rational.of("5.5"), // 4
297             Rational.of("8.0"), // 5
298             Rational.of("10.8"), // 6
299             Rational.of("13.9"), // 7
300             Rational.of("17.2"), // 8
301             Rational.of("20.8"), // 9
302             Rational.of("24.5"), // 10
303             Rational.of("28.5"), // 11
304             Rational.of("32.7"), // 12
305             Rational.of("36.9"), // 13
306             Rational.of("41.4"), // 14
307             Rational.of("46.1"), // 15
308             Rational.of("51.1"), // 16
309             Rational.of("55.8"), // 17
310             Rational.of("61.4"), // artificial end of range 17 to give reasonable midpoint
311         };
312 
scaleToBase(Rational scaleValue, Rational[] minBaseForScaleValues)313         private Rational scaleToBase(Rational scaleValue, Rational[] minBaseForScaleValues) {
314             BigInteger scaleRound = scaleValue.abs().add(Rational.of(1, 2)).floor();
315             BigInteger scaleMax = BigInteger.valueOf(minBaseForScaleValues.length - 2);
316             if (scaleRound.compareTo(scaleMax) > 0) {
317                 scaleRound = scaleMax;
318             }
319             int scaleIndex = scaleRound.intValue();
320             // Return midpont of range (the final range uses an articial end to produce reasonable
321             // midpoint)
322             return minBaseForScaleValues[scaleIndex]
323                     .add(minBaseForScaleValues[scaleIndex + 1])
324                     .divide(Rational.TWO);
325         }
326 
baseToScale(Rational baseValue, Rational[] minBaseForScaleValues)327         private Rational baseToScale(Rational baseValue, Rational[] minBaseForScaleValues) {
328             int scaleIndex = Arrays.binarySearch(minBaseForScaleValues, baseValue.abs());
329             if (scaleIndex < 0) {
330                 // since out first array entry is 0, this value will always be -2 or less
331                 scaleIndex = -scaleIndex - 2;
332             }
333             int scaleMax = minBaseForScaleValues.length - 2;
334             if (scaleIndex > scaleMax) {
335                 scaleIndex = scaleMax;
336             }
337             return Rational.of(scaleIndex);
338         }
339 
invert()340         public ConversionInfo invert() {
341             if (special != null) {
342                 return new ConversionInfo(special, !specialInverse);
343             }
344             Rational factor2 = factor.reciprocal();
345             Rational offset2 =
346                     offset.equals(Rational.ZERO) ? Rational.ZERO : offset.divide(factor).negate();
347             return new ConversionInfo(factor2, offset2);
348             // TODO fix reciprocal
349         }
350 
351         @Override
toString()352         public String toString() {
353             return toString("x");
354         }
355 
toString(String unit)356         public String toString(String unit) {
357             if (special != null) {
358                 return "special" + (specialInverse ? "inv" : "") + ":" + special + "(" + unit + ")";
359             }
360             return factor.toString(FormatStyle.formatted)
361                     + " * "
362                     + unit
363                     + (offset.equals(Rational.ZERO)
364                             ? ""
365                             : (offset.compareTo(Rational.ZERO) < 0 ? " - " : " + ")
366                                     + offset.abs().toString(FormatStyle.formatted));
367         }
368 
toDecimal()369         public String toDecimal() {
370             return toDecimal("x");
371         }
372 
toDecimal(String unit)373         public String toDecimal(String unit) {
374             if (special != null) {
375                 return "special" + (specialInverse ? "inv" : "") + ":" + special + "(" + unit + ")";
376             }
377             return factor.toBigDecimal(MathContext.DECIMAL64)
378                     + " * "
379                     + unit
380                     + (offset.equals(Rational.ZERO)
381                             ? ""
382                             : (offset.compareTo(Rational.ZERO) < 0 ? " - " : " + ")
383                                     + offset.toBigDecimal(MathContext.DECIMAL64).abs());
384         }
385 
386         @Override
compareTo(ConversionInfo o)387         public int compareTo(ConversionInfo o) {
388             // All specials sort at the end
389             int diff;
390             if (special != null) {
391                 if (o.special == null) {
392                     return 1; // This is special, other is not
393                 }
394                 // Both are special check names
395                 if (0 != (diff = special.compareTo(o.special))) {
396                     return diff;
397                 }
398                 // Among specials with the same name, inverses sort later
399                 if (specialInverse != o.specialInverse) {
400                     return (specialInverse) ? 1 : -1;
401                 }
402                 return 0;
403             }
404             if (o.special != null) {
405                 return -1; // This is not special, other is
406             }
407             // Neither this nor other is special
408             if (0 != (diff = factor.compareTo(o.factor))) {
409                 return diff;
410             }
411             return offset.compareTo(o.offset);
412         }
413 
414         @Override
equals(Object obj)415         public boolean equals(Object obj) {
416             return 0 == compareTo((ConversionInfo) obj);
417         }
418 
419         @Override
hashCode()420         public int hashCode() {
421             return Objects.hash(factor, offset, (special == null) ? "" : special);
422         }
423     }
424 
425     public static class Continuation implements Comparable<Continuation> {
426         public final List<String> remainder;
427         public final String result;
428 
addIfNeeded(String source, Multimap<String, Continuation> data)429         public static void addIfNeeded(String source, Multimap<String, Continuation> data) {
430             List<String> sourceParts = BAR_SPLITTER.splitToList(source);
431             if (sourceParts.size() > 1) {
432                 Continuation continuation =
433                         new Continuation(
434                                 ImmutableList.copyOf(sourceParts.subList(1, sourceParts.size())),
435                                 source);
436                 data.put(sourceParts.get(0), continuation);
437             }
438         }
439 
Continuation(List<String> remainder, String source)440         public Continuation(List<String> remainder, String source) {
441             this.remainder = remainder;
442             this.result = source;
443         }
444         /**
445          * The ordering is designed to have longest continuation first so that matching works.
446          * Otherwise the ordering doesn't matter, so we just use the result.
447          */
448         @Override
compareTo(Continuation other)449         public int compareTo(Continuation other) {
450             int diff = other.remainder.size() - remainder.size();
451             if (diff != 0) {
452                 return diff;
453             }
454             return result.compareTo(other.result);
455         }
456 
match(List<String> parts, final int startIndex)457         public boolean match(List<String> parts, final int startIndex) {
458             if (remainder.size() > parts.size() - startIndex) {
459                 return false;
460             }
461             int i = startIndex;
462             for (String unitPart : remainder) {
463                 if (!unitPart.equals(parts.get(i++))) {
464                     return false;
465                 }
466             }
467             return true;
468         }
469 
470         @Override
toString()471         public String toString() {
472             return remainder + " �� " + result;
473         }
474 
split( String derivedUnit, Multimap<String, Continuation> continuations)475         public static UnitIterator split(
476                 String derivedUnit, Multimap<String, Continuation> continuations) {
477             return new UnitIterator(derivedUnit, continuations);
478         }
479 
480         public static class UnitIterator implements Iterable<String>, Iterator<String> {
481             final List<String> parts;
482             final Multimap<String, Continuation> continuations;
483             int nextIndex = 0;
484 
UnitIterator(String derivedUnit, Multimap<String, Continuation> continuations)485             public UnitIterator(String derivedUnit, Multimap<String, Continuation> continuations) {
486                 parts = BAR_SPLITTER.splitToList(derivedUnit);
487                 this.continuations = continuations;
488             }
489 
490             @Override
hasNext()491             public boolean hasNext() {
492                 return nextIndex < parts.size();
493             }
494 
peek()495             public String peek() {
496                 return parts.size() <= nextIndex ? null : parts.get(nextIndex);
497             }
498 
499             @Override
next()500             public String next() {
501                 String result = parts.get(nextIndex++);
502                 Collection<Continuation> continuationOptions = continuations.get(result);
503                 for (Continuation option : continuationOptions) {
504                     if (option.match(parts, nextIndex)) {
505                         nextIndex += option.remainder.size();
506                         return option.result;
507                     }
508                 }
509                 return result;
510             }
511 
512             @Override
iterator()513             public UnitIterator iterator() {
514                 return this;
515             }
516         }
517     }
518 
UnitConverter(RationalParser rationalParser, Validity validity)519     public UnitConverter(RationalParser rationalParser, Validity validity) {
520         this.rationalParser = rationalParser;
521         //        // we need to pass in the validity so it is for the same CLDR version as the
522         // converter
523         //        Set<String> VALID_UNITS =
524         // validity.getStatusToCodes(LstrType.unit).get(Status.regular);
525         //        Map<String,String> _SHORT_TO_LONG_ID = new LinkedHashMap<>();
526         //        for (String longUnit : VALID_UNITS) {
527         //            int dashPos = longUnit.indexOf('-');
528         //            String coreUnit = longUnit.substring(dashPos+1);
529         //            _SHORT_TO_LONG_ID.put(coreUnit, longUnit);
530         //        }
531         //        SHORT_TO_LONG_ID = ImmutableBiMap.copyOf(_SHORT_TO_LONG_ID);
532     }
533 
addRaw( String source, String target, String factor, String offset, String special, String systems)534     public void addRaw(
535             String source,
536             String target,
537             String factor,
538             String offset,
539             String special,
540             String systems) {
541         ConversionInfo info;
542         if (special != null) {
543             info = new ConversionInfo(special, false);
544             if (factor != null || offset != null) {
545                 throw new IllegalArgumentException(
546                         "Cannot have factor or offset with special=" + special);
547             }
548         } else {
549             info =
550                     new ConversionInfo(
551                             factor == null ? Rational.ONE : rationalParser.parse(factor),
552                             offset == null ? Rational.ZERO : rationalParser.parse(offset));
553         }
554         Map<String, String> args = new LinkedHashMap<>();
555         if (factor != null) {
556             args.put("factor", factor);
557         }
558         if (offset != null) {
559             args.put("offset", offset);
560         }
561         if (special != null) {
562             args.put("special", special);
563         }
564 
565         addToSourceToTarget(source, target, info, args, systems);
566         Continuation.addIfNeeded(source, continuations);
567     }
568 
569     public static class TargetInfo {
570         public final String target;
571         public final ConversionInfo unitInfo;
572         public final Map<String, String> inputParameters;
573 
TargetInfo( String target, ConversionInfo unitInfo, Map<String, String> inputParameters)574         public TargetInfo(
575                 String target, ConversionInfo unitInfo, Map<String, String> inputParameters) {
576             this.target = target;
577             this.unitInfo = unitInfo;
578             this.inputParameters = ImmutableMap.copyOf(inputParameters);
579         }
580 
581         @Override
toString()582         public String toString() {
583             return unitInfo + " (" + target + ")";
584         }
585 
formatOriginalSource(String source)586         public String formatOriginalSource(String source) {
587             StringBuilder result =
588                     new StringBuilder()
589                             .append("<convertUnit source='")
590                             .append(source)
591                             .append("' baseUnit='")
592                             .append(target)
593                             .append("'");
594             for (Entry<String, String> entry : inputParameters.entrySet()) {
595                 if (entry.getValue() != null) {
596                     result.append(" " + entry.getKey() + "='" + entry.getValue() + "'");
597                 }
598             }
599             result.append("/>");
600             //            if (unitInfo.equals(UnitInfo.IDENTITY)) {
601             //                result.append("\t<!-- IDENTICAL -->");
602             //            } else {
603             //                result.append("\t<!-- ~")
604             //                .append(unitInfo.toDecimal(target))
605             //                .append(" -->");
606             //            }
607             return result.toString();
608         }
609     }
610 
611     public class TargetInfoComparator implements Comparator<TargetInfo> {
612         @Override
compare(TargetInfo o1, TargetInfo o2)613         public int compare(TargetInfo o1, TargetInfo o2) {
614             String quality1 = baseUnitToQuantity.get(o1.target);
615             String quality2 = baseUnitToQuantity.get(o2.target);
616             int diff;
617             if (0 != (diff = quantityComparator.compare(quality1, quality2))) {
618                 return diff;
619             }
620             if (0 != (diff = o1.unitInfo.compareTo(o2.unitInfo))) {
621                 return diff;
622             }
623             return o1.target.compareTo(o2.target);
624         }
625     }
626 
addToSourceToTarget( String source, String target, ConversionInfo info, Map<String, String> inputParameters, String systems)627     private void addToSourceToTarget(
628             String source,
629             String target,
630             ConversionInfo info,
631             Map<String, String> inputParameters,
632             String systems) {
633         if (sourceToTargetInfo.isEmpty()) {
634             baseUnitToQuantity = ImmutableBiMap.copyOf(baseUnitToQuantity);
635             baseUnitToStatus = ImmutableMap.copyOf(baseUnitToStatus);
636         } else if (sourceToTargetInfo.containsKey(source)) {
637             throw new IllegalArgumentException("Duplicate source: " + source + ", " + target);
638         }
639         sourceToTargetInfo.put(source, new TargetInfo(target, info, inputParameters));
640         String targetQuantity = baseUnitToQuantity.get(target);
641         if (targetQuantity == null) {
642             throw new IllegalArgumentException("No quantity for baseUnit: " + target);
643         }
644         quantityToSimpleUnits.put(targetQuantity, source);
645         if (systems != null) {
646             SPACE_SPLITTER
647                     .splitToList(systems)
648                     .forEach(x -> sourceToSystems.put(source, UnitSystem.valueOf(x)));
649         }
650     }
651 
getQuantityComparator( Map<String, String> baseUnitToQuantity2, Map<String, String> baseUnitToStatus2)652     private Comparator<String> getQuantityComparator(
653             Map<String, String> baseUnitToQuantity2, Map<String, String> baseUnitToStatus2) {
654         // We want to sort all the quantities so that we have a natural ordering within compound
655         // units. So kilowatt-hour, not hour-kilowatt.
656         Collection<String> values;
657         if (true) {
658             values = baseUnitToQuantity2.values();
659         } else {
660             // For simple quantities, just use the ordering from baseUnitToStatus
661             MapComparator<String> simpleBaseUnitComparator =
662                     new MapComparator<>(baseUnitToStatus2.keySet()).freeze();
663             // For non-symbol quantities, use the ordering of the UnitIds
664             Map<UnitId, String> unitIdToQuantity = new TreeMap<>();
665             for (Entry<String, String> buq : baseUnitToQuantity2.entrySet()) {
666                 UnitId uid =
667                         new UnitId(simpleBaseUnitComparator)
668                                 .add(continuations, buq.getKey(), true, 1)
669                                 .freeze();
670                 unitIdToQuantity.put(uid, buq.getValue());
671             }
672             // System.out.println(Joiner.on("\n").join(unitIdToQuantity.values()));
673             values = unitIdToQuantity.values();
674         }
675         if (DEBUG) System.out.println(values);
676         return new MapComparator<>(values).freeze();
677     }
678 
canConvertBetween(String unit)679     public Set<String> canConvertBetween(String unit) {
680         TargetInfo targetInfo = sourceToTargetInfo.get(unit);
681         if (targetInfo == null) {
682             return Collections.emptySet();
683         }
684         String quantity = baseUnitToQuantity.get(targetInfo.target);
685         return getSimpleUnits(quantity);
686     }
687 
getSimpleUnits(String quantity)688     public Set<String> getSimpleUnits(String quantity) {
689         return ImmutableSet.copyOf(quantityToSimpleUnits.get(quantity));
690     }
691 
canConvert()692     public Set<String> canConvert() {
693         return sourceToTargetInfo.keySet();
694     }
695 
696     /** Converts between units, but ONLY if they are both base units */
convertDirect(Rational source, String sourceUnit, String targetUnit)697     public Rational convertDirect(Rational source, String sourceUnit, String targetUnit) {
698         if (sourceUnit.equals(targetUnit)) {
699             return source;
700         }
701         TargetInfo toPivotInfo = sourceToTargetInfo.get(sourceUnit);
702         if (toPivotInfo == null) {
703             return Rational.NaN;
704         }
705         TargetInfo fromPivotInfo = sourceToTargetInfo.get(targetUnit);
706         if (fromPivotInfo == null) {
707             return Rational.NaN;
708         }
709         if (!toPivotInfo.target.equals(fromPivotInfo.target)) {
710             return Rational.NaN;
711         }
712         Rational toPivot = toPivotInfo.unitInfo.convert(source);
713         Rational fromPivot = fromPivotInfo.unitInfo.convertBackwards(toPivot);
714         return fromPivot;
715     }
716 
717     // TODO fix to guarantee single mapping
718 
getUnitInfo(String sourceUnit, Output<String> baseUnit)719     public ConversionInfo getUnitInfo(String sourceUnit, Output<String> baseUnit) {
720         if (isBaseUnit(sourceUnit)) {
721             baseUnit.value = sourceUnit;
722             return ConversionInfo.IDENTITY;
723         }
724         TargetInfo targetToInfo = sourceToTargetInfo.get(sourceUnit);
725         if (targetToInfo == null) {
726             return null;
727         }
728         baseUnit.value = targetToInfo.target;
729         return targetToInfo.unitInfo;
730     }
731 
getBaseUnit(String simpleUnit)732     public String getBaseUnit(String simpleUnit) {
733         TargetInfo targetToInfo = sourceToTargetInfo.get(simpleUnit);
734         if (targetToInfo == null) {
735             return null;
736         }
737         return targetToInfo.target;
738     }
739 
740     /**
741      * Return the standard unit, eg newton for kilogram-meter-per-square-second
742      *
743      * @param simpleUnit
744      * @return
745      */
getStandardUnit(String unit)746     public String getStandardUnit(String unit) {
747         Output<String> metricUnit = new Output<>();
748         parseUnitId(unit, metricUnit, false);
749         String result = sourceToStandard.get(metricUnit.value);
750         if (result == null) {
751             UnitId mUnit = createUnitId(metricUnit.value);
752             mUnit = mUnit.resolve();
753             result = sourceToStandard.get(mUnit.toString());
754             if (result == null) {
755                 mUnit = mUnit.getReciprocal();
756                 result = sourceToStandard.get(mUnit.toString());
757                 if (result != null) {
758                     result = "per-" + result;
759                 }
760             }
761         }
762         return result == null ? metricUnit.value : result;
763     }
764 
765     /**
766      * Reduces a unit, eg square-meter-per-meter-second ==> meter-per-second
767      *
768      * @param unit
769      * @return
770      */
getReducedUnit(String unit)771     public String getReducedUnit(String unit) {
772         UnitId mUnit = createUnitId(unit);
773         mUnit = mUnit.resolve();
774         return mUnit.toString();
775     }
776 
getSpecialBaseUnit(String quantity, Set<UnitSystem> unitSystem)777     public String getSpecialBaseUnit(String quantity, Set<UnitSystem> unitSystem) {
778         if (unitSystem.contains(UnitSystem.ussystem) || unitSystem.contains(UnitSystem.uksystem)) {
779             switch (quantity) {
780                 case "volume":
781                     return unitSystem.contains(UnitSystem.uksystem) ? "gallon-imperial" : "gallon";
782                 case "mass":
783                     return "pound";
784                 case "length":
785                     return "foot";
786                 case "area":
787                     return "square-foot";
788             }
789         }
790         return null;
791     }
792 
793     /**
794      * Takes a derived unit id, and produces the equivalent derived base unit id and UnitInfo to
795      * convert to it
796      *
797      * @author markdavis
798      * @param showYourWork TODO
799      */
parseUnitId( String derivedUnit, Output<String> metricUnit, boolean showYourWork)800     public ConversionInfo parseUnitId(
801             String derivedUnit, Output<String> metricUnit, boolean showYourWork) {
802         // First check whether we are dealing with a special mapping
803         Output<String> testBaseUnit = new Output<>();
804         ConversionInfo testInfo = getUnitInfo(derivedUnit, testBaseUnit);
805         if (testInfo != null && testInfo.special != null) {
806             metricUnit.value = testBaseUnit.value;
807             return new ConversionInfo(testInfo.special, testInfo.specialInverse);
808         }
809         // Not a special mapping, proceed as usual
810         metricUnit.value = null;
811 
812         UnitId outputUnit = new UnitId(UNIT_COMPARATOR);
813         Rational numerator = Rational.ONE;
814         Rational denominator = Rational.ONE;
815         boolean inNumerator = true;
816         int power = 1;
817 
818         Output<Rational> deprefix = new Output<>();
819         Rational offset = Rational.ZERO;
820         int countUnits = 0;
821         for (Iterator<String> it = Continuation.split(derivedUnit, continuations).iterator();
822                 it.hasNext(); ) {
823             String unit = it.next();
824             ++countUnits;
825             if (unit.equals("square")) {
826                 if (power != 1) {
827                     throw new IllegalArgumentException("Can't have power of " + unit);
828                 }
829                 power = 2;
830                 if (showYourWork)
831                     System.out.println(
832                             showRational("\t " + unit + ": ", Rational.of(power), "power"));
833             } else if (unit.equals("cubic")) {
834                 if (power != 1) {
835                     throw new IllegalArgumentException("Can't have power of " + unit);
836                 }
837                 power = 3;
838                 if (showYourWork)
839                     System.out.println(
840                             showRational("\t " + unit + ": ", Rational.of(power), "power"));
841             } else if (unit.startsWith("pow")) {
842                 if (power != 1) {
843                     throw new IllegalArgumentException("Can't have power of " + unit);
844                 }
845                 power = Integer.parseInt(unit.substring(3));
846                 if (showYourWork)
847                     System.out.println(
848                             showRational("\t " + unit + ": ", Rational.of(power), "power"));
849             } else if (unit.equals("per")) {
850                 if (power != 1) {
851                     throw new IllegalArgumentException("Can't have power of per");
852                 }
853                 if (showYourWork && inNumerator) System.out.println("\tper");
854                 inNumerator = false; // ignore multiples
855                 //            } else if ('9' >= unit.charAt(0)) {
856                 //                if (power != 1) {
857                 //                    throw new IllegalArgumentException("Can't have power of " +
858                 // unit);
859                 //                }
860                 //                Rational factor = Rational.of(Integer.parseInt(unit));
861                 //                if (inNumerator) {
862                 //                    numerator = numerator.multiply(factor);
863                 //                } else {
864                 //                    denominator = denominator.multiply(factor);
865                 //                }
866             } else {
867                 // kilo etc.
868                 unit = stripPrefix(unit, deprefix);
869                 if (showYourWork) {
870                     if (!deprefix.value.equals(Rational.ONE)) {
871                         System.out.println(showRational("\tprefix: ", deprefix.value, unit));
872                     } else {
873                         System.out.println("\t" + unit);
874                     }
875                 }
876 
877                 Rational value = deprefix.value;
878                 if (!isSimpleBaseUnit(unit)) {
879                     TargetInfo info = sourceToTargetInfo.get(unit);
880                     if (info == null) {
881                         if (showYourWork) System.out.println("\t⟹ no conversion for: " + unit);
882                         return null; // can't convert
883                     }
884                     String baseUnit = info.target;
885 
886                     value =
887                             (info.unitInfo.special == null)
888                                     ? info.unitInfo.factor.multiply(value)
889                                     : info.unitInfo.convert(value);
890                     // if (showYourWork && !info.unitInfo.factor.equals(Rational.ONE))
891                     // System.out.println(showRational("\tfactor: ", info.unitInfo.factor,
892                     // baseUnit));
893                     // Special handling for offsets. We disregard them if there are any other units.
894                     if (countUnits == 1 && !it.hasNext()) {
895                         offset = info.unitInfo.offset;
896                         if (showYourWork && !info.unitInfo.offset.equals(Rational.ZERO))
897                             System.out.println(
898                                     showRational("\toffset: ", info.unitInfo.offset, baseUnit));
899                     }
900                     unit = baseUnit;
901                 }
902                 for (int p = 1; p <= power; ++p) {
903                     String title = "";
904                     if (value.equals(Rational.ONE)) {
905                         if (showYourWork) System.out.println("\t(already base unit)");
906                         continue;
907                     } else if (inNumerator) {
908                         numerator = numerator.multiply(value);
909                         title = "\t× ";
910                     } else {
911                         denominator = denominator.multiply(value);
912                         title = "\t÷ ";
913                     }
914                     if (showYourWork)
915                         System.out.println(
916                                 showRational("\t× ", value, " ⟹ " + unit)
917                                         + "\t"
918                                         + numerator.divide(denominator)
919                                         + "\t"
920                                         + numerator.divide(denominator).doubleValue());
921                 }
922                 // create cleaned up target unitid
923                 outputUnit.add(continuations, unit, inNumerator, power);
924                 power = 1;
925             }
926         }
927         metricUnit.value = outputUnit.toString();
928         return new ConversionInfo(numerator.divide(denominator), offset);
929     }
930 
931     /** Only for use for simple base unit comparison */
932     // Thus we do not need to handle specials here
933     private class UnitComparator implements Comparator<String> {
934         // TODO, use order in units.xml
935 
936         @Override
compare(String o1, String o2)937         public int compare(String o1, String o2) {
938             if (o1.equals(o2)) {
939                 return 0;
940             }
941             Output<Rational> deprefix1 = new Output<>();
942             o1 = stripPrefix(o1, deprefix1);
943             TargetInfo targetAndInfo1 = sourceToTargetInfo.get(o1);
944             String quantity1 = baseUnitToQuantity.get(targetAndInfo1.target);
945 
946             Output<Rational> deprefix2 = new Output<>();
947             o2 = stripPrefix(o2, deprefix2);
948             TargetInfo targetAndInfo2 = sourceToTargetInfo.get(o2);
949             String quantity2 = baseUnitToQuantity.get(targetAndInfo2.target);
950 
951             int diff;
952             if (0 != (diff = quantityComparator.compare(quantity1, quantity2))) {
953                 return diff;
954             }
955             Rational factor1 = targetAndInfo1.unitInfo.factor.multiply(deprefix1.value);
956             Rational factor2 = targetAndInfo2.unitInfo.factor.multiply(deprefix2.value);
957             if (0 != (diff = factor1.compareTo(factor2))) {
958                 return diff;
959             }
960             return o1.compareTo(o2);
961         }
962     }
963 
964     Comparator<String> UNIT_COMPARATOR = new UnitComparator();
965 
966     /** Only handles the canonical units; no kilo-, only normalized, etc. */
967     // Thus we do not need to handle specials here
968     // TODO: optimize
969     // • the comparators don't have to be fields in this class;
970     //   it is not a static class, so they can be on the converter.
971     // • We can cache the frozen UnitIds, avoiding the parse times
972 
973     public class UnitId implements Freezable<UnitId>, Comparable<UnitId> {
974         public Map<String, Integer> numUnitsToPowers;
975         public Map<String, Integer> denUnitsToPowers;
976         public EntrySetComparator<String, Integer> entrySetComparator;
977         public final Comparator<String> comparator;
978         private boolean frozen = false;
979 
UnitId(Comparator<String> comparator)980         private UnitId(Comparator<String> comparator) {
981             this.comparator = comparator;
982             numUnitsToPowers = new TreeMap<>(comparator);
983             denUnitsToPowers = new TreeMap<>(comparator);
984             entrySetComparator =
985                     new EntrySetComparator<String, Integer>(comparator, Comparator.naturalOrder());
986         } //
987 
getReciprocal()988         public UnitId getReciprocal() {
989             UnitId result = new UnitId(comparator);
990             result.entrySetComparator = entrySetComparator;
991             result.numUnitsToPowers = denUnitsToPowers;
992             result.denUnitsToPowers = numUnitsToPowers;
993             return result;
994         }
995 
add( Multimap<String, Continuation> continuations, String compoundUnit, boolean groupInNumerator, int groupPower)996         private UnitId add(
997                 Multimap<String, Continuation> continuations,
998                 String compoundUnit,
999                 boolean groupInNumerator,
1000                 int groupPower) {
1001             if (frozen) {
1002                 throw new UnsupportedOperationException("Object is frozen.");
1003             }
1004             boolean inNumerator = true;
1005             int power = 1;
1006             // maybe refactor common parts with above code.
1007             for (String unitPart : Continuation.split(compoundUnit, continuations)) {
1008                 switch (unitPart) {
1009                     case "square":
1010                         power = 2;
1011                         break;
1012                     case "cubic":
1013                         power = 3;
1014                         break;
1015                     case "per":
1016                         inNumerator = false;
1017                         break; // sticky, ignore multiples
1018                     default:
1019                         if (unitPart.startsWith("pow")) {
1020                             power = Integer.parseInt(unitPart.substring(3));
1021                         } else {
1022                             Map<String, Integer> target =
1023                                     inNumerator == groupInNumerator
1024                                             ? numUnitsToPowers
1025                                             : denUnitsToPowers;
1026                             Integer oldPower = target.get(unitPart);
1027                             // we multiply powers, so that weight-square-volume =>
1028                             // weight-pow4-length
1029                             int newPower = groupPower * power + (oldPower == null ? 0 : oldPower);
1030                             target.put(unitPart, newPower);
1031                             power = 1;
1032                         }
1033                 }
1034             }
1035             return this;
1036         }
1037 
1038         @Override
toString()1039         public String toString() {
1040             StringBuilder builder = new StringBuilder();
1041             boolean firstDenominator = true;
1042             for (int i = 1; i >= 0; --i) { // two passes, numerator then den.
1043                 boolean positivePass = i > 0;
1044                 Map<String, Integer> target = positivePass ? numUnitsToPowers : denUnitsToPowers;
1045                 for (Entry<String, Integer> entry : target.entrySet()) {
1046                     String unit = entry.getKey();
1047                     int power = entry.getValue();
1048                     // NOTE: zero (eg one-per-one) gets counted twice
1049                     if (builder.length() != 0) {
1050                         builder.append('-');
1051                     }
1052                     if (!positivePass) {
1053                         if (firstDenominator) {
1054                             firstDenominator = false;
1055                             builder.append("per-");
1056                         }
1057                     }
1058                     switch (power) {
1059                         case 1:
1060                             break;
1061                         case 2:
1062                             builder.append("square-");
1063                             break;
1064                         case 3:
1065                             builder.append("cubic-");
1066                             break;
1067                         default:
1068                             if (power > 3) {
1069                                 builder.append("pow" + power + "-");
1070                             } else {
1071                                 throw new IllegalArgumentException("Unhandled power: " + power);
1072                             }
1073                             break;
1074                     }
1075                     builder.append(unit);
1076                 }
1077             }
1078             return builder.toString();
1079         }
1080 
toString( LocaleStringProvider resolvedFile, String width, String _pluralCategory, String caseVariant, Multimap<UnitPathType, String> partsUsed, boolean maximal)1081         public String toString(
1082                 LocaleStringProvider resolvedFile,
1083                 String width,
1084                 String _pluralCategory,
1085                 String caseVariant,
1086                 Multimap<UnitPathType, String> partsUsed,
1087                 boolean maximal) {
1088             if (partsUsed != null) {
1089                 partsUsed.clear();
1090             }
1091             String result = null;
1092             String numerator = null;
1093             String timesPattern = null;
1094             String placeholderPattern = null;
1095             Output<Integer> deprefix = new Output<>();
1096 
1097             PlaceholderLocation placeholderPosition = PlaceholderLocation.missing;
1098             Matcher placeholderMatcher = PLACEHOLDER.matcher("");
1099             Output<String> unitPatternOut = new Output<>();
1100 
1101             PluralInfo pluralInfo =
1102                     CLDRConfig.getInstance()
1103                             .getSupplementalDataInfo()
1104                             .getPlurals(resolvedFile.getLocaleID());
1105             PluralRules pluralRules = pluralInfo.getPluralRules();
1106             String singularPluralCategory = pluralRules.select(1d);
1107             final ULocale locale = new ULocale(resolvedFile.getLocaleID());
1108             String fullPerPattern = null;
1109             int negCount = 0;
1110 
1111             for (int i = 1; i >= 0; --i) { // two passes, numerator then den.
1112                 boolean positivePass = i > 0;
1113                 if (!positivePass) {
1114                     switch (locale.toString()) {
1115                         case "de":
1116                             caseVariant = "accusative";
1117                             break; // German pro rule
1118                     }
1119                     numerator = result; // from now on, result ::= denominator
1120                     result = null;
1121                 }
1122 
1123                 Map<String, Integer> target = positivePass ? numUnitsToPowers : denUnitsToPowers;
1124                 int unitsLeft = target.size();
1125                 for (Entry<String, Integer> entry : target.entrySet()) {
1126                     String possiblyPrefixedUnit = entry.getKey();
1127                     String unit = stripPrefixPower(possiblyPrefixedUnit, deprefix);
1128                     String genderVariant =
1129                             UnitPathType.gender.getTrans(
1130                                     resolvedFile, "long", unit, null, null, null, partsUsed);
1131 
1132                     int power = entry.getValue();
1133                     unitsLeft--;
1134                     String pluralCategory =
1135                             unitsLeft == 0 && positivePass
1136                                     ? _pluralCategory
1137                                     : singularPluralCategory;
1138 
1139                     if (!positivePass) {
1140                         if (maximal && 0 == negCount++) { // special case exact match for per form,
1141                             // and no previous result
1142                             if (true) {
1143                                 throw new UnsupportedOperationException(
1144                                         "not yet implemented fully");
1145                             }
1146                             String fullUnit;
1147                             switch (power) {
1148                                 case 1:
1149                                     fullUnit = unit;
1150                                     break;
1151                                 case 2:
1152                                     fullUnit = "square-" + unit;
1153                                     break;
1154                                 case 3:
1155                                     fullUnit = "cubic-" + unit;
1156                                     break;
1157                                 default:
1158                                     throw new IllegalArgumentException("powers > 3 not supported");
1159                             }
1160                             fullPerPattern =
1161                                     UnitPathType.perUnit.getTrans(
1162                                             resolvedFile,
1163                                             width,
1164                                             fullUnit,
1165                                             _pluralCategory,
1166                                             caseVariant,
1167                                             genderVariant,
1168                                             partsUsed);
1169                             // if there is a special form, we'll use it
1170                             if (fullPerPattern != null) {
1171                                 continue;
1172                             }
1173                         }
1174                     }
1175 
1176                     // handle prefix, like kilo-
1177                     String prefixPattern = null;
1178                     if (deprefix.value != 1) {
1179                         prefixPattern =
1180                                 UnitPathType.prefix.getTrans(
1181                                         resolvedFile,
1182                                         width,
1183                                         "10p" + deprefix.value,
1184                                         _pluralCategory,
1185                                         caseVariant,
1186                                         genderVariant,
1187                                         partsUsed);
1188                     }
1189 
1190                     // get the core pattern. Detect and remove the the placeholder (and surrounding
1191                     // spaces)
1192                     String unitPattern =
1193                             UnitPathType.unit.getTrans(
1194                                     resolvedFile,
1195                                     width,
1196                                     unit,
1197                                     pluralCategory,
1198                                     caseVariant,
1199                                     genderVariant,
1200                                     partsUsed);
1201                     if (unitPattern == null) {
1202                         return null; // unavailable
1203                     }
1204                     // we are set up for 2 kinds of placeholder patterns for units. {0}\s?stuff or
1205                     // stuff\s?{0}, or nothing(Eg Arabic)
1206                     placeholderPosition =
1207                             extractUnit(placeholderMatcher, unitPattern, unitPatternOut);
1208                     if (placeholderPosition == PlaceholderLocation.middle) {
1209                         return null; // signal we can't handle, but shouldn't happen with
1210                         // well-formed data.
1211                     } else if (placeholderPosition != PlaceholderLocation.missing) {
1212                         unitPattern = unitPatternOut.value;
1213                         placeholderPattern = placeholderMatcher.group();
1214                     }
1215 
1216                     // we have all the pieces, so build it up
1217                     if (prefixPattern != null) {
1218                         unitPattern = combineLowercasing(locale, width, prefixPattern, unitPattern);
1219                     }
1220 
1221                     String powerPattern = null;
1222                     switch (power) {
1223                         case 1:
1224                             break;
1225                         case 2:
1226                             powerPattern =
1227                                     UnitPathType.power.getTrans(
1228                                             resolvedFile,
1229                                             width,
1230                                             "power2",
1231                                             pluralCategory,
1232                                             caseVariant,
1233                                             genderVariant,
1234                                             partsUsed);
1235                             break;
1236                         case 3:
1237                             powerPattern =
1238                                     UnitPathType.power.getTrans(
1239                                             resolvedFile,
1240                                             width,
1241                                             "power3",
1242                                             pluralCategory,
1243                                             caseVariant,
1244                                             genderVariant,
1245                                             partsUsed);
1246                             break;
1247                         default:
1248                             throw new IllegalArgumentException("No power pattern > 3: " + this);
1249                     }
1250 
1251                     if (powerPattern != null) {
1252                         unitPattern = combineLowercasing(locale, width, powerPattern, unitPattern);
1253                     }
1254 
1255                     if (result != null) {
1256                         if (timesPattern == null) {
1257                             timesPattern = getTimesPattern(resolvedFile, width);
1258                         }
1259                         result = MessageFormat.format(timesPattern, result, unitPattern);
1260                     } else {
1261                         result = unitPattern;
1262                     }
1263                 }
1264             }
1265 
1266             // if there is a fullPerPattern, then we use it instead of per pattern + first
1267             // denominator element
1268             if (fullPerPattern != null) {
1269                 if (numerator != null) {
1270                     numerator = MessageFormat.format(fullPerPattern, numerator);
1271                 } else {
1272                     numerator = fullPerPattern;
1273                     placeholderPattern = null;
1274                 }
1275                 if (result != null) {
1276                     if (timesPattern == null) {
1277                         timesPattern = getTimesPattern(resolvedFile, width);
1278                     }
1279                     numerator = MessageFormat.format(timesPattern, numerator, result);
1280                 }
1281                 result = numerator;
1282             } else {
1283                 // glue the two parts together, if we have two of them
1284                 if (result == null) {
1285                     result = numerator;
1286                 } else {
1287                     String perPattern =
1288                             UnitPathType.per.getTrans(
1289                                     resolvedFile,
1290                                     width,
1291                                     null,
1292                                     _pluralCategory,
1293                                     caseVariant,
1294                                     null,
1295                                     partsUsed);
1296                     if (numerator == null) {
1297                         result = MessageFormat.format(perPattern, "", result).trim();
1298                     } else {
1299                         result = MessageFormat.format(perPattern, numerator, result);
1300                     }
1301                 }
1302             }
1303             return addPlaceholder(result, placeholderPattern, placeholderPosition);
1304         }
1305 
getTimesPattern( LocaleStringProvider resolvedFile, String width)1306         public String getTimesPattern(
1307                 LocaleStringProvider resolvedFile, String width) { // TODO fix hack!
1308             if (HACK && "en".equals(resolvedFile.getLocaleID())) {
1309                 return "{0}-{1}";
1310             }
1311             String timesPatternPath =
1312                     "//ldml/units/unitLength[@type=\""
1313                             + width
1314                             + "\"]/compoundUnit[@type=\"times\"]/compoundUnitPattern";
1315             return resolvedFile.getStringValue(timesPatternPath);
1316         }
1317 
1318         @Override
equals(Object obj)1319         public boolean equals(Object obj) {
1320             UnitId other = (UnitId) obj;
1321             return numUnitsToPowers.equals(other.numUnitsToPowers)
1322                     && denUnitsToPowers.equals(other.denUnitsToPowers);
1323         }
1324 
1325         @Override
hashCode()1326         public int hashCode() {
1327             return Objects.hash(numUnitsToPowers, denUnitsToPowers);
1328         }
1329 
1330         @Override
isFrozen()1331         public boolean isFrozen() {
1332             return frozen;
1333         }
1334 
1335         @Override
freeze()1336         public UnitId freeze() {
1337             frozen = true;
1338             numUnitsToPowers = ImmutableMap.copyOf(numUnitsToPowers);
1339             denUnitsToPowers = ImmutableMap.copyOf(denUnitsToPowers);
1340             return this;
1341         }
1342 
1343         @Override
cloneAsThawed()1344         public UnitId cloneAsThawed() {
1345             throw new UnsupportedOperationException();
1346         }
1347 
resolve()1348         public UnitId resolve() {
1349             UnitId result = new UnitId(UNIT_COMPARATOR);
1350             result.numUnitsToPowers.putAll(numUnitsToPowers);
1351             result.denUnitsToPowers.putAll(denUnitsToPowers);
1352             for (Entry<String, Integer> entry : numUnitsToPowers.entrySet()) {
1353                 final String key = entry.getKey();
1354                 Integer denPower = denUnitsToPowers.get(key);
1355                 if (denPower == null) {
1356                     continue;
1357                 }
1358                 int power = entry.getValue() - denPower;
1359                 if (power > 0) {
1360                     result.numUnitsToPowers.put(key, power);
1361                     result.denUnitsToPowers.remove(key);
1362                 } else if (power < 0) {
1363                     result.numUnitsToPowers.remove(key);
1364                     result.denUnitsToPowers.put(key, -power);
1365                 } else { // 0, so
1366                     result.numUnitsToPowers.remove(key);
1367                     result.denUnitsToPowers.remove(key);
1368                 }
1369             }
1370             return result.freeze();
1371         }
1372 
1373         @Override
compareTo(UnitId o)1374         public int compareTo(UnitId o) {
1375             int diff =
1376                     compareEntrySets(
1377                             numUnitsToPowers.entrySet(),
1378                             o.numUnitsToPowers.entrySet(),
1379                             entrySetComparator);
1380             if (diff != 0) return diff;
1381             return compareEntrySets(
1382                     denUnitsToPowers.entrySet(), o.denUnitsToPowers.entrySet(), entrySetComparator);
1383         }
1384 
1385         /**
1386          * Default rules Prefixes & powers: the gender of the whole is the same as the gender of the
1387          * operand. In pseudocode: gender(square, meter) = gender(meter) gender(kilo, meter) =
1388          * gender(meter)
1389          *
1390          * <p>Per: the gender of the whole is the gender of the numerator. If there is no numerator,
1391          * then the gender of the denominator gender(gram per meter) = gender(gram)
1392          *
1393          * <p>Times: the gender of the whole is the gender of the last operand gender(gram-meter) =
1394          * gender(gram)
1395          *
1396          * @param source
1397          * @param partsUsed
1398          * @return TODO: add parameter to short-circuit the lookup if the unit is not a compound.
1399          */
getGender( CLDRFile resolvedFile, Output<String> source, Multimap<UnitPathType, String> partsUsed)1400         public String getGender(
1401                 CLDRFile resolvedFile,
1402                 Output<String> source,
1403                 Multimap<UnitPathType, String> partsUsed) {
1404             // will not be empty
1405 
1406             GrammarDerivation gd = null;
1407             // Values power = gd.get(GrammaticalFeature.grammaticalGender,
1408             // CompoundUnitStructure.power); no data available yet
1409             // Values prefix = gd.get(GrammaticalFeature.grammaticalGender,
1410             // CompoundUnitStructure.prefix);
1411 
1412             Map<String, Integer> determiner;
1413             if (numUnitsToPowers.isEmpty()) {
1414                 determiner = denUnitsToPowers;
1415             } else if (denUnitsToPowers.isEmpty()) {
1416                 determiner = numUnitsToPowers;
1417             } else {
1418                 if (gd == null) {
1419                     gd =
1420                             SupplementalDataInfo.getInstance()
1421                                     .getGrammarDerivation(resolvedFile.getLocaleID());
1422                 }
1423                 Values per =
1424                         gd.get(GrammaticalFeature.grammaticalGender, CompoundUnitStructure.per);
1425                 boolean useFirst = per.value0.equals("0");
1426                 determiner =
1427                         useFirst
1428                                 ? numUnitsToPowers // otherwise use numerator if possible
1429                                 : denUnitsToPowers;
1430                 // TODO add test that the value is 0 or 1, so that if it fails we know to upgrade
1431                 // this code.
1432             }
1433 
1434             Entry<String, Integer> bestMeasure;
1435             if (determiner.size() == 1) {
1436                 bestMeasure = determiner.entrySet().iterator().next();
1437             } else {
1438                 if (gd == null) {
1439                     gd =
1440                             SupplementalDataInfo.getInstance()
1441                                     .getGrammarDerivation(resolvedFile.getLocaleID());
1442                 }
1443                 Values times =
1444                         gd.get(GrammaticalFeature.grammaticalGender, CompoundUnitStructure.times);
1445                 boolean useFirst = times.value0.equals("0");
1446                 if (useFirst) {
1447                     bestMeasure = determiner.entrySet().iterator().next();
1448                 } else {
1449                     bestMeasure = null; // we know the determiner is not empty, but this makes the
1450                     // compiler
1451                     for (Entry<String, Integer> entry : determiner.entrySet()) {
1452                         bestMeasure = entry;
1453                     }
1454                 }
1455             }
1456             String strippedUnit = stripPrefix(bestMeasure.getKey(), null);
1457             String gender =
1458                     UnitPathType.gender.getTrans(
1459                             resolvedFile, "long", strippedUnit, null, null, null, partsUsed);
1460             if (gender != null && source != null) {
1461                 source.value = strippedUnit;
1462             }
1463             return gender;
1464         }
1465 
times(UnitId id2)1466         public UnitId times(UnitId id2) {
1467             UnitId result = new UnitId(comparator);
1468             combine(numUnitsToPowers, id2.numUnitsToPowers, result.numUnitsToPowers);
1469             combine(denUnitsToPowers, id2.denUnitsToPowers, result.denUnitsToPowers);
1470             return result;
1471         }
1472 
combine( Map<String, Integer> map1, Map<String, Integer> map2, Map<String, Integer> resultMap)1473         public void combine(
1474                 Map<String, Integer> map1,
1475                 Map<String, Integer> map2,
1476                 Map<String, Integer> resultMap) {
1477             Set<String> units = Sets.union(map1.keySet(), map2.keySet());
1478             for (String unit : units) {
1479                 Integer int1 = map1.get(unit);
1480                 Integer int2 = map2.get(unit);
1481                 resultMap.put(unit, (int1 == null ? 0 : int1) + (int2 == null ? 0 : int2));
1482             }
1483         }
1484     }
1485 
1486     public enum PlaceholderLocation {
1487         before,
1488         middle,
1489         after,
1490         missing
1491     }
1492 
addPlaceholder( String result, String placeholderPattern, PlaceholderLocation placeholderPosition)1493     public static String addPlaceholder(
1494             String result, String placeholderPattern, PlaceholderLocation placeholderPosition) {
1495         return placeholderPattern == null
1496                 ? result
1497                 : placeholderPosition == PlaceholderLocation.before
1498                         ? placeholderPattern + result
1499                         : result + placeholderPattern;
1500     }
1501 
1502     /**
1503      * Returns the location of the placeholder. Call placeholderMatcher.group() after calling this
1504      * to get the placeholder.
1505      *
1506      * @param placeholderMatcher
1507      * @param unitPattern
1508      * @param unitPatternOut
1509      * @param before
1510      * @return
1511      */
extractUnit( Matcher placeholderMatcher, String unitPattern, Output<String> unitPatternOut)1512     public static PlaceholderLocation extractUnit(
1513             Matcher placeholderMatcher, String unitPattern, Output<String> unitPatternOut) {
1514         if (placeholderMatcher.reset(unitPattern).find()) {
1515             if (placeholderMatcher.start() == 0) {
1516                 unitPatternOut.value = unitPattern.substring(placeholderMatcher.end());
1517                 return PlaceholderLocation.before;
1518             } else if (placeholderMatcher.end() == unitPattern.length()) {
1519                 unitPatternOut.value = unitPattern.substring(0, placeholderMatcher.start());
1520                 return PlaceholderLocation.after;
1521             } else {
1522                 unitPatternOut.value = unitPattern;
1523                 return PlaceholderLocation.middle;
1524             }
1525         } else {
1526             unitPatternOut.value = unitPattern;
1527             return PlaceholderLocation.missing;
1528         }
1529     }
1530 
combineLowercasing( final ULocale locale, String width, String prefixPattern, String unitPattern)1531     public static String combineLowercasing(
1532             final ULocale locale, String width, String prefixPattern, String unitPattern) {
1533         // catch special case, ZentiLiter
1534         if (width.equals("long")
1535                 && !prefixPattern.contains(" {")
1536                 && !prefixPattern.contains(" {")) {
1537             unitPattern = UCharacter.toLowerCase(locale, unitPattern);
1538         }
1539         unitPattern = MessageFormat.format(prefixPattern, unitPattern);
1540         return unitPattern;
1541     }
1542 
1543     public static class EntrySetComparator<K extends Comparable<K>, V>
1544             implements Comparator<Entry<K, V>> {
1545         Comparator<K> kComparator;
1546         Comparator<V> vComparator;
1547 
EntrySetComparator(Comparator<K> kComparator, Comparator<V> vComparator)1548         public EntrySetComparator(Comparator<K> kComparator, Comparator<V> vComparator) {
1549             this.kComparator = kComparator;
1550             this.vComparator = vComparator;
1551         }
1552 
1553         @Override
compare(Entry<K, V> o1, Entry<K, V> o2)1554         public int compare(Entry<K, V> o1, Entry<K, V> o2) {
1555             int diff = kComparator.compare(o1.getKey(), o2.getKey());
1556             if (diff != 0) {
1557                 return diff;
1558             }
1559             diff = vComparator.compare(o1.getValue(), o2.getValue());
1560             if (diff != 0) {
1561                 return diff;
1562             }
1563             return o1.getKey().compareTo(o2.getKey());
1564         }
1565     }
1566 
1567     public static <K extends Comparable<K>, V extends Comparable<V>, T extends Entry<K, V>>
compareEntrySets(Collection<T> o1, Collection<T> o2, Comparator<T> comparator)1568             int compareEntrySets(Collection<T> o1, Collection<T> o2, Comparator<T> comparator) {
1569         Iterator<T> iterator1 = o1.iterator();
1570         Iterator<T> iterator2 = o2.iterator();
1571         while (true) {
1572             if (!iterator1.hasNext()) {
1573                 return iterator2.hasNext() ? -1 : 0;
1574             } else if (!iterator2.hasNext()) {
1575                 return 1;
1576             }
1577             T item1 = iterator1.next();
1578             T item2 = iterator2.next();
1579             int diff = comparator.compare(item1, item2);
1580             if (diff != 0) {
1581                 return diff;
1582             }
1583         }
1584     }
1585 
1586     private ConcurrentHashMap<String, UnitId> UNIT_ID = new ConcurrentHashMap<>();
1587     // TODO This is safe but should use regular cache
createUnitId(String unit)1588     public final UnitId createUnitId(String unit) {
1589         UnitId result = UNIT_ID.get(unit);
1590         if (result == null) {
1591             result = new UnitId(UNIT_COMPARATOR).add(continuations, unit, true, 1).freeze();
1592             UNIT_ID.put(unit, result);
1593         }
1594         return result;
1595     }
1596 
isBaseUnit(String unit)1597     public boolean isBaseUnit(String unit) {
1598         return baseUnits.contains(unit);
1599     }
1600 
isSimpleBaseUnit(String unit)1601     public boolean isSimpleBaseUnit(String unit) {
1602         return BASE_UNITS.contains(unit);
1603     }
1604 
baseUnits()1605     public Set<String> baseUnits() {
1606         return baseUnits;
1607     }
1608 
1609     // TODO change to TRIE if the performance isn't good enough, or restructure with regex
1610     // https://www.nist.gov/pml/owm/metric-si-prefixes
1611     public static final ImmutableMap<String, Integer> PREFIX_POWERS =
1612             ImmutableMap.<String, Integer>builder()
1613                     .put("quecto", -30)
1614                     .put("ronto", -27)
1615                     .put("yocto", -24)
1616                     .put("zepto", -21)
1617                     .put("atto", -18)
1618                     .put("femto", -15)
1619                     .put("pico", -12)
1620                     .put("nano", -9)
1621                     .put("micro", -6)
1622                     .put("milli", -3)
1623                     .put("centi", -2)
1624                     .put("deci", -1)
1625                     .put("deka", 1)
1626                     .put("hecto", 2)
1627                     .put("kilo", 3)
1628                     .put("mega", 6)
1629                     .put("giga", 9)
1630                     .put("tera", 12)
1631                     .put("peta", 15)
1632                     .put("exa", 18)
1633                     .put("zetta", 21)
1634                     .put("yotta", 24)
1635                     .put("ronna", 27)
1636                     .put("quetta", 30)
1637                     .build();
1638 
1639     public static final ImmutableMap<String, Rational> PREFIXES;
1640 
1641     static {
1642         Map<String, Rational> temp = new LinkedHashMap<>();
1643         for (Entry<String, Integer> entry : PREFIX_POWERS.entrySet()) {
entry.getKey()1644             temp.put(entry.getKey(), Rational.pow10(entry.getValue()));
1645         }
1646         PREFIXES = ImmutableMap.copyOf(temp);
1647     }
1648 
1649     public static final Set<String> METRIC_TAKING_PREFIXES =
1650             ImmutableSet.of(
1651                     "bit", "byte", "liter", "tonne", "degree", "celsius", "kelvin", "calorie",
1652                     "bar");
1653     public static final Set<String> METRIC_TAKING_BINARY_PREFIXES = ImmutableSet.of("bit", "byte");
1654 
1655     static final Set<String> SKIP_PREFIX =
1656             ImmutableSet.of("millimeter-ofhg", "kilogram", "kilogram-force");
1657 
1658     static final Rational RATIONAL1000 = Rational.of(1000);
1659     /**
1660      * If there is no prefix, return the unit and ONE. If there is a prefix return the unit (with
1661      * prefix stripped) and the prefix factor
1662      */
stripPrefixCommon( String unit, Output<V> deprefix, Map<String, V> unitMap)1663     public static <V> String stripPrefixCommon(
1664             String unit, Output<V> deprefix, Map<String, V> unitMap) {
1665         if (SKIP_PREFIX.contains(unit)) {
1666             return unit;
1667         }
1668 
1669         for (Entry<String, V> entry : unitMap.entrySet()) {
1670             String prefix = entry.getKey();
1671             if (unit.startsWith(prefix)) {
1672                 String result = unit.substring(prefix.length());
1673                 // We have to do a special hack for kilogram, but only for the Rational case.
1674                 // The Integer case is used for name construction, so that is ok.
1675                 final boolean isRational = deprefix != null && deprefix.value instanceof Rational;
1676                 boolean isGramHack = isRational && result.equals("gram");
1677                 if (isGramHack) {
1678                     result = "kilogram";
1679                 }
1680                 if (deprefix != null) {
1681                     deprefix.value = entry.getValue();
1682                     if (isGramHack) {
1683                         final Rational ratValue = (Rational) deprefix.value;
1684                         deprefix.value = (V) ratValue.divide(RATIONAL1000);
1685                     }
1686                 }
1687                 return result;
1688             }
1689         }
1690         return unit;
1691     }
1692 
stripPrefix(String unit, Output<Rational> deprefix)1693     public static String stripPrefix(String unit, Output<Rational> deprefix) {
1694         if (deprefix != null) {
1695             deprefix.value = Rational.ONE;
1696         }
1697         return stripPrefixCommon(unit, deprefix, PREFIXES);
1698     }
1699 
stripPrefixPower(String unit, Output<Integer> deprefix)1700     public static String stripPrefixPower(String unit, Output<Integer> deprefix) {
1701         if (deprefix != null) {
1702             deprefix.value = 1;
1703         }
1704         return stripPrefixCommon(unit, deprefix, PREFIX_POWERS);
1705     }
1706 
getBaseUnitToQuantity()1707     public BiMap<String, String> getBaseUnitToQuantity() {
1708         return (BiMap<String, String>) baseUnitToQuantity;
1709     }
1710 
getQuantityFromUnit(String unit, boolean showYourWork)1711     public String getQuantityFromUnit(String unit, boolean showYourWork) {
1712         Output<String> metricUnit = new Output<>();
1713         unit = fixDenormalized(unit);
1714         ConversionInfo unitInfo = parseUnitId(unit, metricUnit, showYourWork);
1715         return metricUnit.value == null ? null : getQuantityFromBaseUnit(metricUnit.value);
1716     }
1717 
getQuantityFromBaseUnit(String baseUnit)1718     public String getQuantityFromBaseUnit(String baseUnit) {
1719         if (baseUnit == null) {
1720             throw new NullPointerException("baseUnit");
1721         }
1722         String result = getQuantityFromBaseUnit2(baseUnit);
1723         if (result != null) {
1724             return result;
1725         }
1726         result = getQuantityFromBaseUnit2(reciprocalOf(baseUnit));
1727         if (result != null) {
1728             result += "-inverse";
1729         }
1730         return result;
1731     }
1732 
getQuantityFromBaseUnit2(String baseUnit)1733     private String getQuantityFromBaseUnit2(String baseUnit) {
1734         String result = baseUnitToQuantity.get(baseUnit);
1735         if (result != null) {
1736             return result;
1737         }
1738         UnitId unitId = createUnitId(baseUnit);
1739         UnitId resolved = unitId.resolve();
1740         return baseUnitToQuantity.get(resolved.toString());
1741     }
1742 
getSimpleUnits()1743     public Set<String> getSimpleUnits() {
1744         return sourceToTargetInfo.keySet();
1745     }
1746 
addAliases(Map<String, R2<List<String>, String>> tagToReplacement)1747     public void addAliases(Map<String, R2<List<String>, String>> tagToReplacement) {
1748         fixDenormalized = new TreeMap<>();
1749         for (Entry<String, R2<List<String>, String>> entry : tagToReplacement.entrySet()) {
1750             final String badCode = entry.getKey();
1751             final List<String> replacements = entry.getValue().get0();
1752             fixDenormalized.put(badCode, replacements.iterator().next());
1753         }
1754         fixDenormalized = ImmutableMap.copyOf(fixDenormalized);
1755     }
1756 
getInternalConversionData()1757     public Map<String, TargetInfo> getInternalConversionData() {
1758         return sourceToTargetInfo;
1759     }
1760 
getSourceToSystems()1761     public Multimap<String, UnitSystem> getSourceToSystems() {
1762         return sourceToSystems;
1763     }
1764 
1765     public enum UnitSystem { // TODO convert getSystems and SupplementalDataInfo to use natively
1766         si,
1767         si_acceptable,
1768         metric,
1769         metric_adjacent,
1770         ussystem,
1771         uksystem,
1772         jpsystem,
1773         astronomical,
1774         person_age,
1775         other,
1776         prefixable;
1777 
1778         public static final Set<UnitSystem> SiOrMetric =
1779                 ImmutableSet.of(
1780                         UnitSystem.metric,
1781                         UnitSystem.si,
1782                         UnitSystem.metric_adjacent,
1783                         UnitSystem.si_acceptable);
1784         public static final Set<UnitSystem> ALL = ImmutableSet.copyOf(UnitSystem.values());
1785 
fromStringCollection(Collection<String> stringUnitSystems)1786         public static Set<UnitSystem> fromStringCollection(Collection<String> stringUnitSystems) {
1787             return stringUnitSystems.stream()
1788                     .map(x -> UnitSystem.valueOf(x))
1789                     .collect(Collectors.toSet());
1790         }
1791 
1792         @Deprecated
toStringSet(Collection<UnitSystem> stringUnitSystems)1793         public static Set<String> toStringSet(Collection<UnitSystem> stringUnitSystems) {
1794             return new LinkedHashSet<>(
1795                     stringUnitSystems.stream().map(x -> x.toString()).collect(Collectors.toList()));
1796         }
1797 
1798         private static final Joiner SLASH_JOINER = Joiner.on("/");
1799 
getSystemsDisplay(Set<UnitSystem> systems)1800         public static String getSystemsDisplay(Set<UnitSystem> systems) {
1801             List<String> result = new ArrayList<>();
1802             for (UnitSystem system : systems) {
1803                 switch (system) {
1804                     case si_acceptable:
1805                     case metric:
1806                     case metric_adjacent:
1807                         return "";
1808                     case ussystem:
1809                         result.add("US");
1810                         break;
1811                     case uksystem:
1812                         result.add("UK");
1813                         break;
1814                     case jpsystem:
1815                         result.add("JP");
1816                         break;
1817                 }
1818             }
1819             return result.isEmpty() ? "" : " (" + SLASH_JOINER.join(result) + ")";
1820         }
1821     }
1822 
getSystems(String unit)1823     public Set<String> getSystems(String unit) {
1824         return UnitSystem.toStringSet(getSystemsEnum(unit));
1825     }
1826 
getSystemsEnum(String unit)1827     public Set<UnitSystem> getSystemsEnum(String unit) {
1828         Set<UnitSystem> result = null;
1829         UnitId id = createUnitId(unit);
1830 
1831         // we walk through all the units in the numerator and denominator, and keep the
1832         // *intersection* of the units.
1833         // So {ussystem} and {ussystem, uksystem} => ussystem
1834         // Special case: {metric_adjacent} intersect {metric} => {metric_adjacent}.
1835         // We do that by adding metric_adjacent to any set with metric,
1836         // then removing metric_adjacent if there is a metric.
1837         // Same for si_acceptable.
1838         main:
1839         for (Map<String, Integer> unitsToPowers :
1840                 Arrays.asList(id.denUnitsToPowers, id.numUnitsToPowers)) {
1841             for (String subunit : unitsToPowers.keySet()) {
1842                 subunit = UnitConverter.stripPrefix(subunit, null);
1843                 Set<UnitSystem> systems = new TreeSet<>(sourceToSystems.get(subunit));
1844                 if (systems.contains(UnitSystem.metric)) {
1845                     systems.add(UnitSystem.metric_adjacent);
1846                 }
1847                 if (systems.contains(UnitSystem.si)) {
1848                     systems.add(UnitSystem.si_acceptable);
1849                 }
1850 
1851                 if (result == null) {
1852                     result = systems; // first setting
1853                 } else {
1854                     result.retainAll(systems);
1855                 }
1856                 if (result.isEmpty()) {
1857                     break main;
1858                 }
1859             }
1860         }
1861         if (result == null || result.isEmpty()) {
1862             return ImmutableSet.of(UnitSystem.other);
1863         }
1864         if (result.contains(UnitSystem.metric)) {
1865             result.remove(UnitSystem.metric_adjacent);
1866         }
1867         if (result.contains(UnitSystem.si)) {
1868             result.remove(UnitSystem.si_acceptable);
1869         }
1870 
1871         return ImmutableSet.copyOf(EnumSet.copyOf(result)); // the enum is to sort
1872     }
1873 
1874     //    private void addSystems(Set<String> result, String subunit) {
1875     //        Collection<String> systems = sourceToSystems.get(subunit);
1876     //        if (!systems.isEmpty()) {
1877     //            result.addAll(systems);
1878     //        }
1879     //    }
1880 
reciprocalOf(String value)1881     public String reciprocalOf(String value) {
1882         // quick version, input guaranteed to be normalized, if original is
1883         if (value.startsWith("per-")) {
1884             return value.substring(4);
1885         }
1886         int index = value.indexOf("-per-");
1887         if (index < 0) {
1888             return "per-" + value;
1889         }
1890         return value.substring(index + 5) + "-per-" + value.substring(0, index);
1891     }
1892 
parseRational(String source)1893     public Rational parseRational(String source) {
1894         return rationalParser.parse(source);
1895     }
1896 
showRational(String title, Rational rational, String unit)1897     public String showRational(String title, Rational rational, String unit) {
1898         String doubleString = showRational2(rational, " = ", " ≅ ");
1899         final String endResult = title + rational + doubleString + (unit != null ? " " + unit : "");
1900         return endResult;
1901     }
1902 
showRational(Rational rational, String approximatePrefix)1903     public String showRational(Rational rational, String approximatePrefix) {
1904         String doubleString = showRational2(rational, "", approximatePrefix);
1905         return doubleString.isEmpty() ? rational.numerator.toString() : doubleString;
1906     }
1907 
showRational2(Rational rational, String equalPrefix, String approximatePrefix)1908     public String showRational2(Rational rational, String equalPrefix, String approximatePrefix) {
1909         String doubleString = "";
1910         if (!rational.denominator.equals(BigInteger.ONE)) {
1911             String doubleValue =
1912                     String.valueOf(rational.toBigDecimal(MathContext.DECIMAL32).doubleValue());
1913             Rational reverse = parseRational(doubleValue);
1914             doubleString =
1915                     (reverse.equals(rational) ? equalPrefix : approximatePrefix) + doubleValue;
1916         }
1917         return doubleString;
1918     }
1919 
convert( final Rational sourceValue, final String sourceUnitIn, final String targetUnit, boolean showYourWork)1920     public Rational convert(
1921             final Rational sourceValue,
1922             final String sourceUnitIn,
1923             final String targetUnit,
1924             boolean showYourWork) {
1925         if (showYourWork) {
1926             System.out.println(
1927                     showRational("\nconvert:\t", sourceValue, sourceUnitIn) + " ⟹ " + targetUnit);
1928         }
1929         final String sourceUnit = fixDenormalized(sourceUnitIn);
1930         Output<String> sourceBase = new Output<>();
1931         Output<String> targetBase = new Output<>();
1932         ConversionInfo sourceConversionInfo = parseUnitId(sourceUnit, sourceBase, showYourWork);
1933         if (sourceConversionInfo == null) {
1934             if (showYourWork) System.out.println("! unknown unit: " + sourceUnit);
1935             return Rational.NaN;
1936         }
1937         Rational intermediateResult = sourceConversionInfo.convert(sourceValue);
1938         if (showYourWork)
1939             System.out.println(
1940                     showRational("intermediate:\t", intermediateResult, sourceBase.value));
1941         if (showYourWork) System.out.println("invert:\t" + targetUnit);
1942         ConversionInfo targetConversionInfo = parseUnitId(targetUnit, targetBase, showYourWork);
1943         if (targetConversionInfo == null) {
1944             if (showYourWork) System.out.println("! unknown unit: " + targetUnit);
1945             return Rational.NaN;
1946         }
1947         if (!sourceBase.value.equals(targetBase.value)) {
1948             // try resolving
1949             String sourceBaseFixed = createUnitId(sourceBase.value).resolve().toString();
1950             String targetBaseFixed = createUnitId(targetBase.value).resolve().toString();
1951             // try reciprocal
1952             if (!sourceBaseFixed.equals(targetBaseFixed)) {
1953                 String reciprocalUnit = reciprocalOf(sourceBase.value);
1954                 if (reciprocalUnit == null || !targetBase.value.equals(reciprocalUnit)) {
1955                     if (showYourWork)
1956                         System.out.println(
1957                                 "! incomparable units: " + sourceUnit + " and " + targetUnit);
1958                     return Rational.NaN;
1959                 }
1960                 intermediateResult = intermediateResult.reciprocal();
1961                 if (showYourWork)
1962                     System.out.println(
1963                             showRational(
1964                                     " ⟹ 1/intermediate:\t", intermediateResult, reciprocalUnit));
1965             }
1966         }
1967         Rational result = targetConversionInfo.convertBackwards(intermediateResult);
1968         if (showYourWork) System.out.println(showRational("target:\t", result, targetUnit));
1969         return result;
1970     }
1971 
fixDenormalized(String unit)1972     public String fixDenormalized(String unit) {
1973         String fixed = fixDenormalized.get(unit);
1974         return fixed == null ? unit : fixed;
1975     }
1976 
getConstants()1977     public Map<String, Rational> getConstants() {
1978         return rationalParser.getConstants();
1979     }
1980 
getBaseUnitFromQuantity(String unitQuantity)1981     public String getBaseUnitFromQuantity(String unitQuantity) {
1982         boolean invert = false;
1983         if (unitQuantity.endsWith("-inverse")) {
1984             invert = true;
1985             unitQuantity = unitQuantity.substring(0, unitQuantity.length() - 8);
1986         }
1987         String bu = ((BiMap<String, String>) baseUnitToQuantity).inverse().get(unitQuantity);
1988         if (bu == null) {
1989             return null;
1990         }
1991         return invert ? reciprocalOf(bu) : bu;
1992     }
1993 
getQuantities()1994     public Set<String> getQuantities() {
1995         return getBaseUnitToQuantity().inverse().keySet();
1996     }
1997 
1998     public enum UnitComplexity {
1999         simple,
2000         non_simple
2001     }
2002 
2003     private ConcurrentHashMap<String, UnitComplexity> COMPLEXITY = new ConcurrentHashMap<>();
2004     // TODO This is safe but should use regular cache
2005 
getComplexity(String longOrShortId)2006     public UnitComplexity getComplexity(String longOrShortId) {
2007         UnitComplexity result = COMPLEXITY.get(longOrShortId);
2008         if (result == null) {
2009             String shortId;
2010             String longId = getLongId(longOrShortId);
2011             if (longId == null) {
2012                 longId = longOrShortId;
2013                 shortId = SHORT_TO_LONG_ID.inverse().get(longId);
2014             } else {
2015                 shortId = longOrShortId;
2016             }
2017             UnitId uid = createUnitId(shortId);
2018             result = UnitComplexity.simple;
2019 
2020             if (uid.numUnitsToPowers.size() != 1 || !uid.denUnitsToPowers.isEmpty()) {
2021                 result = UnitComplexity.non_simple;
2022             } else {
2023                 Output<Rational> deprefix = new Output<>();
2024                 for (Entry<String, Integer> entry : uid.numUnitsToPowers.entrySet()) {
2025                     final String unitPart = entry.getKey();
2026                     UnitConverter.stripPrefix(unitPart, deprefix);
2027                     if (!deprefix.value.equals(Rational.ONE)
2028                             || !entry.getValue().equals(INTEGER_ONE)) {
2029                         result = UnitComplexity.non_simple;
2030                         break;
2031                     }
2032                 }
2033                 if (result == UnitComplexity.simple) {
2034                     for (Entry<String, Integer> entry : uid.denUnitsToPowers.entrySet()) {
2035                         final String unitPart = entry.getKey();
2036                         UnitConverter.stripPrefix(unitPart, deprefix);
2037                         if (!deprefix.value.equals(Rational.ONE)) {
2038                             result = UnitComplexity.non_simple;
2039                             break;
2040                         }
2041                     }
2042                 }
2043             }
2044             COMPLEXITY.put(shortId, result);
2045             COMPLEXITY.put(longId, result);
2046         }
2047         return result;
2048     }
2049 
isSimple(String x)2050     public boolean isSimple(String x) {
2051         return getComplexity(x) == UnitComplexity.simple;
2052     }
2053 
getLongId(String shortUnitId)2054     public String getLongId(String shortUnitId) {
2055         return CldrUtility.ifNull(SHORT_TO_LONG_ID.get(shortUnitId), shortUnitId);
2056     }
2057 
getLongIds(Iterable<String> shortUnitIds)2058     public Set<String> getLongIds(Iterable<String> shortUnitIds) {
2059         LinkedHashSet<String> result = new LinkedHashSet<>();
2060         for (String longUnitId : shortUnitIds) {
2061             String shortId = SHORT_TO_LONG_ID.get(longUnitId);
2062             if (shortId != null) {
2063                 result.add(shortId);
2064             }
2065         }
2066         return ImmutableSet.copyOf(result);
2067     }
2068 
getShortId(String longUnitId)2069     public String getShortId(String longUnitId) {
2070         if (longUnitId == null) {
2071             return null;
2072         }
2073         String result = SHORT_TO_LONG_ID.inverse().get(longUnitId);
2074         if (result != null) {
2075             return result;
2076         }
2077         int dashPos = longUnitId.indexOf('-');
2078         if (dashPos < 0) {
2079             return longUnitId;
2080         }
2081         String type = longUnitId.substring(0, dashPos);
2082         return LONG_PREFIXES.contains(type) ? longUnitId.substring(dashPos + 1) : longUnitId;
2083     }
2084 
getShortIds(Iterable<String> longUnitIds)2085     public Set<String> getShortIds(Iterable<String> longUnitIds) {
2086         LinkedHashSet<String> result = new LinkedHashSet<>();
2087         for (String longUnitId : longUnitIds) {
2088             String shortId = SHORT_TO_LONG_ID.inverse().get(longUnitId);
2089             if (shortId != null) {
2090                 result.add(shortId);
2091             }
2092         }
2093         return ImmutableSet.copyOf(result);
2094     }
2095 
getContinuations()2096     public Multimap<String, Continuation> getContinuations() {
2097         return continuations;
2098     }
2099 
getBaseUnitToStatus()2100     public Map<String, String> getBaseUnitToStatus() {
2101         return baseUnitToStatus;
2102     }
2103 
2104     static final Rational LIMIT_UPPER_RELATED = Rational.of(10000);
2105     static final Rational LIMIT_LOWER_RELATED = LIMIT_UPPER_RELATED.reciprocal();
2106 
getRelatedExamples( String inputUnit, Set<UnitSystem> allowedSystems)2107     public Map<Rational, String> getRelatedExamples(
2108             String inputUnit, Set<UnitSystem> allowedSystems) {
2109         Set<String> others = new LinkedHashSet<>(canConvertBetween(inputUnit));
2110         if (others.size() <= 1) {
2111             return Map.of();
2112         }
2113         // add common units
2114         if (others.contains("meter")) {
2115             others.add("kilometer");
2116             others.add("millimeter");
2117         } else if (others.contains("liter")) {
2118             others.add("milliliter");
2119         }
2120         // remove unusual units
2121         others.removeAll(
2122                 Set.of(
2123                         "point",
2124                         "fathom",
2125                         "carat",
2126                         "grain",
2127                         "slug",
2128                         "drop",
2129                         "pinch",
2130                         "cup-metric",
2131                         "dram",
2132                         "jigger",
2133                         "pint-metric",
2134                         "bushel, barrel",
2135                         "dunam",
2136                         "rod",
2137                         "chain",
2138                         "furlong",
2139                         "fortnight",
2140                         "rankine",
2141                         "kelvin",
2142                         "calorie-it",
2143                         "british-thermal-unit-it",
2144                         "foodcalorie",
2145                         "nautical-mile",
2146                         "mile-scandinavian",
2147                         "knot",
2148                         "beaufort"));
2149 
2150         Map<Rational, String> result = new TreeMap<>(Comparator.reverseOrder());
2151 
2152         // get metric
2153         Output<String> sourceBase = new Output<>();
2154         ConversionInfo sourceConversionInfo = parseUnitId(inputUnit, sourceBase, false);
2155         String baseUnit = sourceBase.value;
2156         Rational baseUnitToInput = sourceConversionInfo.factor;
2157 
2158         putIfInRange(result, baseUnit, baseUnitToInput);
2159 
2160         // get similar IDs
2161         // TBD
2162 
2163         // get nearby in same system, and in metric
2164 
2165         for (UnitSystem system : allowedSystems) {
2166             if (system.equals(UnitSystem.si)) {
2167                 continue;
2168             }
2169             String closestLess = null;
2170             Rational closestLessValue = Rational.NEGATIVE_INFINITY;
2171             String closestGreater = null;
2172             Rational closestGreaterValue = Rational.INFINITY;
2173 
2174             // check all the units in this system, to find the nearest above,and the nearest below
2175 
2176             for (String other : others) {
2177                 if (other.equals(inputUnit)
2178                         || other.endsWith("-person")
2179                         || other.startsWith("100-")) { // skips
2180                     continue;
2181                 }
2182                 Set<UnitSystem> otherSystems = getSystemsEnum(other);
2183                 if (!otherSystems.contains(system)) {
2184                     continue;
2185                 }
2186 
2187                 sourceConversionInfo = parseUnitId(other, sourceBase, false);
2188                 Rational otherValue =
2189                         baseUnitToInput.multiply(sourceConversionInfo.factor.reciprocal());
2190 
2191                 if (otherValue.compareTo(Rational.ONE) < 0) {
2192                     if (otherValue.compareTo(closestLessValue) > 0) {
2193                         closestLess = other;
2194                         closestLessValue = otherValue;
2195                     }
2196                 } else {
2197                     if (otherValue.compareTo(closestGreaterValue) < 0) {
2198                         closestGreater = other;
2199                         closestGreaterValue = otherValue;
2200                     }
2201                 }
2202             }
2203             putIfInRange(result, closestLess, closestLessValue);
2204             putIfInRange(result, closestGreater, closestGreaterValue);
2205         }
2206 
2207         result.remove(Rational.ONE, inputUnit); // simplest to do here
2208         return result;
2209     }
2210 
putIfInRange(Map<Rational, String> result, String baseUnit, Rational otherValue)2211     public void putIfInRange(Map<Rational, String> result, String baseUnit, Rational otherValue) {
2212         if (baseUnit != null
2213                 && otherValue.compareTo(LIMIT_LOWER_RELATED) >= 0
2214                 && otherValue.compareTo(LIMIT_UPPER_RELATED) <= 0) {
2215             if (baseUnitToQuantity.get(baseUnit) != null) {
2216                 baseUnit = getStandardUnit(baseUnit);
2217             }
2218             result.put(otherValue, baseUnit);
2219         }
2220     }
2221 
2222     static final Set<UnitSystem> NO_UK =
2223             Set.copyOf(Sets.difference(UnitSystem.ALL, Set.of(UnitSystem.uksystem)));
2224     static final Set<UnitSystem> NO_JP =
2225             Set.copyOf(Sets.difference(UnitSystem.ALL, Set.of(UnitSystem.jpsystem)));
2226     static final Set<UnitSystem> NO_JP_UK =
2227             Set.copyOf(
2228                     Sets.difference(
2229                             UnitSystem.ALL, Set.of(UnitSystem.jpsystem, UnitSystem.uksystem)));
2230     /**
2231      * Customize the systems according to the locale
2232      *
2233      * @return
2234      */
getExampleUnitSystems(String locale)2235     public static Set<UnitSystem> getExampleUnitSystems(String locale) {
2236         String language = CLDRLocale.getInstance(locale).getLanguage();
2237         switch (language) {
2238             case "ja":
2239                 return NO_UK;
2240             case "en":
2241                 return NO_JP;
2242             default:
2243                 return NO_JP_UK;
2244         }
2245     }
2246 
2247     /**
2248      * Resolve the unit if possible, eg gram-square-second-per-second ==> gram-second <br>
2249      * TODO handle complex units that don't match a simple quantity, eg
2250      * kilogram-ampere-per-meter-square-second => pascal-ampere
2251      */
resolve(String unit)2252     public String resolve(String unit) {
2253         UnitId unitId = createUnitId(unit);
2254         if (unitId == null) {
2255             return unit;
2256         }
2257         String resolved = unitId.resolve().toString();
2258         return getStandardUnit(resolved.isBlank() ? unit : resolved);
2259     }
2260 
format( final String languageTag, Rational outputAmount, final String unit, UnlocalizedNumberFormatter nf3)2261     public String format(
2262             final String languageTag,
2263             Rational outputAmount,
2264             final String unit,
2265             UnlocalizedNumberFormatter nf3) {
2266         final CLDRConfig config = CLDRConfig.getInstance();
2267         Factory factory = config.getCldrFactory();
2268         int pos = languageTag.indexOf("-u");
2269         String localeBase =
2270                 (pos < 0 ? languageTag : languageTag.substring(0, pos)).replace('-', '_');
2271         CLDRFile localeFile = factory.make(localeBase, true);
2272         PluralRules pluralRules =
2273                 config.getSupplementalDataInfo()
2274                         .getPluralRules(
2275                                 localeBase, com.ibm.icu.text.PluralRules.PluralType.CARDINAL);
2276         String pluralCategory = pluralRules.select(outputAmount.doubleValue());
2277         String path =
2278                 UnitPathType.unit.getTranslationPath(
2279                         localeFile, "long", unit, pluralCategory, "nominative", "neuter");
2280         String pattern = localeFile.getStringValue(path);
2281         final ULocale uLocale = ULocale.forLanguageTag(languageTag);
2282         String cldrFormattedNumber =
2283                 nf3.locale(uLocale).format(outputAmount.doubleValue()).toString();
2284         return com.ibm.icu.text.MessageFormat.format(pattern, cldrFormattedNumber);
2285     }
2286 }
2287