MeasureUnitImpl.java

// © 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.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;
import java.util.List;

public class MeasureUnitImpl {

    /** The full unit identifier. Null if not computed. */
    private String identifier = null;

    /** The complexity, either SINGLE, COMPOUND, or MIXED. */
    private MeasureUnit.Complexity complexity = MeasureUnit.Complexity.SINGLE;

    /**
     * The constant denominator.
     *
     * <p>NOTE: when it is 0, it means there is no constant denominator.
     */
    private long constantDenominator = 0;

    /**
     * The list of single units. These may be summed or multiplied, based on the value of the
     * complexity field.
     *
     * <p>The "dimensionless" unit (SingleUnitImpl default constructor) must not be added to this
     * list.
     *
     * <p>The "dimensionless" <code>MeasureUnitImpl</code> has an empty <code>singleUnits</code>.
     */
    private final ArrayList<SingleUnitImpl> singleUnits;

    public MeasureUnitImpl() {
        singleUnits = new ArrayList<>();
    }

    public MeasureUnitImpl(SingleUnitImpl singleUnit) {
        this();
        this.appendSingleUnit(singleUnit);
    }

    /**
     * Parse a unit identifier into a MeasureUnitImpl.
     *
     * @param identifier The unit identifier string.
     * @return A newly parsed object.
     * @throws IllegalArgumentException in case of incorrect/non-parsed identifier.
     */
    public static MeasureUnitImpl forIdentifier(String identifier) {
        return UnitsParser.parseForIdentifier(identifier);
    }

    /** Used for currency units. */
    public static MeasureUnitImpl forCurrencyCode(String currencyCode) {
        MeasureUnitImpl result = new MeasureUnitImpl();
        result.identifier = currencyCode;
        return result;
    }

    public MeasureUnitImpl copy() {
        MeasureUnitImpl result = new MeasureUnitImpl();
        result.complexity = this.complexity;
        result.identifier = this.identifier;
        result.constantDenominator = this.constantDenominator;
        for (SingleUnitImpl singleUnit : this.singleUnits) {
            result.singleUnits.add(singleUnit.copy());
        }
        return result;
    }

    /**
     * Returns a simplified version of the unit. NOTE: the simplification happen when there are two
     * units equals in their base unit and their prefixes.
     *
     * <p>Example 1: "square-meter-per-meter" --> "meter" Example 2: "square-millimeter-per-meter"
     * --> "square-millimeter-per-meter"
     */
    public MeasureUnitImpl copyAndSimplify() {
        MeasureUnitImpl result = new MeasureUnitImpl();
        for (SingleUnitImpl singleUnit : this.getSingleUnits()) {
            // This `for` loop will cause time complexity to be O(n^2).
            // However, n is very small (number of units, generally, at maximum equal to 10)
            boolean unitExist = false;
            for (SingleUnitImpl resultSingleUnit : result.getSingleUnits()) {
                if (resultSingleUnit.getSimpleUnitID().compareTo(singleUnit.getSimpleUnitID()) == 0
                        && resultSingleUnit
                                        .getPrefix()
                                        .getIdentifier()
                                        .compareTo(singleUnit.getPrefix().getIdentifier())
                                == 0) {
                    unitExist = true;
                    resultSingleUnit.setDimensionality(
                            resultSingleUnit.getDimensionality() + singleUnit.getDimensionality());
                    break;
                }
            }

            if (!unitExist) {
                result.appendSingleUnit(singleUnit);
            }
        }

        return result;
    }

    /** Returns the list of simple units. */
    public ArrayList<SingleUnitImpl> getSingleUnits() {
        return singleUnits;
    }

    /** Mutates this MeasureUnitImpl to take the reciprocal. */
    public void takeReciprocal() {
        this.identifier = null;
        for (SingleUnitImpl singleUnit : this.singleUnits) {
            singleUnit.setDimensionality(singleUnit.getDimensionality() * -1);
        }
    }

    public ArrayList<MeasureUnitImplWithIndex> extractIndividualUnitsWithIndices() {
        ArrayList<MeasureUnitImplWithIndex> result = new ArrayList<>();
        if (this.getComplexity() == MeasureUnit.Complexity.MIXED) {
            // In case of mixed units, each single unit can be considered as a stand alone
            // MeasureUnitImpl.
            int i = 0;
            for (SingleUnitImpl singleUnit : this.getSingleUnits()) {
                result.add(new MeasureUnitImplWithIndex(i++, new MeasureUnitImpl(singleUnit)));
            }

            return result;
        }

        result.add(new MeasureUnitImplWithIndex(0, this.copy()));
        return result;
    }

