summaryrefslogtreecommitdiff
path: root/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/UnitsConverter.java
diff options
context:
space:
mode:
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.java484
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 + "]";
+ }
+}