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