    /**
     * Applies dimensionality to all the internal single units. For example:
     * <b>square-meter-per-second</b>, when we apply dimensionality -2, it will be
     * <b>square-second-per-p4-meter</b>
     */
    public void applyDimensionality(int dimensionality) {
        for (SingleUnitImpl singleUnit : singleUnits) {
            singleUnit.setDimensionality(singleUnit.getDimensionality() * dimensionality);
        }
    }

    /**
     * Mutates this MeasureUnitImpl to append a single unit.
     *
     * @return true if a new item was added. If unit is the dimensionless unit, it is never added:
     *     the return value will always be false.
     */
    public boolean appendSingleUnit(SingleUnitImpl singleUnit) {
        identifier = null;

        if (singleUnit == null) {
            // Do not append dimensionless units.
            return false;
        }

        // Find a similar unit that already exists, to attempt to coalesce
        SingleUnitImpl oldUnit = null;
        for (SingleUnitImpl candidate : this.singleUnits) {
            if (candidate.isCompatibleWith(singleUnit)) {
                oldUnit = candidate;
                break;
            }
        }

        if (oldUnit != null) {
            // Both dimensionalities will be positive, or both will be negative, by
            // virtue of isCompatibleWith().
            oldUnit.setDimensionality(oldUnit.getDimensionality() + singleUnit.getDimensionality());

            return false;
        }

        // Add a copy of singleUnit
        this.singleUnits.add(singleUnit.copy());

        // If the MeasureUnitImpl is `UMEASURE_UNIT_SINGLE` and after the appending a unit, the
        // singleUnits contains
        // more than one. thus means the complexity should be `UMEASURE_UNIT_COMPOUND`
        if (this.singleUnits.size() > 1 && this.complexity == MeasureUnit.Complexity.SINGLE) {
            this.setComplexity(MeasureUnit.Complexity.COMPOUND);
        }

        return true;
    }

    /**
     * Transform this MeasureUnitImpl into a MeasureUnit, simplifying if possible.
     *
     * <p>NOTE: this function must be called from a thread-safe class
     */
    public MeasureUnit build() {
        return MeasureUnit.fromMeasureUnitImpl(this);
    }

    /**
     * @return SingleUnitImpl
     * @throws UnsupportedOperationException if the object could not be converted to SingleUnitImpl.
     */
    public SingleUnitImpl getSingleUnitImpl() {
        if (this.singleUnits.size() == 0) {
            return new SingleUnitImpl();
        }
        if (this.singleUnits.size() == 1) {
            return this.singleUnits.get(0).copy();
        }

        throw new UnsupportedOperationException();
    }

    /** Returns the CLDR unit identifier and null if not computed. */
    public String getIdentifier() {
        return identifier;
    }

    public MeasureUnit.Complexity getComplexity() {
        return complexity;
    }

    public void setComplexity(MeasureUnit.Complexity complexity) {
        this.complexity = complexity;
    }

    /** Get the constant denominator. */
    public long getConstantDenominator() {
        return constantDenominator;
    }

    /** Set the constant denominator. */
    public void setConstantDenominator(long constantDenominator) {
        this.constantDenominator = constantDenominator;
    }

    private int countCharacter(String str, char ch) {
        int count = 0;
        for (int i = 0; i < str.length(); i++) {
            if (str.charAt(i) == ch) {
                count++;
            }
        }
        return count;
    }

    /**
     * Internal function that returns a string of the constants in the correct format.
     *
     * <p>Example: 1000 --> "-per-1000" 1000000 --> "-per-1e6"
     *
     * <p>NOTE: this function is only used when the constant denominator is greater than 0.
     */
    private String getConstantsString(long constantDenominator) {
        assert constantDenominator > 0;
        StringBuilder constantString = new StringBuilder();
        constantString.append(constantDenominator);
        String result = constantString.toString();

        if (constantDenominator <= 1000) {
            return result;
        }

        // Check if the constant denominator is a power of 10
        int zeroCount = countCharacter(result, '0');
        if (zeroCount == result.length() - 1 && result.charAt(0) == '1') {
            return "1e" + zeroCount;
        }

        return result;
    }

