ComplexUnitsConverter.java
// © 2020 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
package com.ibm.icu.impl.units;
import com.ibm.icu.impl.number.DecimalQuantity;
import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD;
import com.ibm.icu.number.Precision;
import com.ibm.icu.util.Measure;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Converts from single or compound unit to single, compound or mixed units. For example, from
* {@code meter} to {@code foot+inch}.
*
* <p>DESIGN: This class uses <code>UnitsConverter</code> in order to perform the single converter
* (i.e. converters from a single unit to another single unit). Therefore, <code>
* ComplexUnitsConverter</code> class contains multiple instances of the <code>UnitsConverter</code>
* to perform the conversion.
*/
public class ComplexUnitsConverter {
public static final BigDecimal EPSILON = BigDecimal.valueOf(Math.ulp(1.0));
public static final BigDecimal EPSILON_MULTIPLIER = BigDecimal.valueOf(1).add(EPSILON);
// TODO(ICU-21937): Make it private after submitting the public units conversion API.
public ArrayList<UnitsConverter> unitsConverters_;
/**
* Individual units of mixed units, sorted big to small, with indices indicating the requested
* output mixed unit order.
*/
// TODO(ICU-21937): Make it private after submitting the public units conversion API.
public List<MeasureUnitImpl.MeasureUnitImplWithIndex> units_;
private MeasureUnitImpl inputUnit_;
/**
* Constructs <code>ComplexUnitsConverter</code> for an <code>inputUnit</code> that could be
* Single, Compound or Mixed. In case of: 1- Single and Compound units, the conversion will not
* perform anything, the input will be equal to the output. 2- Mixed Unit the conversion will
* consider the input in the biggest unit. and will convert it to be spread throw the input
* units. For example: if input unit is "inch-and-foot", and the input is 2.5. The converter
* will consider the input value in "foot", because foot is the biggest unit. Then, it will
* convert 2.5 feet to "inch-and-foot".
*
* @param targetUnit represents the input unit. could be any type. (single, compound or mixed).
*/
public ComplexUnitsConverter(MeasureUnitImpl targetUnit, ConversionRates conversionRates) {
this.units_ = targetUnit.extractIndividualUnitsWithIndices();
assert (!this.units_.isEmpty());
// Assign the biggest unit to inputUnit_.
this.inputUnit_ = this.units_.get(0).unitImpl;
MeasureUnitImpl.MeasureUnitImplComparator comparator =
new MeasureUnitImpl.MeasureUnitImplComparator(conversionRates);
for (MeasureUnitImpl.MeasureUnitImplWithIndex unitWithIndex : this.units_) {
if (comparator.compare(unitWithIndex.unitImpl, this.inputUnit_) > 0) {
this.inputUnit_ = unitWithIndex.unitImpl;
}
}
this.init(conversionRates);
}
/**
* Constructs <code>ComplexUnitsConverter</code> NOTE: - inputUnit and outputUnits must be under
* the same category - e.g. meter to feet and inches --> all of them are length units.
*
* @param inputUnitIdentifier represents the source unit identifier. (should be single or
* compound unit).
* @param outputUnitsIdentifier represents the output unit identifier. could be any type.
* (single, compound or mixed).
*/
public ComplexUnitsConverter(String inputUnitIdentifier, String outputUnitsIdentifier) {
this(
MeasureUnitImpl.forIdentifier(inputUnitIdentifier),
MeasureUnitImpl.forIdentifier(outputUnitsIdentifier),
new ConversionRates());
}
/**
* Constructs <code>ComplexUnitsConverter</code> NOTE: - inputUnit and outputUnits must be under
* the same category - e.g. meter to feet and inches --> all of them are length units.
*
* @param inputUnit represents the source unit. (should be single or compound unit).
* @param outputUnits represents the output unit. could be any type. (single, compound or
* mixed).
* @param conversionRates a ConversionRates instance containing the unit conversion rates.
*/
public ComplexUnitsConverter(
MeasureUnitImpl inputUnit,
MeasureUnitImpl outputUnits,
ConversionRates conversionRates) {
this.inputUnit_ = inputUnit;
this.units_ = outputUnits.extractIndividualUnitsWithIndices();
assert (!this.units_.isEmpty());
this.init(conversionRates);
}
/**
* Sorts units_, which must be populated before calling this, and populates unitsConverters_.
*/
private void init(ConversionRates conversionRates) {
// Sort the units in a descending order.
Collections.sort(
this.units_,
Collections.reverseOrder(
new MeasureUnitImpl.MeasureUnitImplWithIndexComparator(conversionRates)));
// If the `outputUnits` is `UMEASURE_UNIT_MIXED` such as `foot+inch`. Thus means there is
// more than one unit
// and In this case we need more converters to convert from the `inputUnit` to the first
// unit in the
// `outputUnits`. Then, a converter from the first unit in the `outputUnits` to the second
// unit and so on.
// For Example:
// - inputUnit is `meter`
// - outputUnits is `foot+inch`
// - Therefore, we need to have two converters:
// 1. a converter from `meter` to `foot`
// 2. a converter from `foot` to `inch`
// - Therefore, if the input is `2 meter`:
// 1. convert `meter` to `foot` --> 2 meter to 6.56168 feet
// 2. convert the residual of 6.56168 feet (0.56168) to inches, which will be
// (6.74016
// inches)
// 3. then, the final result will be (6 feet and 6.74016 inches)
unitsConverters_ = new ArrayList<>();
for (int i = 0, n = units_.size(); i < n; i++) {
if (i == 0) { // first element
unitsConverters_.add(
new UnitsConverter(
this.inputUnit_, units_.get(i).unitImpl, conversionRates));
} else {
unitsConverters_.add(
new UnitsConverter(
units_.get(i - 1).unitImpl,
units_.get(i).unitImpl,
conversionRates));
}
}
}
/**
* Returns true if the specified {@code quantity} of the {@code inputUnit}, expressed in terms
* of the biggest unit in the MeasureUnit {@code outputUnit}, is greater than or equal to {@code
* limit}.
*
* <p>For example, if the input unit is {@code meter} and the target unit is {@code foot+inch}.
* Therefore, this function will convert the {@code quantity} from {@code meter} to {@code
* foot}, then, it will compare the value in {@code foot} with the {@code limit}.
*/
public boolean greaterThanOrEqual(BigDecimal quantity, BigDecimal limit) {
assert !units_.isEmpty();
// NOTE: First converter converts to the biggest quantity.
return unitsConverters_
.get(0)
.convert(quantity)
.multiply(EPSILON_MULTIPLIER)
.compareTo(limit)
>= 0;
}
public static class ComplexConverterResult {
public final int indexOfQuantity;
public final List<Measure> measures;
ComplexConverterResult(int indexOfQuantity, List<Measure> measures) {
this.indexOfQuantity = indexOfQuantity;
this.measures = measures;
}
}
/**
* Returns outputMeasures which is an array with the corresponding values. - E.g. converting
* meters to feet and inches. 1 meter --> 3 feet, 3.3701 inches NOTE: the smallest element is
* the only element that could have fractional values. And all other elements are floored to the
* nearest integer
*/
public ComplexConverterResult convert(BigDecimal quantity, Precision rounder) {
BigInteger sign = BigInteger.ONE;
if (quantity.compareTo(BigDecimal.ZERO) < 0 && unitsConverters_.size() > 1) {
quantity = quantity.abs();
sign = sign.negate();
}
// For N converters:
// - the first converter converts from the input unit to the largest
// unit,
// - N-1 converters convert to bigger units for which we want integers,
// - the Nth converter (index N-1) converts to the smallest unit, which
// isn't (necessarily) an integer.
List<BigInteger> intValues = new ArrayList<>(unitsConverters_.size() - 1);
for (int i = 0, n = unitsConverters_.size(); i < n; ++i) {
quantity = (unitsConverters_.get(i)).convert(quantity);
if (i < n - 1) {
// The double type has 15 decimal digits of precision. For choosing
// whether to use the current unit or the next smaller unit, we
// therefore nudge up the number with which the thresholding
// decision is made. However after the thresholding, we use the
// original values to ensure unbiased accuracy (to the extent of
// double's capabilities).
BigInteger flooredQuantity =
quantity.multiply(EPSILON_MULTIPLIER)
.setScale(0, RoundingMode.FLOOR)
.toBigInteger();
intValues.add(flooredQuantity);
// Keep the residual of the quantity.
// For example: `3.6 feet`, keep only `0.6 feet`
BigDecimal remainder =
quantity.subtract(BigDecimal.valueOf(flooredQuantity.longValue()));
if (remainder.compareTo(BigDecimal.ZERO) == -1) {
quantity = BigDecimal.ZERO;
} else {
quantity = remainder;
}
}
}
quantity = applyRounder(intValues, quantity, rounder);
// Initialize empty measures.
List<Measure> measures = new ArrayList<>(unitsConverters_.size());
for (int i = 0; i < unitsConverters_.size(); i++) {
measures.add(null);
}
// Package values into Measure instances in measures:
int indexOfQuantity = -1;
for (int i = 0, n = unitsConverters_.size(); i < n; ++i) {
if (i < n - 1) {
Measure measure =
new Measure(
intValues.get(i).multiply(sign), units_.get(i).unitImpl.build());
measures.set(units_.get(i).index, measure);
} else {
indexOfQuantity = units_.get(i).index;
Measure measure =
new Measure(
quantity.multiply(BigDecimal.valueOf(sign.longValue())),
units_.get(i).unitImpl.build());
measures.set(indexOfQuantity, measure);
}
}
return new ComplexConverterResult(indexOfQuantity, measures);
}
/**
* Applies the rounder to the quantity (last element) and bubble up any carried value to all the
* intValues.
*
* @return the rounded quantity
*/
private BigDecimal applyRounder(
List<BigInteger> intValues, BigDecimal quantity, Precision rounder) {
if (rounder == null) {
return quantity;
}
DecimalQuantity quantityBCD = new DecimalQuantity_DualStorageBCD(quantity);
rounder.apply(quantityBCD);
quantity = quantityBCD.toBigDecimal();
if (intValues.size() == 0) {
// There is only one element, Therefore, nothing to be done
return quantity;
}
// Check if there's a carry, and bubble it back up the resulting intValues.
int lastIndex = unitsConverters_.size() - 1;
BigDecimal carry =
unitsConverters_
.get(lastIndex)
.convertInverse(quantity)
.multiply(EPSILON_MULTIPLIER)
.setScale(0, RoundingMode.FLOOR);
if (carry.compareTo(BigDecimal.ZERO) <= 0) { // carry is not greater than zero
return quantity;
}
quantity = quantity.subtract(unitsConverters_.get(lastIndex).convert(carry));
intValues.set(lastIndex - 1, intValues.get(lastIndex - 1).add(carry.toBigInteger()));
// We don't use the first converter: that one is for the input unit
for (int j = lastIndex - 1; j > 0; j--) {
carry =
unitsConverters_
.get(j)
.convertInverse(BigDecimal.valueOf(intValues.get(j).longValue()))
.multiply(EPSILON_MULTIPLIER)
.setScale(0, RoundingMode.FLOOR);
if (carry.compareTo(BigDecimal.ZERO) <= 0) { // carry is not greater than zero
break;
}
intValues.set(
j,
intValues
.get(j)
.subtract(unitsConverters_.get(j).convert(carry).toBigInteger()));
intValues.set(j - 1, intValues.get(j - 1).add(carry.toBigInteger()));
}
return quantity;
}
@Override
public String toString() {
return "ComplexUnitsConverter [unitsConverters_="
+ unitsConverters_
+ ", units_="
+ units_
+ "]";
}
}