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