    /** Normalizes the MeasureUnitImpl and generates the identifier string in place. */
    public void serialize() {
        if (this.getSingleUnits().size() == 0 && this.constantDenominator == 0) {
            // Dimensionless, constructed by the default constructor: no appending
            // to this.result, we wish it to contain the zero-length string.
            return;
        }

        if (this.complexity == MeasureUnit.Complexity.COMPOUND) {
            // Note: don't sort a MIXED unit
            Collections.sort(this.getSingleUnits(), new SingleUnitComparator());
        }

        StringBuilder result = new StringBuilder();
        boolean beforePer = true;
        boolean firstTimeNegativeDimension = false;
        boolean isConstantDenominatorAdded = false;
        for (SingleUnitImpl singleUnit : this.getSingleUnits()) {
            if (beforePer && singleUnit.getDimensionality() < 0) {
                beforePer = false;
                firstTimeNegativeDimension = true;
            } else if (singleUnit.getDimensionality() < 0) {
                firstTimeNegativeDimension = false;
            }

            if (firstTimeNegativeDimension && this.constantDenominator > 0) {
                result.append("-per-");
                result.append(getConstantsString(this.constantDenominator));
                firstTimeNegativeDimension = false;
                isConstantDenominatorAdded = true;
            }

            if (this.getComplexity() == MeasureUnit.Complexity.MIXED) {
                if (result.length() != 0) {
                    result.append("-and-");
                }
            } else {
                if (firstTimeNegativeDimension) {
                    if (result.length() == 0) {
                        result.append("per-");
                    } else {
                        result.append("-per-");
                    }
                } else {
                    if (result.length() != 0) {
                        result.append("-");
                    }
                }
            }

            result.append(singleUnit.getNeutralIdentifier());
        }

        if (this.constantDenominator > 0 && !isConstantDenominatorAdded) {
            result.append("-per-");
            result.append(getConstantsString(this.constantDenominator));
        }

        this.identifier = result.toString();
    }

    @Override
    public String toString() {
        return "MeasureUnitImpl [" + build().getIdentifier() + "]";
    }

    public enum CompoundPart {
        // Represents "-per-"
        PER(0),
        // Represents "-"
        TIMES(1),
        // Represents "-and-"
        AND(2);

        private final int index;

        CompoundPart(int index) {
            this.index = index;
        }

        public static CompoundPart getCompoundPartFromTrieIndex(int trieIndex) {
            int index = trieIndex - UnitsData.Constants.kCompoundPartOffset;
            switch (index) {
                case 0:
                    return CompoundPart.PER;
                case 1:
                    return CompoundPart.TIMES;
                case 2:
                    return CompoundPart.AND;
                default:
                    throw new AssertionError("CompoundPart index must be 0, 1 or 2");
            }
        }

        public int getTrieIndex() {
            return this.index + UnitsData.Constants.kCompoundPartOffset;
        }

        public int getValue() {
            return index;
        }
    }

    public enum PowerPart {
        P2(2),
        P3(3),
        P4(4),
        P5(5),
        P6(6),
        P7(7),
        P8(8),
        P9(9),
        P10(10),
        P11(11),
        P12(12),
        P13(13),
        P14(14),
        P15(15);

        private final int power;

        PowerPart(int power) {
            this.power = power;
        }

        public static int getPowerFromTrieIndex(int trieIndex) {
            return trieIndex - UnitsData.Constants.kPowerPartOffset;
        }

        public int getTrieIndex() {
            return this.power + UnitsData.Constants.kPowerPartOffset;
        }

        public int getValue() {
            return power;
        }
    }

    public enum InitialCompoundPart {

        // Represents "per-", the only compound part that can appear at the start of
        // an identifier.
        INITIAL_COMPOUND_PART_PER(0);

        private final int index;

        InitialCompoundPart(int powerIndex) {
            this.index = powerIndex;
        }

        public static InitialCompoundPart getInitialCompoundPartFromTrieIndex(int trieIndex) {
            int index = trieIndex - UnitsData.Constants.kInitialCompoundPartOffset;
            if (index == 0) {
                return INITIAL_COMPOUND_PART_PER;
            }

            throw new IllegalArgumentException("Incorrect trieIndex");
        }

