mirror of
https://github.com/unicode-org/icu.git
synced 2025-04-08 06:53:45 +00:00
ICU-20568 Implementation of UnitConverter, ComplexUnitConverter and UnitsRouter
See #1279
This commit is contained in:
parent
6198151510
commit
24a06cc33b
10 changed files with 1521 additions and 6 deletions
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue