ICU-20568 Implementation of UnitConverter, ComplexUnitConverter and UnitsRouter

See #1279
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/ComplexUnitsConverter.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/ComplexUnitsConverter.java
new file mode 100644
index 0000000..aec0e5f
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/ComplexUnitsConverter.java
@@ -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;
+    }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/ConversionRates.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/ConversionRates.java
new file mode 100644
index 0000000..eec294c
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/ConversionRates.java
@@ -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;
+        }
+    }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/MeasureUnitImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/MeasureUnitImpl.java
index db98db0..ac8b1ed 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/MeasureUnitImpl.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/MeasureUnitImpl.java
@@ -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;
@@ -115,6 +121,17 @@
     }
 
     /**
+     * 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.
      *
      * @return true if a new item was added. If unit is the dimensionless unit,
@@ -728,10 +745,24 @@
         }
     }
 
-    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);
         }
     }
-}
+}
\ No newline at end of file
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/SingleUnitImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/SingleUnitImpl.java
index 52595c0..1bec1c7 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/SingleUnitImpl.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/SingleUnitImpl.java
@@ -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;
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitConverter.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitConverter.java
new file mode 100644
index 0000000..9ac7e1c
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitConverter.java
@@ -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);
+            }
+        }
+    }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitPreferences.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitPreferences.java
new file mode 100644
index 0000000..ac5d9a7
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitPreferences.java
@@ -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);
+        }
+    }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsData.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsData.java
index 87b4e20..7df22e8 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsData.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsData.java
@@ -7,15 +7,29 @@
 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 @@
         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 @@
         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;
+        }
+    }
 }
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsRouter.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsRouter.java
new file mode 100644
index 0000000..19acafc
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/units/UnitsRouter.java
@@ -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;
+        }
+    }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java b/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java
index 32c05c4..3e81417 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java
@@ -32,7 +32,6 @@
 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.
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/impl/UnitsTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/impl/UnitsTest.java
new file mode 100644
index 0000000..3ace927
--- /dev/null
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/impl/UnitsTest.java
@@ -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());
+                }
+            }
+        }
+    }
+}