        public int getTrieIndex() {
            return this.index + UnitsData.Constants.kInitialCompoundPartOffset;
        }

        public int getValue() {
            return index;
        }
    }

    public static class MeasureUnitImplWithIndex {
        int index;
        MeasureUnitImpl unitImpl;

        MeasureUnitImplWithIndex(int index, MeasureUnitImpl unitImpl) {
            this.index = index;
            this.unitImpl = unitImpl;
        }
    }

    public static class UnitsParser {

        /**
         * Contains a single unit or a constant.
         *
         * @throws IllegalArgumentException when both singleUnit and constant are existing.
         * @param singleUnit the single unit
         * @param constant the constant
         */
        private class SingleUnitOrConstant {
            SingleUnitImpl singleUnit;
            Long constant;

            SingleUnitOrConstant(SingleUnitImpl singleUnit, Long constant) {
                if (singleUnit != null && constant != null) {
                    throw new IllegalArgumentException(
                            "It is a SingleUnit Or a Constant, not both");
                }
                this.singleUnit = singleUnit;
                this.constant = constant;
            }
        }

        // This used only to not build the trie each time we use the parser
        private static volatile CharsTrie savedTrie = null;

        // This trie used in the parsing operation.
        private final CharsTrie trie;

        // The fSource string can be modified if the parser encounters an alias.
        private String fSource;

        // Tracks parser progress: the offset into fSource.
        private int fIndex = 0;
        // Set to true when we've seen a "-per-" or a "per-", after which all units
        // are in the denominator. Until we find an "-and-", at which point the
        // identifier is invalid pending TODO(CLDR-13701).
        private boolean fAfterPer = false;

        // Set to true when we just parsed a "-per-" or a "per-".
        // This is used to ensure that the unit constant (such as "per-100-kilometer")
        // can be parsed when it occurs after a "-per-" or a "per-".
        private boolean fJustAfterPer = false;

        // If an "-and-" was parsed prior to finding the "single
        // * unit", sawAnd is set to true. If not, it is left as is.
        private boolean fSawAnd = false;

        // Cache the MeasurePrefix values array to make getPrefixFromTrieIndex()
        // more efficient
        private static MeasureUnit.MeasurePrefix[] measurePrefixValues =
                MeasureUnit.MeasurePrefix.values();

        private UnitsParser(String identifier) {
            this.fSource = identifier;

            try {
                this.trie = UnitsParser.savedTrie.clone();
            } catch (CloneNotSupportedException e) {
                throw new ICUCloneNotSupportedException();
            }
        }

        static {
            // Build Units trie.
            CharsTrieBuilder trieBuilder;
            trieBuilder = new CharsTrieBuilder();

            // Add SI and binary prefixes
            for (MeasureUnit.MeasurePrefix unitPrefix : measurePrefixValues) {
                trieBuilder.add(unitPrefix.getIdentifier(), getTrieIndexForPrefix(unitPrefix));
            }

            // Add syntax parts (compound, power prefixes)
            trieBuilder.add("-per-", CompoundPart.PER.getTrieIndex());
            trieBuilder.add("-", CompoundPart.TIMES.getTrieIndex());
            trieBuilder.add("-and-", CompoundPart.AND.getTrieIndex());
            trieBuilder.add("per-", InitialCompoundPart.INITIAL_COMPOUND_PART_PER.getTrieIndex());
            trieBuilder.add("square-", PowerPart.P2.getTrieIndex());
            trieBuilder.add("cubic-", PowerPart.P3.getTrieIndex());
            trieBuilder.add("pow2-", PowerPart.P2.getTrieIndex());
            trieBuilder.add("pow3-", PowerPart.P3.getTrieIndex());
            trieBuilder.add("pow4-", PowerPart.P4.getTrieIndex());
            trieBuilder.add("pow5-", PowerPart.P5.getTrieIndex());
            trieBuilder.add("pow6-", PowerPart.P6.getTrieIndex());
            trieBuilder.add("pow7-", PowerPart.P7.getTrieIndex());
            trieBuilder.add("pow8-", PowerPart.P8.getTrieIndex());
            trieBuilder.add("pow9-", PowerPart.P9.getTrieIndex());
            trieBuilder.add("pow10-", PowerPart.P10.getTrieIndex());
            trieBuilder.add("pow11-", PowerPart.P11.getTrieIndex());
            trieBuilder.add("pow12-", PowerPart.P12.getTrieIndex());
            trieBuilder.add("pow13-", PowerPart.P13.getTrieIndex());
            trieBuilder.add("pow14-", PowerPart.P14.getTrieIndex());
            trieBuilder.add("pow15-", PowerPart.P15.getTrieIndex());

            // Add simple units
            String[] simpleUnits = UnitsData.getSimpleUnits();
            for (int i = 0; i < simpleUnits.length; i++) {
                trieBuilder.add(simpleUnits[i], i + UnitsData.Constants.kSimpleUnitOffset);
            }

            // Add aliases
            List<UnitAliases.Alias> aliases = UnitsData.getAliases();
            for (int i = 0; i < aliases.size(); i++) {
                UnitAliases.Alias alias = aliases.get(i);
                trieBuilder.add(alias.alias, i + UnitsData.Constants.kAliasOffset);
            }

            // TODO: Use SLOW or FAST here?
            UnitsParser.savedTrie = trieBuilder.build(StringTrieBuilder.Option.FAST);
        }

