diff options
Diffstat (limited to 'icu4j/main/core/src/main/java/com/ibm/icu/impl/units/UnitsConverter.java')
-rw-r--r-- | icu4j/main/core/src/main/java/com/ibm/icu/impl/units/UnitsConverter.java | 484 |
1 files changed, 484 insertions, 0 deletions
diff --git a/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/UnitsConverter.java b/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/UnitsConverter.java new file mode 100644 index 000000000..da4a0b2ae --- /dev/null +++ b/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/UnitsConverter.java @@ -0,0 +1,484 @@ +// © 2020 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html +package com.ibm.icu.impl.units; + +import static java.math.MathContext.DECIMAL128; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.regex.Pattern; + +import com.ibm.icu.impl.IllegalIcuArgumentException; +import com.ibm.icu.util.MeasureUnit; + +public class UnitsConverter { + private BigDecimal conversionRate; + private boolean reciprocal; + private BigDecimal offset; + + /** + * Constructor of <code>UnitsConverter</code>. + * NOTE: + * - source and target must be under the same category + * - e.g. meter to mile --> both of them are length units. + * <p> + * NOTE: + * This constructor creates an instance of <code>UnitsConverter</code> internally. + * + * @param sourceIdentifier represents the source unit identifier. + * @param targetIdentifier represents the target unit identifier. + */ + public UnitsConverter(String sourceIdentifier, String targetIdentifier) { + this( + MeasureUnitImpl.forIdentifier(sourceIdentifier), + MeasureUnitImpl.forIdentifier(targetIdentifier), + new ConversionRates() + ); + } + + /** + * Constructor of <code>UnitsConverter</code>. + * 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 UnitsConverter(MeasureUnitImpl source, MeasureUnitImpl target, ConversionRates conversionRates) { + Convertibility convertibility = extractConvertibility(source, target, conversionRates); + if (convertibility != Convertibility.CONVERTIBLE && convertibility != Convertibility.RECIPROCAL) { + throw new IllegalIcuArgumentException("input units must be convertible or reciprocal"); + } + + Factor sourceToBase = conversionRates.getFactorToBase(source); + Factor targetToBase = conversionRates.getFactorToBase(target); + + if (convertibility == Convertibility.CONVERTIBLE) { + this.conversionRate = sourceToBase.divide(targetToBase).getConversionRate(); + } else { + assert convertibility == Convertibility.RECIPROCAL; + this.conversionRate = sourceToBase.multiply(targetToBase).getConversionRate(); + } + this.reciprocal = convertibility == Convertibility.RECIPROCAL; + + // calculate the offset + this.offset = conversionRates.getOffset(source, target, sourceToBase, targetToBase, convertibility); + // We should see no offsets for reciprocal conversions - they don't make sense: + assert convertibility != Convertibility.RECIPROCAL || this.offset == BigDecimal.ZERO; + } + + 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.getSimpleUnitID())) { + dimensionMap.put(singleUnit.getSimpleUnitID(), dimensionMap.get(singleUnit.getSimpleUnitID()) + singleUnit.getDimensionality() * multiplier); + } else { + dimensionMap.put(singleUnit.getSimpleUnitID(), 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) { + BigDecimal result = inputValue.multiply(this.conversionRate).add(offset); + if (this.reciprocal) { + // We should see no offsets for reciprocal conversions - they don't make sense: + assert offset == BigDecimal.ZERO; + if (result.compareTo(BigDecimal.ZERO) == 0) { + // TODO(ICU-21988): determine desirable behaviour + return BigDecimal.ZERO; + } + result = BigDecimal.ONE.divide(result, DECIMAL128); + } + return result; + } + + public BigDecimal convertInverse(BigDecimal inputValue) { + BigDecimal result = inputValue; + if (this.reciprocal) { + // We should see no offsets for reciprocal conversions - they don't make sense: + assert offset == BigDecimal.ZERO; + if (result.compareTo(BigDecimal.ZERO) == 0) { + // TODO(ICU-21988): determine desirable behaviour + return BigDecimal.ZERO; + } + result = BigDecimal.ONE.divide(result, DECIMAL128); + } + result = result.subtract(offset).divide(this.conversionRate, DECIMAL128); + return result; + } + + public enum Convertibility { + CONVERTIBLE, + RECIPROCAL, + UNCONVERTIBLE, + } + + public ConversionInfo getConversionInfo() { + ConversionInfo result = new ConversionInfo(); + result.conversionRate = this.conversionRate; + result.offset = this.offset; + result.reciprocal = this.reciprocal; + + return result; + } + + public static class ConversionInfo { + public BigDecimal conversionRate; + public BigDecimal offset; + public boolean reciprocal; + } + + /** + * Responsible for all the Factor operation + * NOTE: + * This class is immutable + */ + static class Factor { + private BigDecimal factorNum; + private BigDecimal factorDen; + + // The exponents below correspond to ICU4C's Factor::exponents[]. + + /** Exponent for the ft_to_m constant */ + private int exponentFtToM = 0; + /** Exponent for PI */ + private int exponentPi = 0; + /** Exponent for gravity (gravity-of-earth, "g") */ + private int exponentGravity = 0; + /** Exponent for Newtonian constant of gravitation "G". */ + private int exponentG = 0; + /** Exponent for the imperial-gallon to cubic-meter conversion rate constant */ + private int exponentGalImpToM3 = 0; + /** Exponent for the pound to kilogram conversion rate constant */ + private int exponentLbToKg = 0; + /** Exponent for the glucose molar mass conversion rate constant */ + private int exponentGlucoseMolarMass = 0; + /** Exponent for the item per mole conversion rate constant */ + private int exponentItemPerMole = 0; + /** Exponent for the meters per AU conversion rate constant */ + private int exponentMetersPerAU = 0; + /** Exponent for the sec per julian year conversion rate constant */ + private int exponentSecPerJulianYear = 0; + /** Exponent for the speed of light meters per second" conversion rate constant */ + private int exponentSpeedOfLightMetersPerSecond = 0; + /** Exponent for https://en.wikipedia.org/wiki/Japanese_units_of_measurement */ + private int exponentShoToM3 = 0; + /** Exponent for https://en.wikipedia.org/wiki/Japanese_units_of_measurement */ + private int exponentTsuboToM2 = 0; + /** Exponent for https://en.wikipedia.org/wiki/Japanese_units_of_measurement */ + private int exponentShakuToM = 0; + /** Exponent for Atomic Mass Unit */ + private int exponentAMU = 0; + + /** + * 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 = 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; + } + + /** + * Copy this <code>Factor</code>. + */ + protected Factor copy() { + Factor result = new Factor(); + result.factorNum = this.factorNum; + result.factorDen = this.factorDen; + + result.exponentFtToM = this.exponentFtToM; + result.exponentPi = this.exponentPi; + result.exponentGravity = this.exponentGravity; + result.exponentG = this.exponentG; + result.exponentGalImpToM3 = this.exponentGalImpToM3; + result.exponentLbToKg = this.exponentLbToKg; + result.exponentGlucoseMolarMass = this.exponentGlucoseMolarMass; + result.exponentItemPerMole = this.exponentItemPerMole; + result.exponentMetersPerAU = this.exponentMetersPerAU; + result.exponentSecPerJulianYear = this.exponentSecPerJulianYear; + result.exponentSpeedOfLightMetersPerSecond = this.exponentSpeedOfLightMetersPerSecond; + result.exponentShoToM3 = this.exponentShoToM3; + result.exponentTsuboToM2 = this.exponentTsuboToM2; + result.exponentShakuToM = this.exponentShakuToM; + result.exponentAMU = this.exponentAMU; + + return result; + } + + /** + * Returns a single {@code BigDecimal} that represent the conversion rate after substituting all the constants. + * + * In ICU4C, see Factor::substituteConstants(). + */ + public BigDecimal getConversionRate() { + // TODO: this copies all the exponents then doesn't use them at all. + Factor resultCollector = this.copy(); + + // TODO(icu-units#92): port C++ unit tests to Java. + // These values are a hard-coded subset of unitConstants in the + // units resources file. A unit test should check that all constants + // in the resource file are at least recognised by the code. + // In ICU4C, these constants live in constantsValues[]. + resultCollector.multiply(new BigDecimal("0.3048"), this.exponentFtToM); + // TODO: this recalculates this division every time this is called. + resultCollector.multiply(new BigDecimal("411557987.0").divide(new BigDecimal("131002976.0"), DECIMAL128), this.exponentPi); + resultCollector.multiply(new BigDecimal("9.80665"), this.exponentGravity); + resultCollector.multiply(new BigDecimal("6.67408E-11"), this.exponentG); + resultCollector.multiply(new BigDecimal("0.00454609"), this.exponentGalImpToM3); + resultCollector.multiply(new BigDecimal("0.45359237"), this.exponentLbToKg); + resultCollector.multiply(new BigDecimal("180.1557"), this.exponentGlucoseMolarMass); + resultCollector.multiply(new BigDecimal("6.02214076E+23"), this.exponentItemPerMole); + resultCollector.multiply(new BigDecimal("149597870700"), this.exponentMetersPerAU); + resultCollector.multiply(new BigDecimal("31557600"), this.exponentSecPerJulianYear); + resultCollector.multiply(new BigDecimal("299792458"), this.exponentSpeedOfLightMetersPerSecond); + resultCollector.multiply(new BigDecimal("0.001803906836964688204"), this.exponentShoToM3); // 2401/(1331*1000) + resultCollector.multiply(new BigDecimal("3.305785123966942"), this.exponentTsuboToM2); // 400/121 + resultCollector.multiply(new BigDecimal("0.033057851239669"), this.exponentShakuToM); // 4/121 + resultCollector.multiply(new BigDecimal("1.66053878283E-27"), this.exponentAMU); + + return resultCollector.factorNum.divide(resultCollector.factorDen, DECIMAL128); + } + + /** Multiplies the Factor instance by value^power. */ + private void multiply(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); + } + } + + /** Apply SI or binary prefix to the Factor. */ + public Factor applyPrefix(MeasureUnit.MeasurePrefix unitPrefix) { + Factor result = this.copy(); + if (unitPrefix == MeasureUnit.MeasurePrefix.ONE) { + return result; + } + + int base = unitPrefix.getBase(); + int power = unitPrefix.getPower(); + BigDecimal absFactor = + BigDecimal.valueOf(base).pow(Math.abs(power), DECIMAL128); + + if (power < 0) { + result.factorDen = this.factorDen.multiply(absFactor); + return result; + } + + result.factorNum = this.factorNum.multiply(absFactor); + 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.exponentFtToM = this.exponentFtToM * power; + result.exponentPi = this.exponentPi * power; + result.exponentGravity = this.exponentGravity * power; + result.exponentG = this.exponentG * power; + result.exponentGalImpToM3 = this.exponentGalImpToM3 * power; + result.exponentLbToKg = this.exponentLbToKg * power; + result.exponentGlucoseMolarMass = this.exponentGlucoseMolarMass * power; + result.exponentItemPerMole = this.exponentItemPerMole * power; + result.exponentMetersPerAU = this.exponentMetersPerAU * power; + result.exponentSecPerJulianYear = this.exponentSecPerJulianYear * power; + result.exponentSpeedOfLightMetersPerSecond = + this.exponentSpeedOfLightMetersPerSecond * power; + result.exponentShoToM3 = this.exponentShoToM3 * power; + result.exponentTsuboToM2 = this.exponentTsuboToM2 * power; + result.exponentShakuToM = this.exponentShakuToM * power; + result.exponentAMU = this.exponentAMU * 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.exponentFtToM = this.exponentFtToM - other.exponentFtToM; + result.exponentPi = this.exponentPi - other.exponentPi; + result.exponentGravity = this.exponentGravity - other.exponentGravity; + result.exponentG = this.exponentG - other.exponentG; + result.exponentGalImpToM3 = this.exponentGalImpToM3 - other.exponentGalImpToM3; + result.exponentLbToKg = this.exponentLbToKg - other.exponentLbToKg; + result.exponentGlucoseMolarMass = + this.exponentGlucoseMolarMass - other.exponentGlucoseMolarMass; + result.exponentItemPerMole = this.exponentItemPerMole - other.exponentItemPerMole; + result.exponentMetersPerAU = this.exponentMetersPerAU - other.exponentMetersPerAU; + result.exponentSecPerJulianYear = this.exponentSecPerJulianYear - other.exponentSecPerJulianYear; + result.exponentSpeedOfLightMetersPerSecond = + this.exponentSpeedOfLightMetersPerSecond - other.exponentSpeedOfLightMetersPerSecond; + result.exponentShoToM3 = this.exponentShoToM3 - other.exponentShoToM3; + result.exponentTsuboToM2 = this.exponentTsuboToM2 - other.exponentTsuboToM2; + result.exponentShakuToM = this.exponentShakuToM - other.exponentShakuToM; + result.exponentAMU = this.exponentAMU - other.exponentAMU; + + 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.exponentFtToM = this.exponentFtToM + other.exponentFtToM; + result.exponentPi = this.exponentPi + other.exponentPi; + result.exponentGravity = this.exponentGravity + other.exponentGravity; + result.exponentG = this.exponentG + other.exponentG; + result.exponentGalImpToM3 = this.exponentGalImpToM3 + other.exponentGalImpToM3; + result.exponentLbToKg = this.exponentLbToKg + other.exponentLbToKg; + result.exponentGlucoseMolarMass = + this.exponentGlucoseMolarMass + other.exponentGlucoseMolarMass; + result.exponentItemPerMole = this.exponentItemPerMole + other.exponentItemPerMole; + result.exponentMetersPerAU = this.exponentMetersPerAU + other.exponentMetersPerAU; + result.exponentSecPerJulianYear = this.exponentSecPerJulianYear + other.exponentSecPerJulianYear; + result.exponentSpeedOfLightMetersPerSecond = + this.exponentSpeedOfLightMetersPerSecond + other.exponentSpeedOfLightMetersPerSecond; + result.exponentShoToM3 = this.exponentShoToM3 + other.exponentShoToM3; + result.exponentTsuboToM2 = this.exponentTsuboToM2 + other.exponentTsuboToM2; + result.exponentShakuToM = this.exponentShakuToM + other.exponentShakuToM; + result.exponentAMU = this.exponentAMU + other.exponentAMU; + + return result; + } + + /** + * Adds Entity with power or not. For example, {@code 12 ^ 3} or {@code 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.exponentFtToM += power; + } else if ("ft2_to_m2".equals(entity)) { + this.exponentFtToM += 2 * power; + } else if ("ft3_to_m3".equals(entity)) { + this.exponentFtToM += 3 * power; + } else if ("in3_to_m3".equals(entity)) { + this.exponentFtToM += 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.exponentFtToM += 3 * power; + this.factorDen = this.factorDen.multiply(BigDecimal.valueOf(12 * 12 * 12)); + } else if ("gal_imp_to_m3".equals(entity)) { + this.exponentGalImpToM3 += power; + } else if ("G".equals(entity)) { + this.exponentG += power; + } else if ("gravity".equals(entity)) { + this.exponentGravity += power; + } else if ("lb_to_kg".equals(entity)) { + this.exponentLbToKg += power; + } else if ("glucose_molar_mass".equals(entity)) { + this.exponentGlucoseMolarMass += power; + } else if ("item_per_mole".equals(entity)) { + this.exponentItemPerMole += power; + } else if ("meters_per_AU".equals(entity)) { + this.exponentMetersPerAU += power; + } else if ("PI".equals(entity)) { + this.exponentPi += power; + } else if ("sec_per_julian_year".equals(entity)) { + this.exponentSecPerJulianYear += power; + } else if ("speed_of_light_meters_per_second".equals(entity)) { + this.exponentSpeedOfLightMetersPerSecond += power; + } else if ("sho_to_m3".equals(entity)) { + this.exponentShoToM3 += power; + } else if ("tsubo_to_m2".equals(entity)) { + this.exponentTsuboToM2 += power; + } else if ("shaku_to_m".equals(entity)) { + this.exponentShakuToM += power; + } else if ("AMU".equals(entity)) { + this.exponentAMU += power; + } else { + BigDecimal decimalEntity = new BigDecimal(entity).pow(power, DECIMAL128); + this.factorNum = this.factorNum.multiply(decimalEntity); + } + } + } + + @Override + public String toString() { + return "UnitsConverter [conversionRate=" + conversionRate + ", offset=" + offset + "]"; + } +} |