ICU-20568 Implementation of UnitConverter, ComplexUnitConverter and UnitsRouter

See #1279
This commit is contained in:
younies 2020-09-15 15:22:41 +00:00 committed by Younies Mahmoud
parent 6198151510
commit 24a06cc33b
10 changed files with 1521 additions and 6 deletions

View file

@ -0,0 +1,128 @@
// © 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.util.Measure;
import java.math.BigDecimal;
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 `meter` to `foot+inch`.
* <p>
* DESIGN:
* This class uses `UnitConverter` in order to perform the single converter (i.e. converters from a
* single unit to another single unit). Therefore, `ComplexUnitsConverter` class contains multiple
* instances of the `UnitConverter` 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);
private ArrayList<UnitConverter> unitConverters_;
private ArrayList<MeasureUnitImpl> units_;
/**
* Constructor of `ComplexUnitsConverter`.
* 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).
*/
public ComplexUnitsConverter(MeasureUnitImpl inputUnit, MeasureUnitImpl outputUnits,
ConversionRates conversionRates) {
units_ = outputUnits.extractIndividualUnits();
assert (!units_.isEmpty());
// Sort the units in a descending order.
Collections.sort(
this.units_,
Collections.reverseOrder(new MeasureUnitImpl.MeasureUnitImplComparator(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)
unitConverters_ = new ArrayList<>();
for (int i = 0, n = units_.size(); i < n; i++) {
if (i == 0) { // first element
unitConverters_.add(new UnitConverter(inputUnit, units_.get(i), conversionRates));
} else {
unitConverters_.add(new UnitConverter(units_.get(i - 1), units_.get(i), conversionRates));
}
}
}
/**
* Returns true if the specified `quantity` of the `inputUnit`, expressed in terms of the biggest
* unit in the MeasureUnit `outputUnit`, is greater than or equal to `limit`.
* <p>
* For example, if the input unit is `meter` and the target unit is `foot+inch`. Therefore, this
* function will convert the `quantity` from `meter` to `foot`, then, it will compare the value in
* `foot` with the `limit`.
*/
public boolean greaterThanOrEqual(BigDecimal quantity, BigDecimal limit) {
assert !units_.isEmpty();
// NOTE: First converter converts to the biggest quantity.
return unitConverters_.get(0).convert(quantity).multiply(EPSILON_MULTIPLIER).compareTo(limit) >= 0;
}
/**
* 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 List<Measure> convert(BigDecimal quantity) {
List<Measure> result = new ArrayList<>();
for (int i = 0, n = unitConverters_.size(); i < n; ++i) {
quantity = (unitConverters_.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).
BigDecimal newQuantity = quantity.multiply(EPSILON_MULTIPLIER).setScale(0, RoundingMode.FLOOR);
result.add(new Measure(newQuantity, units_.get(i).build()));
// Keep the residual of the quantity.
// For example: `3.6 feet`, keep only `0.6 feet`
quantity = quantity.subtract(newQuantity);
if (quantity.compareTo(BigDecimal.ZERO) == -1) {
quantity = BigDecimal.ZERO;
}
} else { // LAST ELEMENT
result.add(new Measure(quantity, units_.get(i).build()));
}
}
return result;
}
}

View file

@ -0,0 +1,234 @@
// © 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.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.UResourceBundle;
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.ArrayList;
import java.util.HashMap;
public class ConversionRates {
/**
* Map from any simple unit (i.e. "meter", "foot", "inch") to its basic/root conversion rate info.
*/
private HashMap<String, ConversionRateInfo> mapToConversionRate;
public ConversionRates() {
// Read the conversion rates from the data (units.txt).
ICUResourceBundle resource;
resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "units");
ConversionRatesSink sink = new ConversionRatesSink();
resource.getAllItemsWithFallback(UnitsData.Constants.CONVERSION_UNIT_TABLE_NAME, sink);
this.mapToConversionRate = sink.getMapToConversionRate();
}
/**
* Extracts the factor from a `SingleUnitImpl` to its Basic Unit.
*
* @param singleUnit
* @return
*/
private UnitConverter.Factor getFactorToBase(SingleUnitImpl singleUnit) {
int power = singleUnit.getDimensionality();
MeasureUnit.SIPrefix siPrefix = singleUnit.getSiPrefix();
UnitConverter.Factor result = UnitConverter.Factor.processFactor(mapToConversionRate.get(singleUnit.getSimpleUnit()).getConversionRate());
return result.applySiPrefix(siPrefix).power(power); // NOTE: you must apply the SI prefixes before the power.
}
public UnitConverter.Factor getFactorToBase(MeasureUnitImpl measureUnit) {
UnitConverter.Factor result = new UnitConverter.Factor();
for (SingleUnitImpl singleUnit :
measureUnit.getSingleUnits()) {
result = result.multiply(getFactorToBase(singleUnit));
}
return result;
}
protected BigDecimal getOffset(MeasureUnitImpl source, MeasureUnitImpl target, UnitConverter.Factor
sourceToBase, UnitConverter.Factor targetToBase, UnitConverter.Convertibility convertibility) {
if (convertibility != UnitConverter.Convertibility.CONVERTIBLE) return BigDecimal.valueOf(0);
if (!(checkSimpleUnit(source) && checkSimpleUnit(target))) return BigDecimal.valueOf(0);
String sourceSimpleIdentifier = source.getSingleUnits().get(0).getSimpleUnit();
String targetSimpleIdentifier = target.getSingleUnits().get(0).getSimpleUnit();
BigDecimal sourceOffset = this.mapToConversionRate.get(sourceSimpleIdentifier).getOffset();
BigDecimal targetOffset = this.mapToConversionRate.get(targetSimpleIdentifier).getOffset();
return sourceOffset
.subtract(targetOffset)
.divide(targetToBase.getConversionRate(), MathContext.DECIMAL128);
}
public MeasureUnitImpl extractCompoundBaseUnit(MeasureUnitImpl measureUnit) {
ArrayList<SingleUnitImpl> baseUnits = this.extractBaseUnits(measureUnit);
MeasureUnitImpl result = new MeasureUnitImpl();
for (SingleUnitImpl baseUnit :
baseUnits) {
result.appendSingleUnit(baseUnit);
}
return result;
}
public ArrayList<SingleUnitImpl> extractBaseUnits(MeasureUnitImpl measureUnitImpl) {
ArrayList<SingleUnitImpl> result = new ArrayList<>();
ArrayList<SingleUnitImpl> singleUnits = measureUnitImpl.getSingleUnits();
for (SingleUnitImpl singleUnit :
singleUnits) {
result.addAll(extractBaseUnits(singleUnit));
}
return result;
}
/**
* @param singleUnit
* @return The bese units in the `SingleUnitImpl` with applying the dimensionality only and not the SI prefix.
* <p>
* NOTE:
* This method is helpful when checking the convertibility because no need to check convertibility.
*/
public ArrayList<SingleUnitImpl> extractBaseUnits(SingleUnitImpl singleUnit) {
String target = mapToConversionRate.get(singleUnit.getSimpleUnit()).getTarget();
MeasureUnitImpl targetImpl = MeasureUnitImpl.UnitsParser.parseForIdentifier(target);
// Each unit must be powered by the same dimension
targetImpl.applyDimensionality(singleUnit.getDimensionality());
// NOTE: we do not apply SI prefixes.
return targetImpl.getSingleUnits();
}
/**
* Checks if the `MeasureUnitImpl` is simple or not.
*
* @param measureUnitImpl
* @return true if the `MeasureUnitImpl` is simple, false otherwise.
*/
private boolean checkSimpleUnit(MeasureUnitImpl measureUnitImpl) {
if (measureUnitImpl.getComplexity() != MeasureUnit.Complexity.SINGLE) return false;
SingleUnitImpl singleUnit = measureUnitImpl.getSingleUnits().get(0);
if (singleUnit.getSiPrefix() != MeasureUnit.SIPrefix.ONE) return false;
if (singleUnit.getDimensionality() != 1) return false;
return true;
}
public static class ConversionRatesSink extends UResource.Sink {
/**
* Map from any simple unit (i.e. "meter", "foot", "inch") to its basic/root conversion rate info.
*/
private HashMap<String, ConversionRateInfo> mapToConversionRate = new HashMap<>();
@Override
public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
assert (UnitsData.Constants.CONVERSION_UNIT_TABLE_NAME.equals(key.toString()));
UResource.Table conversionRateTable = value.getTable();
for (int i = 0; conversionRateTable.getKeyAndValue(i, key, value); i++) {
assert (value.getType() == UResourceBundle.TABLE);
String simpleUnit = key.toString();
UResource.Table simpleUnitConversionInfo = value.getTable();
String target = null;
String factor = null;
String offset = "0";
for (int j = 0; simpleUnitConversionInfo.getKeyAndValue(j, key, value); j++) {
assert (value.getType() == UResourceBundle.STRING);
String keyString = key.toString();
String valueString = value.toString().replaceAll(" ", "");
if ("target".equals(keyString)) {
target = valueString;
} else if ("factor".equals(keyString)) {
factor = valueString;
} else if ("offset".equals(keyString)) {
offset = valueString;
} else {
assert false : "The key must be target, factor or offset";
}
}
// HERE a single conversion rate data should be loaded
assert (target != null);
assert (factor != null);
mapToConversionRate.put(simpleUnit, new ConversionRateInfo(simpleUnit, target, factor, offset));
}
}
public HashMap<String, ConversionRateInfo> getMapToConversionRate() {
return mapToConversionRate;
}
}
public static class ConversionRateInfo {
private final String simpleUnit;
private final String target;
private final String conversionRate;
private final BigDecimal offset;
public ConversionRateInfo(String simpleUnit, String target, String conversionRate, String offset) {
this.simpleUnit = simpleUnit;
this.target = target;
this.conversionRate = conversionRate;
this.offset = forNumberWithDivision(offset);
}
private static BigDecimal forNumberWithDivision(String numberWithDivision) {
String[] numbers = numberWithDivision.split("/");
assert (numbers.length <= 2);
if (numbers.length == 1) {
return new BigDecimal(numbers[0]);
}
return new BigDecimal(numbers[0]).divide(new BigDecimal(numbers[1]), MathContext.DECIMAL128);
}
/**
* @return the base unit.
* <p>
* For example:
* ("meter", "foot", "inch", "mile" ... etc.) have "meter" as a base/root unit.
*/
public String getTarget() {
return this.target;
}
/**
* @return The offset from this unit to the base unit.
*/
public BigDecimal getOffset() {
return this.offset;
}
/**
* @return The conversion rate from this unit to the base unit.
*/
public String getConversionRate() {
return conversionRate;
}
}
}