        /**
         * Construct a MeasureUnit from a CLDR Unit Identifier, defined in UTS 35. Validates and
         * canonicalizes the identifier.
         *
         * @return MeasureUnitImpl object or null if the identifier is empty.
         * @throws IllegalArgumentException in case of invalid identifier.
         */
        public static MeasureUnitImpl parseForIdentifier(String identifier) {
            if (identifier == null || identifier.isEmpty()) {
                return null;
            }

            UnitsParser parser = new UnitsParser(identifier);
            return parser.parse();
        }

        private static MeasureUnit.MeasurePrefix getPrefixFromTrieIndex(int trieIndex) {
            return measurePrefixValues[trieIndex - UnitsData.Constants.kPrefixOffset];
        }

        private static int getTrieIndexForPrefix(MeasureUnit.MeasurePrefix prefix) {
            return prefix.ordinal() + UnitsData.Constants.kPrefixOffset;
        }

        private MeasureUnitImpl parse() {
            MeasureUnitImpl result = new MeasureUnitImpl();

            if (fSource.isEmpty()) {
                // The dimensionless unit: nothing to parse. return null.
                return null;
            }

            while (hasNext()) {
                fSawAnd = false;
                SingleUnitOrConstant nextSingleUnitPair = nextSingleUnit();

                if (nextSingleUnitPair.singleUnit == null) {
                    if (result.getConstantDenominator() != 0) {
                        throw new IllegalArgumentException("Can't have multiple unit constants");
                    }
                    result.setConstantDenominator(nextSingleUnitPair.constant);
                    result.setComplexity(MeasureUnit.Complexity.COMPOUND);
                    continue;
                }

                SingleUnitImpl singleUnit = nextSingleUnitPair.singleUnit;

                boolean added = result.appendSingleUnit(singleUnit);
                if (fSawAnd && !added) {
                    throw new IllegalArgumentException(
                            "Two similar units are not allowed in a mixed unit.");
                }

                if ((result.singleUnits.size()) >= 2) {
                    // nextSingleUnit fails appropriately for "per" and "and" in the
                    // same identifier. It doesn't fail for other compound units
                    // (COMPOUND_PART_TIMES). Consequently we take care of that
                    // here.
                    MeasureUnit.Complexity complexity =
                            fSawAnd
                                    ? MeasureUnit.Complexity.MIXED
                                    : MeasureUnit.Complexity.COMPOUND;
                    if (result.getSingleUnits().size() == 2) {
                        // After appending two singleUnits, the complexity will be
                        // MeasureUnit.Complexity.COMPOUND
                        assert result.getComplexity() == MeasureUnit.Complexity.COMPOUND;
                        result.setComplexity(complexity);
                    } else if (result.getComplexity() != complexity) {
                        throw new IllegalArgumentException("Can't have mixed compound units");
                    }
                }
            }

            if (result.getSingleUnits().size() == 0) {
                throw new IllegalArgumentException("Error in parsing a unit identifier.");
            }

            return result;
        }

