• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // © 2020 and later: Unicode, Inc. and others.
2 // License & terms of use: http://www.unicode.org/copyright.html
3 package com.ibm.icu.impl.units;
4 
5 import java.math.BigDecimal;
6 import java.util.ArrayList;
7 import java.util.List;
8 
9 import com.ibm.icu.impl.IllegalIcuArgumentException;
10 import com.ibm.icu.impl.number.MicroProps;
11 import com.ibm.icu.number.Precision;
12 import com.ibm.icu.util.MeasureUnit;
13 import com.ibm.icu.util.ULocale;
14 
15 /**
16  * `UnitsRouter` responsible for converting from a single unit (such as `meter` or `meter-per-second`) to
17  * one of the complex units based on the limits.
18  * For example:
19  * if the input is `meter` and the output as following
20  * {`foot+inch`, limit: 3.0}
21  * {`inch`     , limit: no value (-inf)}
22  * Thus means if the input in `meter` is greater than or equal to `3.0 feet`, the output will be in
23  * `foot+inch`, otherwise, the output will be in `inch`.
24  * <p>
25  * NOTE:
26  * the output units and their limits MUST BE in order, for example, if the output units, from the
27  * previous example, are the following:
28  * {`inch`     , limit: no value (-inf)}
29  * {`foot+inch`, limit: 3.0}
30  * IN THIS CASE THE OUTPUT WILL BE ALWAYS IN `inch`.
31  * <p>
32  * NOTE:
33  * the output units  and their limits will be extracted from the units preferences database by knowing
34  * the followings:
35  * - input unit
36  * - locale
37  * - usage
38  * <p>
39  * DESIGN:
40  * `UnitRouter` uses internally `ComplexUnitConverter` in order to convert the input units to the
41  * desired complex units and to check the limit too.
42  */
43 public class UnitsRouter {
44     // List of possible output units. TODO: converterPreferences_ now also has
45     // this data available. Maybe drop outputUnits_ and have getOutputUnits
46     // construct a the list from data in converterPreferences_ instead?
47     private ArrayList<MeasureUnit> outputUnits_ = new ArrayList<>();
48     private ArrayList<ConverterPreference> converterPreferences_ = new ArrayList<>();
49 
UnitsRouter(String inputUnitIdentifier, ULocale locale, String usage)50     public UnitsRouter(String inputUnitIdentifier, ULocale locale, String usage) {
51         this(MeasureUnitImpl.forIdentifier(inputUnitIdentifier), locale, usage);
52     }
53 
UnitsRouter(MeasureUnitImpl inputUnit, ULocale locale, String usage)54     public UnitsRouter(MeasureUnitImpl inputUnit, ULocale locale, String usage) {
55         // TODO: do we want to pass in ConversionRates and UnitPreferences instead?
56         // of loading in each UnitsRouter instance? (Or make global?)
57         UnitsData data = new UnitsData();
58 
59         String category = data.getCategory(inputUnit);
60         UnitPreferences.UnitPreference[] unitPreferences = data.getPreferencesFor(category, usage, locale);
61 
62         for (int i = 0; i < unitPreferences.length; ++i) {
63             UnitPreferences.UnitPreference preference = unitPreferences[i];
64 
65             MeasureUnitImpl complexTargetUnitImpl =
66                     MeasureUnitImpl.UnitsParser.parseForIdentifier(preference.getUnit());
67 
68             String precision = preference.getSkeleton();
69 
70             // For now, we only have "precision-increment" in Units Preferences skeleton.
71             // Therefore, we check if the skeleton starts with "precision-increment" and force the program to
72             // fail otherwise.
73             // NOTE:
74             //  It is allowed to have an empty precision.
75             if (!precision.isEmpty() && !precision.startsWith("precision-increment")) {
76                 throw new AssertionError("Only `precision-increment` is allowed");
77             }
78 
79             outputUnits_.add(complexTargetUnitImpl.build());
80             converterPreferences_.add(new ConverterPreference(inputUnit, complexTargetUnitImpl,
81                     preference.getGeq(), precision,
82                     data.getConversionRates()));
83         }
84     }
85 
86     /** If micros.rounder is a BogusRounder, this function replaces it with a valid one. */
route(BigDecimal quantity, MicroProps micros)87     public RouteResult route(BigDecimal quantity, MicroProps micros) {
88         Precision rounder = micros == null ? null : micros.rounder;
89         ConverterPreference converterPreference = null;
90         for (ConverterPreference itr : converterPreferences_) {
91             converterPreference = itr;
92             if (converterPreference.converter.greaterThanOrEqual(quantity.abs(),
93                                                                  converterPreference.limit)) {
94                 break;
95             }
96         }
97         assert converterPreference != null;
98         assert converterPreference.precision != null;
99 
100         // Set up the rounder for this preference's precision
101         if (rounder != null && rounder instanceof Precision.BogusRounder) {
102             Precision.BogusRounder bogus = (Precision.BogusRounder)rounder;
103             if (converterPreference.precision.length() > 0) {
104                 rounder = bogus.into(parseSkeletonToPrecision(converterPreference.precision));
105             } else {
106                 // We use the same rounding mode as COMPACT notation: known to be a
107                 // human-friendly rounding mode: integers, but add a decimal digit
108                 // as needed to ensure we have at least 2 significant digits.
109                 rounder = bogus.into(Precision.integer().withMinDigits(2));
110             }
111         }
112 
113         if (micros != null) {
114             micros.rounder = rounder;
115         }
116         return new RouteResult(
117                 converterPreference.converter.convert(quantity, rounder),
118                 converterPreference.targetUnit
119         );
120     }
121 
parseSkeletonToPrecision(String precisionSkeleton)122     private static Precision parseSkeletonToPrecision(String precisionSkeleton) {
123         final String kSkeletonPrefix = "precision-increment/";
124         if (!precisionSkeleton.startsWith(kSkeletonPrefix)) {
125             throw new IllegalIcuArgumentException("precisionSkeleton is only precision-increment");
126         }
127 
128         // TODO(icu-units#104): the C++ code uses a more sophisticated
129         // parseIncrementOption which supports "withMinFraction" - e.g.
130         // "precision-increment/0.5". Test with a unit preference that uses
131         // this, and fix Java.
132         String incrementValue = precisionSkeleton.substring(kSkeletonPrefix.length());
133         return Precision.increment(new BigDecimal(incrementValue));
134     }
135 
136     /**
137      * Returns the list of possible output units, i.e. the full set of
138      * preferences, for the localized, usage-specific unit preferences.
139      * <p>
140      * The returned pointer should be valid for the lifetime of the
141      * UnitsRouter instance.
142      */
getOutputUnits()143     public List<MeasureUnit> getOutputUnits() {
144         return this.outputUnits_;
145     }
146 
147     /**
148      * Contains the complex unit converter and the limit which representing the smallest value that the
149      * converter should accept. For example, if the converter is converting to `foot+inch` and the limit
150      * equals 3.0, thus means the converter should not convert to a value less than `3.0 feet`.
151      * <p>
152      * NOTE:
153      * if the limit doest not has a value `i.e. (std::numeric_limits<double>::lowest())`, this mean there
154      * is no limit for the converter.
155      */
156     public static class ConverterPreference {
157         // The output unit for this ConverterPreference. This may be a MIXED unit -
158         // for example: "yard-and-foot-and-inch".
159         final MeasureUnitImpl targetUnit;
160         final ComplexUnitsConverter converter;
161         final BigDecimal limit;
162         final String precision;
163 
164         // In case there is no limit, the limit will be -inf.
ConverterPreference(MeasureUnitImpl source, MeasureUnitImpl targetUnit, String precision, ConversionRates conversionRates)165         public ConverterPreference(MeasureUnitImpl source, MeasureUnitImpl targetUnit,
166                                    String precision, ConversionRates conversionRates) {
167             this(source, targetUnit, BigDecimal.valueOf(Double.MIN_VALUE), precision,
168                     conversionRates);
169         }
170 
ConverterPreference(MeasureUnitImpl source, MeasureUnitImpl targetUnit, BigDecimal limit, String precision, ConversionRates conversionRates)171         public ConverterPreference(MeasureUnitImpl source, MeasureUnitImpl targetUnit,
172                                    BigDecimal limit, String precision, ConversionRates conversionRates) {
173             this.converter = new ComplexUnitsConverter(source, targetUnit, conversionRates);
174             this.limit = limit;
175             this.precision = precision;
176             this.targetUnit = targetUnit;
177 
178         }
179     }
180 
181     public class RouteResult {
182         public final ComplexUnitsConverter.ComplexConverterResult complexConverterResult;
183 
184         // The output unit for this RouteResult. This may be a MIXED unit - for
185         // example: "yard-and-foot-and-inch", for which `measures` will have three
186         // elements.
187         public final MeasureUnitImpl outputUnit;
188 
RouteResult(ComplexUnitsConverter.ComplexConverterResult complexConverterResult, MeasureUnitImpl outputUnit)189         RouteResult(ComplexUnitsConverter.ComplexConverterResult complexConverterResult, MeasureUnitImpl outputUnit) {
190             this.complexConverterResult = complexConverterResult;
191             this.outputUnit = outputUnit;
192         }
193     }
194 }
195