View file

@ -4,8 +4,14 @@
package com.ibm.icu.impl.units;
import com.ibm.icu.util.*;
import com.ibm.icu.util.BytesTrie;
import com.ibm.icu.util.CharsTrie;
import com.ibm.icu.util.CharsTrieBuilder;
import com.ibm.icu.util.ICUCloneNotSupportedException;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.StringTrieBuilder;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@ -114,6 +120,17 @@ public class MeasureUnitImpl {
return result;
}
/**
* Applies dimensionality to all the internal single units.
* For example: <b>square-meter-per-second</b>, when we apply dimensionality -2, it will be <b>square-second-per-p4-meter</b>
*/
public void applyDimensionality(int dimensionality) {
for (SingleUnitImpl singleUnit :
singleUnits) {
singleUnit.setDimensionality(singleUnit.getDimensionality() * dimensionality);
}
}
/**
* Mutates this MeasureUnitImpl to append a single unit.
*
@ -728,10 +745,24 @@ public class MeasureUnitImpl {
}
}
class SingleUnitComparator implements Comparator<SingleUnitImpl> {
static class MeasureUnitImplComparator implements Comparator<MeasureUnitImpl> {
private ConversionRates conversionRates;
public MeasureUnitImplComparator(ConversionRates conversionRates) {
this.conversionRates = conversionRates;
}
@Override
public int compare(MeasureUnitImpl o1, MeasureUnitImpl o2) {
UnitConverter fromO1toO2 = new UnitConverter(o1, o2, conversionRates);
return fromO1toO2.convert(BigDecimal.valueOf(1)).compareTo(BigDecimal.valueOf(1));
}
}
static class SingleUnitComparator implements Comparator<SingleUnitImpl> {
@Override
public int compare(SingleUnitImpl o1, SingleUnitImpl o2) {
return o1.compareTo(o2);
}
}
}
}

View file

@ -1,7 +1,6 @@
// © 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.util.MeasureUnit;

View file

@ -0,0 +1,311 @@
// © 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.util.MeasureUnit;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.regex.Pattern;
import static java.math.MathContext.DECIMAL128;
public class UnitConverter {
private BigDecimal conversionRate;
private BigDecimal offset;
/**
* Constructor of `UnitConverter`.
* NOTE:
* - source and target must be under the same category
* - e.g. meter to mile --> both of them are length units.
*
* @param source represents the source unit.
* @param target represents the target unit.
* @param conversionRates contains all the needed conversion rates.
*/
public UnitConverter(MeasureUnitImpl source, MeasureUnitImpl target, ConversionRates conversionRates) {
Convertibility convertibility = extractConvertibility(source, target, conversionRates);
assert (convertibility == Convertibility.CONVERTIBLE || convertibility == Convertibility.RECIPROCAL);
Factor sourceToBase = conversionRates.getFactorToBase(source);
Factor targetToBase = conversionRates.getFactorToBase(target);
if (convertibility == Convertibility.CONVERTIBLE) {
this.conversionRate = sourceToBase.divide(targetToBase).getConversionRate();
} else {
this.conversionRate = sourceToBase.multiply(targetToBase).getConversionRate();
}
// calculate the offset
this.offset = conversionRates.getOffset(source, target, sourceToBase, targetToBase, convertibility);
}
static public Convertibility extractConvertibility(MeasureUnitImpl source, MeasureUnitImpl target, ConversionRates conversionRates) {
ArrayList<SingleUnitImpl> sourceSingleUnits = conversionRates.extractBaseUnits(source);
ArrayList<SingleUnitImpl> targetSingleUnits = conversionRates.extractBaseUnits(target);
HashMap<String, Integer> dimensionMap = new HashMap<>();
insertInMap(dimensionMap, sourceSingleUnits, 1);
insertInMap(dimensionMap, targetSingleUnits, -1);
if (areDimensionsZeroes(dimensionMap)) return Convertibility.CONVERTIBLE;
insertInMap(dimensionMap, targetSingleUnits, 2);
if (areDimensionsZeroes(dimensionMap)) return Convertibility.RECIPROCAL;
return Convertibility.UNCONVERTIBLE;
}
/**
* Helpers
*/
private static void insertInMap(HashMap<String, Integer> dimensionMap, ArrayList<SingleUnitImpl> singleUnits, int multiplier) {
for (SingleUnitImpl singleUnit :
singleUnits) {
if (dimensionMap.containsKey(singleUnit.getSimpleUnit())) {
dimensionMap.put(singleUnit.getSimpleUnit(), dimensionMap.get(singleUnit.getSimpleUnit()) + singleUnit.getDimensionality() * multiplier);
} else {
dimensionMap.put(singleUnit.getSimpleUnit(), singleUnit.getDimensionality() * multiplier);
}
}
}
private static boolean areDimensionsZeroes(HashMap<String, Integer> dimensionMap) {
for (Integer value :
dimensionMap.values()) {
if (!value.equals(0)) return false;
}
return true;
}
public BigDecimal convert(BigDecimal inputValue) {
return inputValue.multiply(this.conversionRate).add(offset);
}
public enum Convertibility {
CONVERTIBLE,
RECIPROCAL,
UNCONVERTIBLE,
}
// TODO: improve documentation and Constant implementation
/**
* Responsible for all the Factor operation
* NOTE:
* This class is immutable
*/
static class Factor {
private BigDecimal factorNum;
private BigDecimal factorDen;
/* FACTOR CONSTANTS */
private int
CONSTANT_FT2M = 0, // ft2m stands for foot to meter.
CONSTANT_PI = 0, // PI
CONSTANT_GRAVITY = 0, // Gravity
CONSTANT_G = 0,
CONSTANT_GAL_IMP2M3 = 0, // Gallon imp to m3
CONSTANT_LB2KG = 0; // Pound to Kilogram
/**
* Creates Empty Factor
*/
public Factor() {
this.factorNum = BigDecimal.valueOf(1);
this.factorDen = BigDecimal.valueOf(1);
}
public static Factor processFactor(String factor) {
assert (!factor.isEmpty());
// Remove all spaces in the factor
factor.replaceAll("\\s+", "");
String[] fractions = factor.split("/");
assert (fractions.length == 1 || fractions.length == 2);
if (fractions.length == 1) {
return processFactorWithoutDivision(fractions[0]);
}
Factor num = processFactorWithoutDivision(fractions[0]);
Factor den = processFactorWithoutDivision(fractions[1]);
return num.divide(den);
}
private static Factor processFactorWithoutDivision(String factorWithoutDivision) {
Factor result = new Factor();
for (String poweredEntity :
factorWithoutDivision.split(Pattern.quote("*"))) {
result.addPoweredEntity(poweredEntity);
}
return result;
}
/**
* Clone this <code>Factor</code>.
*/
protected Factor clone() {
Factor result = new Factor();
result.factorNum = this.factorNum;
result.factorDen = this.factorDen;
result.CONSTANT_FT2M = this.CONSTANT_FT2M;
result.CONSTANT_PI = this.CONSTANT_PI;
result.CONSTANT_GRAVITY = this.CONSTANT_GRAVITY;
result.CONSTANT_G = this.CONSTANT_G;
result.CONSTANT_GAL_IMP2M3 = this.CONSTANT_GAL_IMP2M3;
result.CONSTANT_LB2KG = this.CONSTANT_LB2KG;
return result;
}
/**
* Returns a single `BigDecimal` that represent the conversion rate after substituting all the constants.
*
* @return
*/
public BigDecimal getConversionRate() {
Factor resultCollector = this.clone();
resultCollector.substitute(new BigDecimal("0.3048"), this.CONSTANT_FT2M);
resultCollector.substitute(new BigDecimal("411557987.0").divide(new BigDecimal("131002976.0"), DECIMAL128), this.CONSTANT_PI);
resultCollector.substitute(new BigDecimal("9.80665"), this.CONSTANT_GRAVITY);
resultCollector.substitute(new BigDecimal("6.67408E-11"), this.CONSTANT_G);
resultCollector.substitute(new BigDecimal("0.00454609"), this.CONSTANT_GAL_IMP2M3);
resultCollector.substitute(new BigDecimal("0.45359237"), this.CONSTANT_LB2KG);
return resultCollector.factorNum.divide(resultCollector.factorDen, DECIMAL128);
}
private void substitute(BigDecimal value, int power) {
if (power == 0) return;
BigDecimal absPoweredValue = value.pow(Math.abs(power), DECIMAL128);
if (power > 0) {
this.factorNum = this.factorNum.multiply(absPoweredValue);
} else {
this.factorDen = this.factorDen.multiply(absPoweredValue);
}
}
public Factor applySiPrefix(MeasureUnit.SIPrefix siPrefix) {
Factor result = this.clone();
if (siPrefix == MeasureUnit.SIPrefix.ONE) {
return result;
}
BigDecimal siApplied = BigDecimal.valueOf(Math.pow(10.0, Math.abs(siPrefix.getSiPrefixPower())));
if (siPrefix.getSiPrefixPower() < 0) {
result.factorDen = this.factorDen.multiply(siApplied);
return result;
}
result.factorNum = this.factorNum.multiply(siApplied);
return result;
}
public Factor power(int power) {
Factor result = new Factor();
if (power == 0) return result;
if (power > 0) {
result.factorNum = this.factorNum.pow(power);
result.factorDen = this.factorDen.pow(power);
} else {
result.factorNum = this.factorDen.pow(power * -1);
result.factorDen = this.factorNum.pow(power * -1);
}
result.CONSTANT_FT2M = this.CONSTANT_FT2M * power;
result.CONSTANT_PI = this.CONSTANT_PI * power;
result.CONSTANT_GRAVITY = this.CONSTANT_GRAVITY * power;
result.CONSTANT_G = this.CONSTANT_G * power;
result.CONSTANT_GAL_IMP2M3 = this.CONSTANT_GAL_IMP2M3 * power;
result.CONSTANT_LB2KG = this.CONSTANT_LB2KG * power;
return result;
}
public Factor divide(Factor other) {
Factor result = new Factor();
result.factorNum = this.factorNum.multiply(other.factorDen);
result.factorDen = this.factorDen.multiply(other.factorNum);
result.CONSTANT_FT2M = this.CONSTANT_FT2M - other.CONSTANT_FT2M;
result.CONSTANT_PI = this.CONSTANT_PI - other.CONSTANT_PI;
result.CONSTANT_GRAVITY = this.CONSTANT_GRAVITY - other.CONSTANT_GRAVITY;
result.CONSTANT_G = this.CONSTANT_G - other.CONSTANT_G;
result.CONSTANT_GAL_IMP2M3 = this.CONSTANT_GAL_IMP2M3 - other.CONSTANT_GAL_IMP2M3;
result.CONSTANT_LB2KG = this.CONSTANT_LB2KG - other.CONSTANT_LB2KG;
return result;
}
public Factor multiply(Factor other) {
Factor result = new Factor();
result.factorNum = this.factorNum.multiply(other.factorNum);
result.factorDen = this.factorDen.multiply(other.factorDen);
result.CONSTANT_FT2M = this.CONSTANT_FT2M + other.CONSTANT_FT2M;
result.CONSTANT_PI = this.CONSTANT_PI + other.CONSTANT_PI;
result.CONSTANT_GRAVITY = this.CONSTANT_GRAVITY + other.CONSTANT_GRAVITY;
result.CONSTANT_G = this.CONSTANT_G + other.CONSTANT_G;
result.CONSTANT_GAL_IMP2M3 = this.CONSTANT_GAL_IMP2M3 + other.CONSTANT_GAL_IMP2M3;
result.CONSTANT_LB2KG = this.CONSTANT_LB2KG + other.CONSTANT_LB2KG;
return result;
}
/**
* Adds Entity with power or not. For example, `12 ^ 3` or `12`.
*
* @param poweredEntity
*/
private void addPoweredEntity(String poweredEntity) {
String[] entities = poweredEntity.split(Pattern.quote("^"));
assert (entities.length == 1 || entities.length == 2);
int power = entities.length == 2 ? Integer.parseInt(entities[1]) : 1;
this.addEntity(entities[0], power);
}
private void addEntity(String entity, int power) {
if ("ft_to_m".equals(entity)) {
this.CONSTANT_FT2M += power;
} else if ("ft2_to_m2".equals(entity)) {
this.CONSTANT_FT2M += 2 * power;
} else if ("ft3_to_m3".equals(entity)) {
this.CONSTANT_FT2M += 3 * power;
} else if ("in3_to_m3".equals(entity)) {
this.CONSTANT_FT2M += 3 * power;
this.factorDen = this.factorDen.multiply(BigDecimal.valueOf(Math.pow(12, 3)));
} else if ("gal_to_m3".equals(entity)) {
this.factorNum = this.factorNum.multiply(BigDecimal.valueOf(231));
this.CONSTANT_FT2M += 3 * power;
this.factorDen = this.factorDen.multiply(BigDecimal.valueOf(12 * 12 * 12));
} else if ("gal_imp_to_m3".equals(entity)) {
this.CONSTANT_GAL_IMP2M3 += power;
} else if ("G".equals(entity)) {
this.CONSTANT_G += power;
} else if ("gravity".equals(entity)) {
this.CONSTANT_GRAVITY += power;
} else if ("lb_to_kg".equals(entity)) {
this.CONSTANT_LB2KG += power;
} else if ("PI".equals(entity)) {
this.CONSTANT_PI += power;
} else {
BigDecimal decimalEntity = new BigDecimal(entity).pow(power, DECIMAL128);
this.factorNum = this.factorNum.multiply(decimalEntity);
}
}
}
}