        /** Token states definitions. */
        enum TokenState {
            // No tokens seen yet (will accept power, SI or binary prefix, or simple unit)
            NO_TOKENS_SEEN,
            // Power token seen (will not accept another power token)
            POWER_TOKEN_SEEN,
            // SI or binary prefix token seen (will not accept a power, or SI or binary prefix
            // token)
            PREFIX_TOKEN_SEEN
        }

        /**
         * Returns the next "single unit" via result.
         *
         * <p>If a "-per-" was parsed, the result will have appropriate negative dimensionality.
         *
         * <p>
         *
         * @throws IllegalArgumentException if we parse both compound units and "-and-", since mixed
         *     compound units are not yet supported - TODO(CLDR-13701).
         */
        private SingleUnitOrConstant nextSingleUnit() {
            SingleUnitImpl result = new SingleUnitImpl();

            TokenState state = TokenState.NO_TOKENS_SEEN;

            boolean atStart = fIndex == 0;
            Token token = nextToken();
            fJustAfterPer = false;

            // Handles the case where the alias replacement begins with "per-".
            // For example:
            //      if the alias is "permeter" and the replacement is "per-meter".
            // NOTE: This case does not currently exist in CLDR, but this code anticipates
            // possible future additions.
            if (token.getType() == Token.Type.TYPE_ALIAS) {
                processAlias(token);
                token = nextToken();
            }

            if (atStart) {
                if (token.getType() == Token.Type.TYPE_UNIT_CONSTANT) {
                    throw new IllegalArgumentException("Unit constant cannot be the first token");
                }
                // Identifiers optionally start with "per-".
                if (token.getType() == Token.Type.TYPE_INITIAL_COMPOUND_PART) {
                    assert token.getInitialCompoundPart()
                            == InitialCompoundPart.INITIAL_COMPOUND_PART_PER;

                    fAfterPer = true;
                    fJustAfterPer = true;
                    result.setDimensionality(-1);

                    token = nextToken();
                }
            } else {
                // All other SingleUnit's are separated from previous SingleUnit's
                // via a compound part:
                if (token.getType() != Token.Type.TYPE_COMPOUND_PART) {
                    throw new IllegalArgumentException("token type must be TYPE_COMPOUND_PART");
                }

                CompoundPart compoundPart =
                        CompoundPart.getCompoundPartFromTrieIndex(token.getMatch());
                switch (compoundPart) {
                    case PER:
                        if (fSawAnd) {
                            throw new IllegalArgumentException(
                                    "Mixed compound units not yet supported");
                            // TODO(CLDR-13701).
                        }

                        fAfterPer = true;
                        fJustAfterPer = true;
                        result.setDimensionality(-1);
                        break;

                    case TIMES:
                        if (fAfterPer) {
                            result.setDimensionality(-1);
                        }
                        break;

                    case AND:
                        if (fAfterPer) {
                            // not yet supported, TODO(CLDR-13701).
                            throw new IllegalArgumentException(
                                    "Can't start with \"-and-\", and mixed compound units");
                        }
                        fSawAnd = true;
                        break;
                }

                token = nextToken();
            }

            // Handle unit constant
            if (token.getType() == Token.Type.TYPE_UNIT_CONSTANT) {
                if (!fJustAfterPer) {
                    throw new IllegalArgumentException("Unit constant cannot be the first token");
                }

                return new SingleUnitOrConstant(null, token.getConstantDenominator());
            }

            // Read tokens until we have a complete SingleUnit or we reach the end.
            while (true) {
                switch (token.getType()) {
                    case TYPE_POWER_PART:
                        if (state != TokenState.NO_TOKENS_SEEN) {
                            throw new IllegalArgumentException();
                        }

                        result.setDimensionality(result.getDimensionality() * token.getPower());
                        state = TokenState.POWER_TOKEN_SEEN;
                        break;

                    case TYPE_PREFIX:
                        if (state == TokenState.PREFIX_TOKEN_SEEN) {
                            throw new IllegalArgumentException();
                        }

                        result.setPrefix(token.getPrefix());
                        state = TokenState.PREFIX_TOKEN_SEEN;
                        break;

                    case TYPE_SIMPLE_UNIT:
                        result.setSimpleUnit(
                                token.getSimpleUnitIndex(), UnitsData.getSimpleUnits());
                        return new SingleUnitOrConstant(result, null);

                    case TYPE_ALIAS:
                        processAlias(token);
                        break;

                    default:
                        throw new IllegalArgumentException();
                }

                if (!hasNext()) {
                    throw new IllegalArgumentException(
                            "We ran out of tokens before finding a complete single unit.");
                }

                token = nextToken();
            }
        }

