• 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.math.BigInteger;
7 import java.math.RoundingMode;
8 import java.util.ArrayList;
9 import java.util.Collections;
10 import java.util.List;
11 
12 import com.ibm.icu.impl.number.DecimalQuantity;
13 import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD;
14 import com.ibm.icu.number.Precision;
15 import com.ibm.icu.util.Measure;
16 
17 /**
18  * Converts from single or compound unit to single, compound or mixed units. For example, from `meter` to `foot+inch`.
19  * <p>
20  * DESIGN: This class uses <code>UnitsConverter</code> in order to perform the single converter (i.e. converters from
21  * a single unit to another single unit). Therefore, <code>ComplexUnitsConverter</code> class contains multiple
22  * instances of the <code>UnitsConverter</code> to perform the conversion.
23  */
24 public class ComplexUnitsConverter {
25     public static final BigDecimal EPSILON = BigDecimal.valueOf(Math.ulp(1.0));
26     public static final BigDecimal EPSILON_MULTIPLIER = BigDecimal.valueOf(1).add(EPSILON);
27     private ArrayList<UnitsConverter> unitsConverters_;
28     /**
29      * Individual units of mixed units, sorted big to small, with indices
30      * indicating the requested output mixed unit order.
31      */
32     private List<MeasureUnitImpl.MeasureUnitImplWithIndex> units_;
33     private MeasureUnitImpl inputUnit_;
34 
35     /**
36      * Constructs <code>ComplexUnitsConverter</code> for an <code>inputUnit</code> that could be Single, Compound or
37      * Mixed. In case of: 1- Single and Compound units, the conversion will not perform anything, the input will be
38      * equal to the output. 2- Mixed Unit the conversion will consider the input in the biggest unit. and will convert
39      * it to be spread throw the input units. For example: if input unit is "inch-and-foot", and the input is 2.5. The
40      * converter will consider the input value in "foot", because foot is the biggest unit. Then, it will convert 2.5
41      * feet to "inch-and-foot".
42      *
43      * @param targetUnit
44      *            represents the input unit. could be any type. (single, compound or mixed).
45      */
ComplexUnitsConverter(MeasureUnitImpl targetUnit, ConversionRates conversionRates)46     public ComplexUnitsConverter(MeasureUnitImpl targetUnit, ConversionRates conversionRates) {
47         this.units_ = targetUnit.extractIndividualUnitsWithIndices();
48         assert (!this.units_.isEmpty());
49 
50         // Assign the biggest unit to inputUnit_.
51         this.inputUnit_ = this.units_.get(0).unitImpl;
52         MeasureUnitImpl.MeasureUnitImplComparator comparator = new MeasureUnitImpl.MeasureUnitImplComparator(
53                 conversionRates);
54         for (MeasureUnitImpl.MeasureUnitImplWithIndex unitWithIndex : this.units_) {
55             if (comparator.compare(unitWithIndex.unitImpl, this.inputUnit_) > 0) {
56                 this.inputUnit_ = unitWithIndex.unitImpl;
57             }
58         }
59 
60         this.init(conversionRates);
61     }
62 
63     /**
64      * Constructs <code>ComplexUnitsConverter</code> NOTE: - inputUnit and outputUnits must be under the same category -
65      * e.g. meter to feet and inches --> all of them are length units.
66      *
67      * @param inputUnitIdentifier
68      *              represents the source unit identifier. (should be single or compound unit).
69      * @param outputUnitsIdentifier
70      *              represents the output unit identifier. could be any type. (single, compound or mixed).
71      */
ComplexUnitsConverter(String inputUnitIdentifier, String outputUnitsIdentifier)72     public ComplexUnitsConverter(String inputUnitIdentifier, String outputUnitsIdentifier) {
73         this(
74                 MeasureUnitImpl.forIdentifier(inputUnitIdentifier),
75                 MeasureUnitImpl.forIdentifier(outputUnitsIdentifier),
76                 new ConversionRates()
77         );
78     }
79 
80     /**
81      * Constructs <code>ComplexUnitsConverter</code> NOTE: - inputUnit and outputUnits must be under the same category -
82      * e.g. meter to feet and inches --> all of them are length units.
83      *
84      * @param inputUnit
85      *            represents the source unit. (should be single or compound unit).
86      * @param outputUnits
87      *            represents the output unit. could be any type. (single, compound or mixed).
88      * @param conversionRates
89      *            a ConversionRates instance containing the unit conversion rates.
90      */
ComplexUnitsConverter(MeasureUnitImpl inputUnit, MeasureUnitImpl outputUnits, ConversionRates conversionRates)91     public ComplexUnitsConverter(MeasureUnitImpl inputUnit, MeasureUnitImpl outputUnits,
92             ConversionRates conversionRates) {
93         this.inputUnit_ = inputUnit;
94         this.units_ = outputUnits.extractIndividualUnitsWithIndices();
95         assert (!this.units_.isEmpty());
96 
97         this.init(conversionRates);
98     }
99 
100     /**
101      * Sorts units_, which must be populated before calling this, and populates
102      * unitsConverters_.
103      */
init(ConversionRates conversionRates)104     private void init(ConversionRates conversionRates) {
105         // Sort the units in a descending order.
106         Collections.sort(this.units_,
107                 Collections.reverseOrder(new MeasureUnitImpl.MeasureUnitImplWithIndexComparator(conversionRates)));
108 
109         // If the `outputUnits` is `UMEASURE_UNIT_MIXED` such as `foot+inch`. Thus means there is more than one unit
110         //  and In this case we need more converters to convert from the `inputUnit` to the first unit in the
111         //  `outputUnits`. Then, a converter from the first unit in the `outputUnits` to the second unit and so on.
112         //      For Example:
113         //          - inputUnit is `meter`
114         //          - outputUnits is `foot+inch`
115         //              - Therefore, we need to have two converters:
116         //                      1. a converter from `meter` to `foot`
117         //                      2. a converter from `foot` to `inch`
118         //          - Therefore, if the input is `2 meter`:
119         //              1. convert `meter` to `foot` --> 2 meter to 6.56168 feet
120         //              2. convert the residual of 6.56168 feet (0.56168) to inches, which will be (6.74016
121         //              inches)
122         //              3. then, the final result will be (6 feet and 6.74016 inches)
123         unitsConverters_ = new ArrayList<>();
124         for (int i = 0, n = units_.size(); i < n; i++) {
125             if (i == 0) { // first element
126                 unitsConverters_.add(new UnitsConverter(this.inputUnit_, units_.get(i).unitImpl, conversionRates));
127             } else {
128                 unitsConverters_
129                         .add(new UnitsConverter(units_.get(i - 1).unitImpl, units_.get(i).unitImpl, conversionRates));
130             }
131         }
132     }
133 
134     /**
135      * Returns true if the specified `quantity` of the `inputUnit`, expressed in terms of the biggest unit in the
136      * MeasureUnit `outputUnit`, is greater than or equal to `limit`.
137      * <p>
138      * For example, if the input unit is `meter` and the target unit is `foot+inch`. Therefore, this function will
139      * convert the `quantity` from `meter` to `foot`, then, it will compare the value in `foot` with the `limit`.
140      */
greaterThanOrEqual(BigDecimal quantity, BigDecimal limit)141     public boolean greaterThanOrEqual(BigDecimal quantity, BigDecimal limit) {
142         assert !units_.isEmpty();
143 
144         // NOTE: First converter converts to the biggest quantity.
145         return unitsConverters_.get(0).convert(quantity).multiply(EPSILON_MULTIPLIER).compareTo(limit) >= 0;
146     }
147 
148     public static class ComplexConverterResult {
149         public final int indexOfQuantity;
150         public final List<Measure> measures;
151 
ComplexConverterResult(int indexOfQuantity, List<Measure> measures)152         ComplexConverterResult(int indexOfQuantity, List<Measure> measures) {
153             this.indexOfQuantity = indexOfQuantity;
154             this.measures = measures;
155         }
156     }
157 
158     /**
159      * Returns outputMeasures which is an array with the corresponding values.
160      * - E.g. converting meters to feet and inches.
161      * 1 meter --> 3 feet, 3.3701 inches
162      * NOTE:
163      * the smallest element is the only element that could have fractional values. And all
164      * other elements are floored to the nearest integer
165      */
convert(BigDecimal quantity, Precision rounder)166     public ComplexConverterResult convert(BigDecimal quantity, Precision rounder) {
167         BigInteger sign = BigInteger.ONE;
168         if (quantity.compareTo(BigDecimal.ZERO) < 0) {
169             quantity = quantity.abs();
170             sign = sign.negate();
171         }
172 
173         // For N converters:
174         // - the first converter converts from the input unit to the largest
175         //   unit,
176         // - N-1 converters convert to bigger units for which we want integers,
177         // - the Nth converter (index N-1) converts to the smallest unit, which
178         //   isn't (necessarily) an integer.
179         List<BigInteger> intValues = new ArrayList<>(unitsConverters_.size() - 1);
180         for (int i = 0, n = unitsConverters_.size(); i < n; ++i) {
181             quantity = (unitsConverters_.get(i)).convert(quantity);
182 
183             if (i < n - 1) {
184                 // The double type has 15 decimal digits of precision. For choosing
185                 // whether to use the current unit or the next smaller unit, we
186                 // therefore nudge up the number with which the thresholding
187                 // decision is made. However after the thresholding, we use the
188                 // original values to ensure unbiased accuracy (to the extent of
189                 // double's capabilities).
190                 BigInteger flooredQuantity = quantity.multiply(EPSILON_MULTIPLIER).setScale(0, RoundingMode.FLOOR).toBigInteger();
191                 intValues.add(flooredQuantity);
192 
193                 // Keep the residual of the quantity.
194                 // For example: `3.6 feet`, keep only `0.6 feet`
195                 BigDecimal remainder = quantity.subtract(BigDecimal.valueOf(flooredQuantity.longValue()));
196                 if (remainder.compareTo(BigDecimal.ZERO) == -1) {
197                     quantity = BigDecimal.ZERO;
198                 } else {
199                     quantity = remainder;
200                 }
201             }
202         }
203 
204         quantity = applyRounder(intValues, quantity, rounder);
205 
206         // Initialize empty measures.
207         List<Measure> measures = new ArrayList<>(unitsConverters_.size());
208         for (int i = 0; i < unitsConverters_.size(); i++) {
209             measures.add(null);
210         }
211 
212         // Package values into Measure instances in measures:
213         int indexOfQuantity = -1;
214         for (int i = 0, n = unitsConverters_.size(); i < n; ++i) {
215             if (i < n - 1) {
216                 Measure measure = new Measure(intValues.get(i).multiply(sign), units_.get(i).unitImpl.build());
217                 measures.set(units_.get(i).index, measure);
218             } else {
219                 indexOfQuantity = units_.get(i).index;
220                 Measure measure =
221                         new Measure(quantity.multiply(BigDecimal.valueOf(sign.longValue())),
222                                 units_.get(i).unitImpl.build());
223                 measures.set(indexOfQuantity, measure);
224             }
225         }
226 
227         return new ComplexConverterResult(indexOfQuantity , measures);
228     }
229 
230     /**
231      * Applies the rounder to the quantity (last element) and bubble up any carried value to all the intValues.
232      *
233      * @return the rounded quantity
234      */
applyRounder(List<BigInteger> intValues, BigDecimal quantity, Precision rounder)235     private BigDecimal applyRounder(List<BigInteger> intValues, BigDecimal quantity, Precision rounder) {
236         if (rounder == null) {
237             return quantity;
238         }
239 
240         DecimalQuantity quantityBCD = new DecimalQuantity_DualStorageBCD(quantity);
241         rounder.apply(quantityBCD);
242         quantity = quantityBCD.toBigDecimal();
243 
244         if (intValues.size() == 0) {
245             // There is only one element, Therefore, nothing to be done
246             return quantity;
247         }
248 
249         // Check if there's a carry, and bubble it back up the resulting intValues.
250         int lastIndex = unitsConverters_.size() - 1;
251         BigDecimal carry = unitsConverters_.get(lastIndex).convertInverse(quantity).multiply(EPSILON_MULTIPLIER)
252                 .setScale(0, RoundingMode.FLOOR);
253         if (carry.compareTo(BigDecimal.ZERO) <= 0) { // carry is not greater than zero
254             return quantity;
255         }
256         quantity = quantity.subtract(unitsConverters_.get(lastIndex).convert(carry));
257         intValues.set(lastIndex - 1, intValues.get(lastIndex - 1).add(carry.toBigInteger()));
258 
259         // We don't use the first converter: that one is for the input unit
260         for (int j = lastIndex - 1; j > 0; j--) {
261             carry = unitsConverters_.get(j)
262                     .convertInverse(BigDecimal.valueOf(intValues.get(j).longValue()))
263                     .multiply(EPSILON_MULTIPLIER)
264                     .setScale(0, RoundingMode.FLOOR);
265             if (carry.compareTo(BigDecimal.ZERO) <= 0) { // carry is not greater than zero
266                 break;
267             }
268             intValues.set(j, intValues.get(j).subtract(unitsConverters_.get(j).convert(carry).toBigInteger()));
269             intValues.set(j - 1, intValues.get(j - 1).add(carry.toBigInteger()));
270         }
271 
272         return quantity;
273     }
274 
275     @Override
toString()276     public String toString() {
277         return "ComplexUnitsConverter [unitsConverters_=" + unitsConverters_ + ", units_=" + units_ + "]";
278     }
279 }
280