View file

@ -0,0 +1,210 @@
// © 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.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.util.UResourceBundle;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
public class UnitPreferences {
private HashMap<String, HashMap<String, UnitPreference[]>> mapToUnitPreferences = new HashMap<>();
public UnitPreferences() {
// Read unit preferences
ICUResourceBundle resource;
resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "units");
UnitPreferencesSink sink = new UnitPreferencesSink();
resource.getAllItemsWithFallback(UnitsData.Constants.UNIT_PREFERENCE_TABLE_NAME, sink);
this.mapToUnitPreferences = sink.getMapToUnitPreferences();
}
public static String formMapKey(String category, String usage) {
return category + "++" + usage;
}
/**
* Extracts all the sub-usages from a usage including the default one in the end.
* The usages will be in order starting with the longest matching one.
* For example:
* if usage : "person-height-child"
* the function will return: "person-height-child"
* "person-height"
* "person"
* "default"
*
* @param usage
* @return
*/
private static String[] getAllUsages(String usage) {
ArrayList<String> result = new ArrayList<>();
result.add(usage);
for (int i = usage.length() - 1; i >= 0; --i) {
if (usage.charAt(i) == '-') {
result.add(usage.substring(0, i));
}
}
if (!usage.equals(UnitsData.Constants.DEFAULT_USAGE)) { // Do not add default usage twice.
result.add(UnitsData.Constants.DEFAULT_USAGE);
}
return result.toArray(new String[0]);
}
public UnitPreference[] getPreferencesFor(String category, String usage, String region) {
String[] subUsages = getAllUsages(usage);
UnitPreference[] result = null;
for (String subUsage :
subUsages) {
result = getUnitPreferences(category, subUsage, region);
if (result != null) break;
}
assert (result != null) : "At least the category must be exist";
return result;
}
/**
* @param category
* @param usage
* @param region
* @return null if there is no entry associated to the category and usage. O.W. returns the corresponding UnitPreference[]
*/
private UnitPreference[] getUnitPreferences(String category, String usage, String region) {
String key = formMapKey(category, usage);
if (this.mapToUnitPreferences.containsKey(key)) {
HashMap<String, UnitPreference[]> unitPreferencesMap = this.mapToUnitPreferences.get(key);
UnitPreference[] result =
unitPreferencesMap.containsKey(region) ?
unitPreferencesMap.get(region) :
unitPreferencesMap.get(UnitsData.Constants.DEFAULT_REGION);
assert (result != null);
return result;
}
return null;
}
public static class UnitPreference {
private final String unit;
private final BigDecimal geq;
private final String skeleton;
public UnitPreference(String unit, String geq, String skeleton) {
this.unit = unit;
this.geq = new BigDecimal(geq);
this.skeleton = skeleton;
}
public String getUnit() {
return this.unit;
}
public BigDecimal getGeq() {
return geq;
}
public String getSkeleton() {
return skeleton;
}
}
public static class UnitPreferencesSink extends UResource.Sink {
private HashMap<String, HashMap<String, UnitPreference[]>> mapToUnitPreferences;
public UnitPreferencesSink() {
this.mapToUnitPreferences = new HashMap<>();
}
public HashMap<String, HashMap<String, UnitPreference[]>> getMapToUnitPreferences() {
return mapToUnitPreferences;
}
/**
* The unitPreferenceData structure (see icu4c/source/data/misc/units.txt) contains a
* hierarchy of category/usage/region, within which are a set of
* preferences. Hence three for-loops and another loop for the
* preferences themselves.
*/
@Override
public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
assert (UnitsData.Constants.UNIT_PREFERENCE_TABLE_NAME.equals(key.toString()));
UResource.Table categoryTable = value.getTable();
for (int i = 0; categoryTable.getKeyAndValue(i, key, value); i++) {
assert (value.getType() == UResourceBundle.TABLE);
String category = key.toString();
UResource.Table usageTable = value.getTable();
for (int j = 0; usageTable.getKeyAndValue(j, key, value); j++) {
assert (value.getType() == UResourceBundle.TABLE);
String usage = key.toString();
UResource.Table regionTable = value.getTable();
for (int k = 0; regionTable.getKeyAndValue(k, key, value); k++) {
assert (value.getType() == UResourceBundle.ARRAY);
String region = key.toString();
UResource.Array preferencesTable = value.getArray();
ArrayList<UnitPreference> unitPreferences = new ArrayList<>();
for (int l = 0; preferencesTable.getValue(l, value); l++) {
assert (value.getType() == UResourceBundle.TABLE);
UResource.Table singlePrefTable = value.getTable();
// TODO collect the data
String unit = null;
String geq = "1";
String skeleton = "";
for (int m = 0; singlePrefTable.getKeyAndValue(m, key, value); m++) {
assert (value.getType() == UResourceBundle.STRING);
String keyString = key.toString();
if ("unit".equals(keyString)) {
unit = value.getString();
} else if ("geq".equals(keyString)) {
geq = value.getString();
} else if ("skeleton".equals(keyString)) {
skeleton = value.getString();
} else {
assert false : "key must be unit, geq or skeleton";
}
}
assert (unit != null);
unitPreferences.add(new UnitPreference(unit, geq, skeleton));
}
assert (!unitPreferences.isEmpty());
this.insertUnitPreferences(
category,
usage,
region,
unitPreferences.toArray(new UnitPreference[0])
);
}
}
}
}
private void insertUnitPreferences(String category, String usage, String region, UnitPreference[] unitPreferences) {
String key = formMapKey(category, usage);
HashMap<String, UnitPreference[]> shouldInsert;
if (this.mapToUnitPreferences.containsKey(key)) {
shouldInsert = this.mapToUnitPreferences.get(key);
} else {
shouldInsert = new HashMap<>();
this.mapToUnitPreferences.put(key, shouldInsert);
}
shouldInsert.put(region, unitPreferences);
}
}
}