        private boolean hasNext() {
            return fIndex < fSource.length();
        }

        private Token nextToken() {
            trie.reset();
            int matchingValue = -1;
            // Saves the position in the `fSource` string at the end of the most
            // recently matched token.
            int prevIndex = -1;

            int savedIndex = fIndex;

            // Find the longest token that matches a value in the trie:
            while (fIndex < fSource.length()) {
                BytesTrie.Result result = trie.next(fSource.charAt(fIndex++));
                if (result == BytesTrie.Result.NO_MATCH) {
                    break;
                }

                if (result == BytesTrie.Result.NO_VALUE) {
                    continue;
                }

                matchingValue = trie.getValue();
                prevIndex = fIndex;

                if (result == BytesTrie.Result.FINAL_VALUE) {
                    break;
                }

                if (result != BytesTrie.Result.INTERMEDIATE_VALUE) {
                    throw new IllegalArgumentException("Result must have an intermediate value");
                }
            }

            if (matchingValue < 0) {
                if (fJustAfterPer) {
                    // We've just parsed a "per-", so we can expect a unit constant.
                    int hyphenIndex = fSource.indexOf('-', savedIndex);

                    // extract the unit constant from the string
                    String unitConstant =
                            (hyphenIndex == -1)
                                    ? fSource.substring(savedIndex)
                                    : fSource.substring(savedIndex, hyphenIndex);
                    fIndex = (hyphenIndex == -1) ? fSource.length() : hyphenIndex;

                    return Token.tokenWithConstant(unitConstant);

                } else {
                    throw new IllegalArgumentException(
                            "Encountered unknown token starting at index " + prevIndex);
                }
            } else {
                fIndex = prevIndex;
            }

            return new Token(matchingValue);
        }

        static class Token {

            private final long fMatch;
            private final Type type;

            public Token(long fMatch) {
                this.fMatch = fMatch;
                type = calculateType(fMatch);
            }

            private Token(long fMatch, Type type) {
                this.fMatch = fMatch;
                this.type = type;
            }

            public static Token tokenWithConstant(String constantStr) {
                BigDecimal unitConstantValue = new BigDecimal(constantStr);
                if (unitConstantValue.scale() <= 0
                        && unitConstantValue.compareTo(BigDecimal.ZERO) >= 0
                        && unitConstantValue.compareTo(BigDecimal.valueOf(Long.MAX_VALUE)) <= 0) {
                    return new Token(unitConstantValue.longValueExact(), Type.TYPE_UNIT_CONSTANT);
                } else {
                    throw new IllegalArgumentException(
                            "The unit constant value is not a valid non-negative long integer.");
                }
            }

            public Type getType() {
                return this.type;
            }

            public MeasureUnit.MeasurePrefix getPrefix() {
                assert this.type == Type.TYPE_PREFIX;
                assert this.fMatch <= Integer.MAX_VALUE;

                int trieIndex = (int) this.fMatch;
                return getPrefixFromTrieIndex(trieIndex);
            }

            // Valid only for tokens with type TYPE_UNIT_CONSTANT.
            public long getConstantDenominator() {
                assert this.type == Type.TYPE_UNIT_CONSTANT;
                return this.fMatch;
            }

            // Valid only for tokens with type TYPE_COMPOUND_PART.
            public int getMatch() {
                assert getType() == Type.TYPE_COMPOUND_PART;
                assert this.fMatch <= Integer.MAX_VALUE;

                int matchIndex = (int) this.fMatch;
                return matchIndex;
            }

            public int getAliasIndex() {
                assert this.type == Type.TYPE_ALIAS;
                return (int) this.fMatch - UnitsData.Constants.kAliasOffset;
            }

            // Even if there is only one InitialCompoundPart value, we have this
            // function for the simplicity of code consistency.
            public InitialCompoundPart getInitialCompoundPart() {
                assert this.type == Type.TYPE_INITIAL_COMPOUND_PART;
                assert fMatch == InitialCompoundPart.INITIAL_COMPOUND_PART_PER.getTrieIndex();
                assert fMatch <= Integer.MAX_VALUE;
                int trieIndex = (int) fMatch;
                return InitialCompoundPart.getInitialCompoundPartFromTrieIndex(trieIndex);
            }

