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