View file

@ -7,15 +7,29 @@ package com.ibm.icu.impl.units;
import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.UResourceBundle;
import java.util.ArrayList;
import java.util.HashMap;
/**
* Responsible for all units data operations (retriever, analysis, extraction certain data ... etc.).
*/
class UnitsData {
public class UnitsData {
private volatile static String[] simpleUnits = null;
private ConversionRates conversionRates;
private UnitPreferences unitPreferences;
/**
* Pairs of categories and the corresponding base units.
*/
private Categories categories;
public UnitsData() {
this.conversionRates = new ConversionRates();
this.unitPreferences = new UnitPreferences();
this.categories = new Categories();
}
public static String[] getSimpleUnits() {
if (simpleUnits != null) {
@ -32,6 +46,38 @@ class UnitsData {
return simpleUnits;
}
public ConversionRates getConversionRates() {
return conversionRates;
}
public UnitPreferences getUnitPreferences() {
return unitPreferences;
}
/**
* @param measureUnit
* @return the corresponding category.
*/
public String getCategory(MeasureUnitImpl measureUnit) {
MeasureUnitImpl baseMeasureUnit
= this.getConversionRates().extractCompoundBaseUnit(measureUnit);
String baseUnitIdentifier = MeasureUnit.fromMeasureUnitImpl(baseMeasureUnit).getIdentifier();
if (baseUnitIdentifier.equals("meter-per-cubic-meter")) {
// TODO(CLDR-13787,hugovdm): special-casing the consumption-inverse
// case. Once CLDR-13787 is clarified, this should be generalised (or
// possibly removed):
return "consumption-inverse";
}
return this.categories.mapFromUnitToCategory.get(baseUnitIdentifier);
}
public UnitPreferences.UnitPreference[] getPreferencesFor(String category, String usage, String region) {
return this.unitPreferences.getPreferencesFor(category, usage, region);
}
public static class SimpleUnitIdentifiersSink extends UResource.Sink {
String[] simpleUnits = null;
@ -89,4 +135,51 @@ class UnitsData {
public static final String DEFAULT_REGION = "001";
public static final String DEFAULT_USAGE = "default";
}
public static class Categories {
/**
* Contains the map between units in their base units into their category.
* For example: meter-per-second --> "speed"
*/
HashMap<String, String> mapFromUnitToCategory;
public Categories() {
// Read unit Categories
ICUResourceBundle resource;
resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "units");
CategoriesSink sink = new CategoriesSink();
resource.getAllItemsWithFallback(Constants.CATEGORY_TABLE_NAME, sink);
this.mapFromUnitToCategory = sink.getMapFromUnitToCategory();
}
}
public static class CategoriesSink extends UResource.Sink {
/**
* Contains the map between units in their base units into their category.
* For example: meter-per-second --> "speed"
*/
HashMap<String, String> mapFromUnitToCategory;
public CategoriesSink() {
mapFromUnitToCategory = new HashMap<>();
}
@Override
public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
assert (key.toString() == Constants.CATEGORY_TABLE_NAME);
assert (value.getType() == UResourceBundle.TABLE);
UResource.Table categoryTable = value.getTable();
for (int i = 0; categoryTable.getKeyAndValue(i, key, value); i++) {
assert (value.getType() == UResourceBundle.STRING);
mapFromUnitToCategory.put(key.toString(), value.toString());
}
}
public HashMap<String, String> getMapFromUnitToCategory() {
return mapFromUnitToCategory;
}
}
}

View file

@ -0,0 +1,144 @@
// © 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.util.Measure;
import com.ibm.icu.util.MeasureUnit;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* `UnitsRouter` responsible for converting from a single unit (such as `meter` or `meter-per-second`) to
* one of the complex units based on the limits.
* For example:
* if the input is `meter` and the output as following
* {`foot+inch`, limit: 3.0}
* {`inch` , limit: no value (-inf)}
* Thus means if the input in `meter` is greater than or equal to `3.0 feet`, the output will be in
* `foot+inch`, otherwise, the output will be in `inch`.
* <p>
* NOTE:
* the output units and the their limits MUST BE in order, for example, if the output units, from the
* previous example, are the following:
* {`inch` , limit: no value (-inf)}
* {`foot+inch`, limit: 3.0}
* IN THIS CASE THE OUTPUT WILL BE ALWAYS IN `inch`.
* <p>
* NOTE:
* the output units and their limits will be extracted from the units preferences database by knowing
* the followings:
* - input unit
* - locale
* - usage
* <p>
* DESIGN:
* `UnitRouter` uses internally `ComplexUnitConverter` in order to convert the input units to the
* desired complex units and to check the limit too.
*/
public class UnitsRouter {
// List of possible output units. TODO: converterPreferences_ now also has
// this data available. Maybe drop outputUnits_ and have getOutputUnits
// construct a the list from data in converterPreferences_ instead?
private ArrayList<MeasureUnit> outputUnits_ = new ArrayList<>();
private ArrayList<ConverterPreference> converterPreferences_ = new ArrayList<>();
public UnitsRouter(MeasureUnitImpl inputUnitImpl, String region, String usage) {
// TODO: do we want to pass in ConversionRates and UnitPreferences instead?
// of loading in each UnitsRouter instance? (Or make global?)
UnitsData data = new UnitsData();
//MeasureUnitImpl inputUnitImpl = MeasureUnitImpl.forMeasureUnitMaybeCopy(inputUnit);
String category = data.getCategory(inputUnitImpl);
UnitPreferences.UnitPreference[] unitPreferences = data.getPreferencesFor(category, usage, region);
for (int i = 0; i < unitPreferences.length; ++i) {
UnitPreferences.UnitPreference preference = unitPreferences[i];
MeasureUnitImpl complexTargetUnitImpl =
MeasureUnitImpl.UnitsParser.parseForIdentifier(preference.getUnit());
String precision = preference.getSkeleton();
// For now, we only have "precision-increment" in Units Preferences skeleton.
// Therefore, we check if the skeleton starts with "precision-increment" and force the program to
// fail otherwise.
// NOTE:
// It is allowed to have an empty precision.
if (!precision.isEmpty() && !precision.startsWith("precision-increment")) {
throw new AssertionError("Only `precision-increment` is allowed");
}
outputUnits_.add(complexTargetUnitImpl.build());
converterPreferences_.add(new ConverterPreference(inputUnitImpl, complexTargetUnitImpl,
preference.getGeq(), precision,
data.getConversionRates()));
}
}
public RouteResult route(BigDecimal quantity) {
for (ConverterPreference converterPreference :
converterPreferences_) {
if (converterPreference.converter.greaterThanOrEqual(quantity, converterPreference.limit)) {
return new RouteResult(converterPreference.converter.convert(quantity), converterPreference.precision);
}
}
// In case of the `quantity` does not fit in any converter limit, use the last converter.
ConverterPreference lastConverterPreference = converterPreferences_.get(converterPreferences_.size() - 1);
return new RouteResult(lastConverterPreference.converter.convert(quantity), lastConverterPreference.precision);
}
/**
* Returns the list of possible output units, i.e. the full set of
* preferences, for the localized, usage-specific unit preferences.
* <p>
* The returned pointer should be valid for the lifetime of the
* UnitsRouter instance.
*/
public ArrayList<MeasureUnit> getOutputUnits() {
return this.outputUnits_;
}
/**
* Contains the complex unit converter and the limit which representing the smallest value that the
* converter should accept. For example, if the converter is converting to `foot+inch` and the limit
* equals 3.0, thus means the converter should not convert to a value less than `3.0 feet`.
* <p>
* NOTE:
* if the limit doest not has a value `i.e. (std::numeric_limits<double>::lowest())`, this mean there
* is no limit for the converter.
*/
public static class ConverterPreference {
ComplexUnitsConverter converter;
BigDecimal limit;
String precision;
// In case there is no limit, the limit will be -inf.
public ConverterPreference(MeasureUnitImpl source, MeasureUnitImpl outputUnits,
String precision, ConversionRates conversionRates) {
this(source, outputUnits, BigDecimal.valueOf(Double.MIN_VALUE), precision,
conversionRates);
}
public ConverterPreference(MeasureUnitImpl source, MeasureUnitImpl outputUnits,
BigDecimal limit, String precision, ConversionRates conversionRates) {
this.converter = new ComplexUnitsConverter(source, outputUnits, conversionRates);
this.limit = limit;
this.precision = precision;
}
}
public class RouteResult {
public List<Measure> measures;
public String precision;
RouteResult(List<Measure> measures, String precision) {
this.measures = measures;
this.precision = precision;
}
}
}

View file

@ -32,7 +32,6 @@ import com.ibm.icu.impl.units.SingleUnitImpl;
import com.ibm.icu.text.UnicodeSet;
/**
* A unit such as length, mass, volume, currency, etc. A unit is
* coupled with a numeric amount to produce a Measure. MeasureUnit objects are immutable.

View file

@ -0,0 +1,366 @@
// © 2020 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
package com.ibm.icu.dev.test.impl;
import com.ibm.icu.dev.test.TestUtil;
import com.ibm.icu.impl.Pair;
import com.ibm.icu.impl.units.ComplexUnitsConverter;
import com.ibm.icu.impl.units.ConversionRates;
import com.ibm.icu.impl.units.MeasureUnitImpl;
import com.ibm.icu.impl.units.UnitConverter;
import com.ibm.icu.impl.units.UnitsRouter;
import com.ibm.icu.util.Measure;
import com.ibm.icu.util.MeasureUnit;
import org.junit.Test;
import java.io.BufferedReader;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.*;
public class UnitsTest {
public static boolean compareTwoBigDecimal(BigDecimal expected, BigDecimal actual, BigDecimal delta) {
BigDecimal diff =
expected.abs().compareTo(BigDecimal.ZERO) < 1 ?
expected.subtract(actual).abs() :
(expected.subtract(actual).divide(expected, MathContext.DECIMAL128)).abs();
if (diff.compareTo(delta) == -1) return true;
return false;
}
@Test
public void testComplexUnitsConverter() {
ConversionRates rates = new ConversionRates();
MeasureUnit input = MeasureUnit.FOOT;
MeasureUnit output = MeasureUnit.forIdentifier("foot-and-inch");
final MeasureUnitImpl inputImpl = MeasureUnitImpl.forIdentifier(input.getIdentifier());
final MeasureUnitImpl outputImpl = MeasureUnitImpl.forIdentifier(output.getIdentifier());
ComplexUnitsConverter converter = new ComplexUnitsConverter(inputImpl, outputImpl, rates);
// Significantly less than 2.0.
List<Measure> measures = converter.convert(BigDecimal.valueOf(1.9999));
assertEquals("measures length", 2, measures.size());
assertEquals("1.9999: measures[0] value", BigDecimal.valueOf(1), measures.get(0).getNumber());
assertEquals("1.9999: measures[0] unit", MeasureUnit.FOOT.getIdentifier(),
measures.get(0).getUnit().getIdentifier());
assertTrue("1.9999: measures[1] value", compareTwoBigDecimal(BigDecimal.valueOf(11.9988),
BigDecimal.valueOf(measures.get(1).getNumber().doubleValue()), BigDecimal.valueOf(0.0001)));
assertEquals("1.9999: measures[1] unit", MeasureUnit.INCH.getIdentifier(),
measures.get(1).getUnit().getIdentifier());
// TODO: consider factoring out the set of tests to make this function more
// data-driven, *after* dealing appropriately with the memory leaks that can
// be demonstrated by this code.
// TODO: reusing measures results in a leak.
// A minimal nudge under 2.0.
List<Measure> measures2 = converter.convert(BigDecimal.valueOf(2.0).subtract(ComplexUnitsConverter.EPSILON));
assertEquals("measures length", 2, measures2.size());
assertEquals("1 - eps: measures[0] value", BigDecimal.valueOf(2), measures2.get(0).getNumber());
assertEquals("1 - eps: measures[0] unit", MeasureUnit.FOOT.getIdentifier(),
measures2.get(0).getUnit().getIdentifier());
assertEquals("1 - eps: measures[1] value", BigDecimal.ZERO, measures2.get(1).getNumber());
assertEquals("1 - eps: measures[1] unit", MeasureUnit.INCH.getIdentifier(),
measures2.get(1).getUnit().getIdentifier());
// Testing precision with meter and light-year. 1e-16 light years is
// 0.946073 meters, and double precision can provide only ~15 decimal
// digits, so we don't expect to get anything less than 1 meter.
// An epsilon's nudge under one light-year: should give 1 ly, 0 m.
input = MeasureUnit.LIGHT_YEAR;
output = MeasureUnit.forIdentifier("light-year-and-meter");
final MeasureUnitImpl inputImpl3 = MeasureUnitImpl.forIdentifier(input.getIdentifier());
final MeasureUnitImpl outputImpl3 = MeasureUnitImpl.forIdentifier(output.getIdentifier());
// TODO: reusing converter results in a leak.
ComplexUnitsConverter converter3 = new ComplexUnitsConverter(inputImpl3, outputImpl3, rates);
// TODO: reusing measures results in a leak.
List<Measure> measures3 = converter3.convert(BigDecimal.valueOf(2.0).subtract(ComplexUnitsConverter.EPSILON));
assertEquals("measures length", 2, measures3.size());
assertEquals("light-year test: measures[0] value", BigDecimal.valueOf(2), measures3.get(0).getNumber());
assertEquals("light-year test: measures[0] unit", MeasureUnit.LIGHT_YEAR.getIdentifier(),
measures3.get(0).getUnit().getIdentifier());
assertEquals("light-year test: measures[1] value", BigDecimal.ZERO, measures3.get(1).getNumber());
assertEquals("light-year test: measures[1] unit", MeasureUnit.METER.getIdentifier(),
measures3.get(1).getUnit().getIdentifier());
// 1e-15 light years is 9.46073 meters (calculated using "bc" and the CLDR
// conversion factor). With double-precision maths, we get 10.5. In this
// case, we're off by almost 1 meter.
List<Measure> measures4 = converter3.convert(BigDecimal.valueOf(1.0 + 1e-15));
assertEquals("measures length", 2, measures4.size());
assertEquals("light-year test: measures[0] value", BigDecimal.ONE, measures4.get(0).getNumber());
assertEquals("light-year test: measures[0] unit", MeasureUnit.LIGHT_YEAR.getIdentifier(),
measures4.get(0).getUnit().getIdentifier());
assertTrue("light-year test: measures[1] value", compareTwoBigDecimal(BigDecimal.valueOf(10),
BigDecimal.valueOf(measures4.get(1).getNumber().doubleValue()),
BigDecimal.valueOf(1)));
assertEquals("light-year test: measures[1] unit", MeasureUnit.METER.getIdentifier(),
measures4.get(1).getUnit().getIdentifier());
// 2e-16 light years is 1.892146 meters. We consider this in the noise, and
// thus expect a 0. (This test fails when 2e-16 is increased to 4e-16.)
List<Measure> measures5 = converter3.convert(BigDecimal.valueOf(1.0 + 2e-17));
assertEquals("measures length", 2, measures5.size());
assertEquals("light-year test: measures[0] value", BigDecimal.ONE, measures5.get(0).getNumber());
assertEquals("light-year test: measures[0] unit", MeasureUnit.LIGHT_YEAR.getIdentifier(),
measures5.get(0).getUnit().getIdentifier());
assertEquals("light-year test: measures[1] value", BigDecimal.valueOf(0.0),
measures5.get(1).getNumber());
assertEquals("light-year test: measures[1] unit", MeasureUnit.METER.getIdentifier(),
measures5.get(1).getUnit().getIdentifier());
// TODO(icu-units#63): test negative numbers!
}
@Test
public void testComplexUnitConverterSorting() {
MeasureUnitImpl source = MeasureUnitImpl.forIdentifier("meter");
MeasureUnitImpl target = MeasureUnitImpl.forIdentifier("inch-and-foot");
ConversionRates conversionRates = new ConversionRates();
ComplexUnitsConverter complexConverter = new ComplexUnitsConverter(source, target, conversionRates);
List<Measure> measures = complexConverter.convert(BigDecimal.valueOf(10.0));
assertEquals(measures.size(), 2);
assertEquals("inch-and-foot unit 0", "foot", measures.get(0).getUnit().getIdentifier());
assertEquals("inch-and-foot unit 1", "inch", measures.get(1).getUnit().getIdentifier());
assertTrue("inch-and-foot value 0", compareTwoBigDecimal(BigDecimal.valueOf(32), BigDecimal.valueOf(measures.get(0).getNumber().doubleValue()), BigDecimal.valueOf(0.0001)));
assertTrue("inch-and-foot value 1", compareTwoBigDecimal(BigDecimal.valueOf(9.7008), BigDecimal.valueOf(measures.get(1).getNumber().doubleValue()), BigDecimal.valueOf(0.0001)));
}
@Test
public void testExtractConvertibility() {
class TestData {
MeasureUnitImpl source;
MeasureUnitImpl target;
UnitConverter.Convertibility expected;
TestData(String source, String target, UnitConverter.Convertibility convertibility) {
this.source = MeasureUnitImpl.UnitsParser.parseForIdentifier(source);
this.target = MeasureUnitImpl.UnitsParser.parseForIdentifier(target);
this.expected = convertibility;
}
}
TestData[] tests = {
new TestData("meter", "foot", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("square-meter-per-square-hour", "hectare-per-square-second", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("hertz", "revolution-per-second", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("millimeter", "meter", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("yard", "meter", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("ounce-troy", "kilogram", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("percent", "portion", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("ofhg", "kilogram-per-square-meter-square-second", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("second-per-meter", "meter-per-second", UnitConverter.Convertibility.RECIPROCAL),
};
ConversionRates conversionRates = new ConversionRates();
for (TestData test :
tests) {
assertEquals(test.expected, UnitConverter.extractConvertibility(test.source, test.target, conversionRates));
}
}
@Test
public void testConverterForTemperature() {
class TestData {
MeasureUnitImpl source;
MeasureUnitImpl target;
BigDecimal input;
BigDecimal expected;
TestData(String source, String target, double input, double expected) {
this.source = MeasureUnitImpl.UnitsParser.parseForIdentifier(source);
this.target = MeasureUnitImpl.UnitsParser.parseForIdentifier(target);
this.input = BigDecimal.valueOf(input);
this.expected = BigDecimal.valueOf(expected);
}
}
TestData[] tests = {
new TestData("celsius", "fahrenheit", 1000, 1832),
new TestData("fahrenheit", "fahrenheit", 1000, 1000),
};
ConversionRates conversionRates = new ConversionRates();
for (TestData test :
tests) {
UnitConverter converter = new UnitConverter(test.source, test.target, conversionRates);
assertEquals(test.expected.doubleValue(), converter.convert(test.input).doubleValue(), (0.001));
}
}
@Test
public void testConverterFromUnitTests() throws IOException {
class TestCase {
String category;
String sourceString;
String targetString;
MeasureUnitImpl source;
MeasureUnitImpl target;
BigDecimal input;
BigDecimal expected;
TestCase(String line) {
String[] fields = line
.replaceAll(" ", "") // Remove all the spaces.
.replaceAll(",", "") // Remove all the commas.
.replaceAll("\t", "")
.split(";");
this.category = fields[0].replaceAll(" ", "");
this.sourceString = fields[1];
this.targetString = fields[2];
this.source = MeasureUnitImpl.UnitsParser.parseForIdentifier(fields[1]);
this.target = MeasureUnitImpl.UnitsParser.parseForIdentifier(fields[2]);
this.input = BigDecimal.valueOf(1000);
this.expected = new BigDecimal(fields[4]);
}
}
String codePage = "UTF-8";
BufferedReader f = TestUtil.getDataReader("cldr/units/unitsTest.txt", codePage);
ArrayList<TestCase> tests = new ArrayList<>();
while (true) {
String line = f.readLine();
if (line == null) break;
if (line.isEmpty() || line.startsWith("#")) continue;
tests.add(new TestCase(line));
}
ConversionRates conversionRates = new ConversionRates();
for (TestCase testCase :
tests) {
UnitConverter converter = new UnitConverter(testCase.source, testCase.target, conversionRates);
if (compareTwoBigDecimal(testCase.expected, converter.convert(testCase.input), BigDecimal.valueOf(0.000001))) {
continue;
} else {
fail(new StringBuilder()
.append(testCase.category)
.append(" ")
.append(testCase.sourceString)
.append(" ")
.append(testCase.targetString)
.append(" ")
.append(converter.convert(testCase.input).toString())
.append(" expected ")
.append(testCase.expected.toString())
.toString());
}
}
}
@Test
public void testUnitPreferencesFromUnitTests() throws IOException {
class TestCase {
final ArrayList<Pair<String, MeasureUnitImpl>> outputUnitInOrder = new ArrayList<>();
final ArrayList<BigDecimal> expectedInOrder = new ArrayList<>();
/**
* Test Case Data
*/
String category;
String usage;
String region;
Pair<String, MeasureUnitImpl> inputUnit;
BigDecimal input;
TestCase(String line) {
String[] fields = line
.replaceAll(" ", "") // Remove all the spaces.
.replaceAll(",", "") // Remove all the commas.
.replaceAll("\t", "")
.split(";");
String category = fields[0];
String usage = fields[1];
String region = fields[2];
String inputValue = fields[4];
String inputUnit = fields[5];
ArrayList<Pair<String, String>> outputs = new ArrayList<>();
for (int i = 6; i < fields.length - 2; i += 2) {
if (i == fields.length - 3) { // last field
outputs.add(Pair.of(fields[i + 2], fields[i + 1]));
} else {
outputs.add(Pair.of(fields[i + 1], fields[i]));
}
}
this.insertData(category, usage, region, inputUnit, inputValue, outputs);
}
private void insertData(String category,
String usage,
String region,
String inputUnitString,
String inputValue,
ArrayList<Pair<String, String>> outputs /* Unit Identifier, expected value */) {
this.category = category;
this.usage = usage;
this.region = region;
this.inputUnit = Pair.of(inputUnitString, MeasureUnitImpl.UnitsParser.parseForIdentifier(inputUnitString));
this.input = new BigDecimal(inputValue);
for (Pair<String, String> output :
outputs) {
outputUnitInOrder.add(Pair.of(output.first, MeasureUnitImpl.UnitsParser.parseForIdentifier(output.first)));
expectedInOrder.add(new BigDecimal(output.second));
}
}
}
// Read Test data from the unitPreferencesTest
String codePage = "UTF-8";
BufferedReader f = TestUtil.getDataReader("cldr/units/unitPreferencesTest.txt", codePage);
ArrayList<TestCase> tests = new ArrayList<>();
while (true) {
String line = f.readLine();
if (line == null) break;
if (line.isEmpty() || line.startsWith("#")) continue;
tests.add(new TestCase(line));
}
for (TestCase testCase :
tests) {
UnitsRouter router = new UnitsRouter(testCase.inputUnit.second, testCase.region, testCase.usage);
List<Measure> measures = router.route(testCase.input).measures;
assertEquals("Measures size must be the same as expected units",
measures.size(), testCase.expectedInOrder.size());
assertEquals("Measures size must be the same as output units",
measures.size(), testCase.outputUnitInOrder.size());
for (int i = 0; i < measures.size(); i++) {
if (!UnitsTest
.compareTwoBigDecimal(testCase.expectedInOrder.get(i),
BigDecimal.valueOf(measures.get(i).getNumber().doubleValue()),
BigDecimal.valueOf(0.00001))) {
fail(testCase.toString() + measures.toString());
}
}
}
}
}