            public int getPower() {
                assert this.type == Type.TYPE_POWER_PART;
                assert this.fMatch <= Integer.MAX_VALUE;
                int trieIndex = (int) this.fMatch;
                return PowerPart.getPowerFromTrieIndex(trieIndex);
            }

            public int getSimpleUnitIndex() {
                assert this.type == Type.TYPE_SIMPLE_UNIT;
                assert this.fMatch <= Integer.MAX_VALUE;
                return ((int) this.fMatch) - UnitsData.Constants.kSimpleUnitOffset;
            }

            // It is invalid to call calculateType() with a non-positive Token value,
            // as it will result in an assertion failure.
            private Type calculateType(long fMatch) {
                if (fMatch <= 0) {
                    throw new AssertionError("fMatch must have a positive value");
                }
                if (fMatch < UnitsData.Constants.kCompoundPartOffset) {
                    return Type.TYPE_PREFIX;
                }
                if (fMatch < UnitsData.Constants.kInitialCompoundPartOffset) {
                    return Type.TYPE_COMPOUND_PART;
                }
                if (fMatch < UnitsData.Constants.kPowerPartOffset) {
                    return Type.TYPE_INITIAL_COMPOUND_PART;
                }
                if (fMatch < UnitsData.Constants.kSimpleUnitOffset) {
                    return Type.TYPE_POWER_PART;
                }
                if (fMatch < UnitsData.Constants.kAliasOffset) {
                    return Type.TYPE_SIMPLE_UNIT;
                }

                return Type.TYPE_ALIAS;
            }

            enum Type {
                TYPE_UNDEFINED,
                TYPE_PREFIX,
                // Token type for "-per-", "-", and "-and-".
                TYPE_COMPOUND_PART,
                // Token type for "per-".
                TYPE_INITIAL_COMPOUND_PART,
                TYPE_POWER_PART,
                TYPE_SIMPLE_UNIT,
                TYPE_UNIT_CONSTANT,
                TYPE_ALIAS,
            }
        }

        /**
         * Helper function to process alias replacement.
         *
         * @param token The token of TYPE_ALIAS to process
         */
        private void processAlias(Token token) {
            this.fSource =
                    UnitsData.getReplacementFromAliasIndex(token.getAliasIndex())
                            + this.fSource.substring(fIndex);
            this.fIndex = 0;
        }
    }

    static class MeasureUnitImplComparator implements Comparator<MeasureUnitImpl> {
        private final ConversionRates conversionRates;

        public MeasureUnitImplComparator(ConversionRates conversionRates) {
            this.conversionRates = conversionRates;
        }

        @Override
        public int compare(MeasureUnitImpl o1, MeasureUnitImpl o2) {
            String special1 = this.conversionRates.getSpecialMappingName(o1);
            String special2 = this.conversionRates.getSpecialMappingName(o2);
            if (special1 != null || special2 != null) {
                if (special1 == null) {
                    // non-specials come first
                    return -1;
                }
                if (special2 == null) {
                    // non-specials come first
                    return 1;
                }
                // both are specials, compare lexicographically
                return special1.compareTo(special2);
            }
            BigDecimal factor1 = this.conversionRates.getFactorToBase(o1).getConversionRate();
            BigDecimal factor2 = this.conversionRates.getFactorToBase(o2).getConversionRate();

            return factor1.compareTo(factor2);
        }
    }

    static class MeasureUnitImplWithIndexComparator
            implements Comparator<MeasureUnitImplWithIndex> {
        private MeasureUnitImplComparator measureUnitImplComparator;

        public MeasureUnitImplWithIndexComparator(ConversionRates conversionRates) {
            this.measureUnitImplComparator = new MeasureUnitImplComparator(conversionRates);
        }

        @Override
        public int compare(MeasureUnitImplWithIndex o1, MeasureUnitImplWithIndex o2) {
            return this.measureUnitImplComparator.compare(o1.unitImpl, o2.unitImpl);
        }
    }

    static class SingleUnitComparator implements Comparator<SingleUnitImpl> {
        @Override
        public int compare(SingleUnitImpl o1, SingleUnitImpl o2) {
            return o1.compareTo(o2);
        }
    }
}