NFSubstitution.java

// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 *******************************************************************************
 * Copyright (C) 1996-2015, International Business Machines Corporation and    *
 * others. All Rights Reserved.                                                *
 *******************************************************************************
 */
package com.ibm.icu.text;

import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD;
import com.ibm.icu.math.BigDecimal;
import java.text.ParsePosition;

// ===================================================================
// NFSubstitution (abstract base class)
// ===================================================================

/**
 * An abstract class defining protocol for substitutions. A substitution is a section of a rule that
 * inserts text into the rule's rule text based on some part of the number being formatted.
 *
 * @author Richard Gillam
 */
abstract class NFSubstitution {
    // -----------------------------------------------------------------------
    // data members
    // -----------------------------------------------------------------------

    /** The substitution's position in the rule text of the rule that owns it */
    final int pos;

    /**
     * The rule set this substitution uses to format its result, or null. (Either this or
     * numberFormat has to be non-null.)
     */
    final NFRuleSet ruleSet;

    /**
     * The DecimalFormat this substitution uses to format its result, or null. (Either this or
     * ruleSet has to be non-null.)
     */
    final DecimalFormat numberFormat;

    // -----------------------------------------------------------------------
    // construction
    // -----------------------------------------------------------------------

    /**
     * Parses the description, creates the right kind of substitution, and initializes it based on
     * the description.
     *
     * @param pos The substitution's position in the rule text of the rule that owns it.
     * @param rule The rule containing this substitution
     * @param rulePredecessor The rule preceding the one that contains this substitution in the rule
     *     set's rule list (this is used only for >>> substitutions).
     * @param ruleSet The rule set containing the rule containing this substitution
     * @param formatter The RuleBasedNumberFormat that ultimately owns this substitution
     * @param description The description to parse to build the substitution (this is just the
     *     substring of the rule's description containing the substitution token itself)
     * @return A new substitution constructed according to the description
     */
    public static NFSubstitution makeSubstitution(
            int pos,
            NFRule rule,
            NFRule rulePredecessor,
            NFRuleSet ruleSet,
            RuleBasedNumberFormat formatter,
            String description) {
        // if the description is empty, return a NullSubstitution
        if (description.length() == 0) {
            return null;
        }

        switch (description.charAt(0)) {
            case '<':
                if (rule.getBaseValue() == NFRule.NEGATIVE_NUMBER_RULE) {
                    // throw an exception if the rule is a negative number rule
                    /// CLOVER:OFF
                    // If you look at the call hierarchy of this method, the rule would
                    // never be directly modified by the user and therefore makes the
                    // following pointless unless the user changes the ruleset.
                    throw new IllegalArgumentException("<< not allowed in negative-number rule");
                    /// CLOVER:ON
                } else if (rule.getBaseValue() == NFRule.IMPROPER_FRACTION_RULE
                        || rule.getBaseValue() == NFRule.PROPER_FRACTION_RULE
                        || rule.getBaseValue() == NFRule.DEFAULT_RULE) {
                    // if the rule is a fraction rule, return an IntegralPartSubstitution
                    return new IntegralPartSubstitution(pos, ruleSet, description);
                } else if (ruleSet.isFractionSet()) {
                    // if the rule set containing the rule is a fraction
                    // rule set, return a NumeratorSubstitution
                    return new NumeratorSubstitution(
                            pos, rule.getBaseValue(), formatter.getDefaultRuleSet(), description);
                } else {
                    // otherwise, return a MultiplierSubstitution
                    return new MultiplierSubstitution(pos, rule, ruleSet, description);
                }

            case '>':
                if (rule.getBaseValue() == NFRule.NEGATIVE_NUMBER_RULE) {
                    // if the rule is a negative-number rule, return
                    // an AbsoluteValueSubstitution
                    return new AbsoluteValueSubstitution(pos, ruleSet, description);
                } else if (rule.getBaseValue() == NFRule.IMPROPER_FRACTION_RULE
                        || rule.getBaseValue() == NFRule.PROPER_FRACTION_RULE
                        || rule.getBaseValue() == NFRule.DEFAULT_RULE) {
                    // if the rule is a fraction rule, return a
                    // FractionalPartSubstitution
                    return new FractionalPartSubstitution(pos, ruleSet, description);
                } else if (ruleSet.isFractionSet()) {
                    // if the rule set owning the rule is a fraction rule set,
                    // throw an exception
                    /// CLOVER:OFF
                    // If you look at the call hierarchy of this method, the rule would
                    // never be directly modified by the user and therefore makes the
                    // following pointless unless the user changes the ruleset.
                    throw new IllegalArgumentException(">> not allowed in fraction rule set");
                    /// CLOVER:ON
                } else {
                    // otherwise, return a ModulusSubstitution
                    return new ModulusSubstitution(
                            pos, rule, rulePredecessor, ruleSet, description);
                }
            case '=':
                return new SameValueSubstitution(pos, ruleSet, description);
            default:
                // and if it's anything else, throw an exception
                /// CLOVER:OFF
                // If you look at the call hierarchy of this method, the rule would
                // never be directly modified by the user and therefore makes the
                // following pointless unless the user changes the ruleset.
                throw new IllegalArgumentException("Illegal substitution character");
                /// CLOVER:ON
        }
    }

    /**
     * Base constructor for substitutions. This constructor sets up the fields which are common to
     * all substitutions.
     *
     * @param pos The substitution's position in the owning rule's rule text
     * @param ruleSet The rule set that owns this substitution
     * @param description The substitution descriptor (i.e., the text inside the token characters)
     */
    NFSubstitution(int pos, NFRuleSet ruleSet, String description) {
        // initialize the substitution's position in its parent rule
        this.pos = pos;
        int descriptionLen = description.length();

        // the description should begin and end with the same character.
        // If it doesn't that's a syntax error.  Otherwise,
        // makeSubstitution() was the only thing that needed to know
        // about these characters, so strip them off
        if (descriptionLen >= 2
                && description.charAt(0) == description.charAt(descriptionLen - 1)) {
            description = description.substring(1, descriptionLen - 1);
        } else if (descriptionLen != 0) {
            throw new IllegalArgumentException("Illegal substitution syntax");
        }

        // if the description was just two paired token characters
        // (i.e., "<<" or ">>"), it uses the rule set it belongs to to
        // format its result
        if (description.length() == 0) {
            this.ruleSet = ruleSet;
            this.numberFormat = null;
        } else if (description.charAt(0) == '%') {
            // if the description contains a rule set name, that's the rule
            // set we use to format the result: get a reference to the
            // names rule set
            this.ruleSet = ruleSet.owner.findRuleSet(description);
            this.numberFormat = null;
        } else if (description.charAt(0) == '#' || description.charAt(0) == '0') {
            // if the description begins with 0 or #, treat it as a
            // DecimalFormat pattern, and initialize a DecimalFormat with
            // that pattern (then set it to use the DecimalFormatSymbols
            // belonging to our formatter)
            this.ruleSet = null;
            this.numberFormat = ruleSet.owner.getDecimalFormat().clone();
            this.numberFormat.applyPattern(description);
        } else if (description.charAt(0) == '>') {
            // if the description is ">>>", this substitution bypasses the
            // usual rule-search process and always uses the rule that precedes
            // it in its own rule set's rule list (this is used for place-value
            // notations: formats where you want to see a particular part of
            // a number even when it's 0)
            this.ruleSet = ruleSet; // was null, thai rules added to control space
            this.numberFormat = null;
        } else {
            // and of the description is none of these things, it's a syntax error
            throw new IllegalArgumentException("Illegal substitution syntax");
        }
    }

    /**
     * Set's the substitution's divisor. Used by NFRule.setBaseValue(). A no-op for all
     * substitutions except multiplier and modulus substitutions.
     *
     * @param radix The radix of the divisor
     * @param exponent The exponent of the divisor
     */
    public void setDivisor(int radix, short exponent) {
        // a no-op for all substitutions except multiplier and modulus substitutions
    }

    // -----------------------------------------------------------------------
    // boilerplate
    // -----------------------------------------------------------------------

    /**
     * Compares two substitutions for equality
     *
     * @param that The substitution to compare this one to
     * @return true if the two substitutions are functionally equivalent
     */
    @Override
    public boolean equals(Object that) {
        // compare class and all of the fields all substitutions have
        // in common
        if (that == null) {
            return false;
        }
        if (this == that) {
            return true;
        }
        if (this.getClass() == that.getClass()) {
            NFSubstitution that2 = (NFSubstitution) that;

            return pos == that2.pos
                    && (ruleSet != null
                            || that2.ruleSet
                                    == null) // can't compare tree structure, no .equals or recurse
                    && (numberFormat == null
                            ? (that2.numberFormat == null)
                            : numberFormat.equals(that2.numberFormat));
        }
        return false;
    }

    @Override
    public int hashCode() {
        assert false : "hashCode not designed";
        return 42;
    }

    /**
     * Returns a textual description of the substitution
     *
     * @return A textual description of the substitution. This might not be identical to the
     *     description it was created from, but it'll produce the same result.
     */
    @Override
    public String toString() {
        // use tokenChar() to get the character at the beginning and
        // end of the substitution token.  In between them will go
        // either the name of the rule set it uses, or the pattern of
        // the DecimalFormat it uses
        if (ruleSet != null) {
            return tokenChar() + ruleSet.getName() + tokenChar();
        } else {
            return tokenChar() + numberFormat.toPattern() + tokenChar();
        }
    }

    // -----------------------------------------------------------------------
    // formatting
    // -----------------------------------------------------------------------

    private static final long MAX_INT64_IN_DOUBLE = 0x1FFFFFFFFFFFFFL;

    /**
     * Performs a mathematical operation on the number, formats it using either ruleSet or
     * decimalFormat, and inserts the result into toInsertInto.
     *
     * @param number The number being formatted.
     * @param toInsertInto The string we insert the result into
     * @param position The position in toInsertInto where the owning rule's rule text begins (this
     *     value is added to this substitution's position to determine exactly where to insert the
     *     new text)
     */
    public void doSubstitution(
            long number, StringBuilder toInsertInto, int position, int recursionCount) {
        if (ruleSet != null) {
            // Perform a transformation on the number that is dependent
            // on the type of substitution this is, then just call its
            // rule set's format() method to format the result
            long numberToFormat = transformNumber(number);

            ruleSet.format(numberToFormat, toInsertInto, position + pos, recursionCount);
        } else {
            if (number <= MAX_INT64_IN_DOUBLE) {
                // or perform the transformation on the number,
                // then use that formatter's format() method
                // to format the result
                toInsertInto.insert(
                        position + pos, numberFormat.format(transformNumber((double) number)));
            } else {
                // We have gone beyond double precision. Something has to give.
                // We're favoring accuracy of the large number over potential rules
                // that round like a CompactDecimalFormat, which is not a common use case.
                //
                // Perform a transformation on the number that is dependent
                // on the type of substitution this is, then just call its
                // rule set's format() method to format the result
                long numberToFormat = transformNumber(number);
                toInsertInto.insert(position + pos, numberFormat.format(numberToFormat));
            }
        }
    }

    /**
     * Performs a mathematical operation on the number, formats it using either ruleSet or
     * decimalFormat, and inserts the result into toInsertInto.
     *
     * @param number The number being formatted.
     * @param toInsertInto The string we insert the result into
     * @param position The position in toInsertInto where the owning rule's rule text begins (this
     *     value is added to this substitution's position to determine exactly where to insert the
     *     new text)
     */
    public void doSubstitution(
            double number, StringBuilder toInsertInto, int position, int recursionCount) {
        // perform a transformation on the number being formatted that
        // is dependent on the type of substitution this is
        double numberToFormat = transformNumber(number);

        if (Double.isInfinite(numberToFormat)) {
            // This is probably a minus rule. Combine it with an infinite rule.
            NFRule infiniteRule = ruleSet.findRule(Double.POSITIVE_INFINITY);
            infiniteRule.doFormat(numberToFormat, toInsertInto, position + pos, recursionCount);
            return;
        }

        // if the result is an integer, from here on out we work in integer
        // space (saving time and memory and preserving accuracy)
        if (numberToFormat == Math.floor(numberToFormat) && ruleSet != null) {
            ruleSet.format((long) numberToFormat, toInsertInto, position + pos, recursionCount);

            // if the result isn't an integer, then call either our rule set's
            // format() method or our DecimalFormat's format() method to
            // format the result
        } else {
            if (ruleSet != null) {
                ruleSet.format(numberToFormat, toInsertInto, position + pos, recursionCount);
            } else {
                toInsertInto.insert(position + this.pos, numberFormat.format(numberToFormat));
            }
        }
    }

    /**
     * Subclasses override this function to perform some kind of mathematical operation on the
     * number. The result of this operation is formatted using the rule set or DecimalFormat that
     * this substitution refers to, and the result is inserted into the result string.
     *
     * @param number The number being formatted
     * @return The result of performing the opreration on the number
     */
    public abstract long transformNumber(long number);

    /**
     * Subclasses override this function to perform some kind of mathematical operation on the
     * number. The result of this operation is formatted using the rule set or DecimalFormat that
     * this substitution refers to, and the result is inserted into the result string.
     *
     * @param number The number being formatted
     * @return The result of performing the opreration on the number
     */
    public abstract double transformNumber(double number);

    // -----------------------------------------------------------------------
    // parsing
    // -----------------------------------------------------------------------

    /**
     * Parses a string using the rule set or DecimalFormat belonging to this substitution. If
     * there's a match, a mathematical operation (the inverse of the one used in formatting) is
     * performed on the result of the parse and the value passed in and returned as the result. The
     * parse position is updated to point to the first unmatched character in the string.
     *
     * @param text The string to parse
     * @param parsePosition On entry, ignored, but assumed to be 0. On exit, this is updated to
     *     point to the first unmatched character (or 0 if the substitution didn't match)
     * @param baseValue A partial parse result that should be combined with the result of this parse
     * @param upperBound When searching the rule set for a rule matching the string passed in, only
     *     rules with base values lower than this are considered
     * @param lenientParse If true and matching against rules fails, the substitution will also try
     *     matching the text against numerals using a default-constructed NumberFormat. If false, no
     *     extra work is done. (This value is false whenever the formatter isn't in lenient-parse
     *     mode, but is also false under some conditions even when the formatter _is_ in
     *     lenient-parse mode.)
     * @return If there's a match, this is the result of composing baseValue with whatever was
     *     returned from matching the characters. This will be either a Long or a Double. If there's
     *     no match this is Long.valueOf(0) (not null), and parsePosition is left unchanged.
     */
    public Number doParse(
            String text,
            ParsePosition parsePosition,
            double baseValue,
            double upperBound,
            boolean lenientParse,
            int nonNumericalExecutedRuleMask,
            int recursionCount) {
        Number tempResult;

        // figure out the highest base value a rule can have and match
        // the text being parsed (this varies according to the type of
        // substitutions: multiplier, modulus, and numerator substitutions
        // restrict the search to rules with base values lower than their
        // own; same-value substitutions leave the upper bound wherever
        // it was, and the others allow any rule to match
        upperBound = calcUpperBound(upperBound);

        // use our rule set to parse the text.  If that fails and
        // lenient parsing is enabled (this is always false if the
        // formatter's lenient-parsing mode is off, but it may also
        // be false even when the formatter's lenient-parse mode is
        // on), then also try parsing the text using a default-
        // constructed NumberFormat
        if (ruleSet != null) {
            tempResult =
                    ruleSet.parse(
                            text,
                            parsePosition,
                            upperBound,
                            nonNumericalExecutedRuleMask,
                            recursionCount);
            if (lenientParse && !ruleSet.isFractionSet() && parsePosition.getIndex() == 0) {
                tempResult = ruleSet.owner.getDecimalFormat().parse(text, parsePosition);
            }

            // ...or use our DecimalFormat to parse the text
        } else {
            tempResult = numberFormat.parse(text, parsePosition);
        }

        // if the parse was successful, we've already advanced the caller's
        // parse position (this is the one function that doesn't have one
        // of its own).  Derive a parse result and return it as a Long,
        // if possible, or a Double
        if (parsePosition.getIndex() != 0) {
            double result = tempResult.doubleValue();

            // composeRuleValue() produces a full parse result from
            // the partial parse result passed to this function from
            // the caller (this is either the owning rule's base value
            // or the partial result obtained from composing the
            // owning rule's base value with its other substitution's
            // parse result) and the partial parse result obtained by
            // matching the substitution (which will be the same value
            // the caller would get by parsing just this part of the
            // text with RuleBasedNumberFormat.parse() ).  How the two
            // values are used to derive the full parse result depends
            // on the types of substitutions: For a regular rule, the
            // ultimate result is its multiplier substitution's result
            // times the rule's divisor (or the rule's base value) plus
            // the modulus substitution's result (which will actually
            // supersede part of the rule's base value).  For a negative-
            // number rule, the result is the negative of its substitution's
            // result.  For a fraction rule, it's the sum of its two
            // substitution results.  For a rule in a fraction rule set,
            // it's the numerator substitution's result divided by
            // the rule's base value.  Results from same-value substitutions
            // propagate back upward, and null substitutions don't affect
            // the result.
            result = composeRuleValue(result, baseValue);
            if (result == (long) result) {
                return (long) result;
            } else {
                return result;
            }

            // if the parse was UNsuccessful, return 0
        } else {
            return tempResult;
        }
    }

    /**
     * Derives a new value from the two values passed in. The two values are typically either the
     * base values of two rules (the one containing the substitution and the one matching the
     * substitution) or partial parse results derived in some other way. The operation is generally
     * the inverse of the operation performed by transformNumber().
     *
     * @param newRuleValue The value produced by matching this substitution
     * @param oldRuleValue The value that was passed to the substitution by the rule that owns it
     * @return A third value derived from the other two, representing a partial parse result
     */
    public abstract double composeRuleValue(double newRuleValue, double oldRuleValue);

    /**
     * Calculates an upper bound when searching for a rule that matches this substitution. Rules
     * with base values greater than or equal to upperBound are not considered.
     *
     * @param oldUpperBound The current upper-bound setting. The new upper bound can't be any
     *     higher.
     */
    public abstract double calcUpperBound(double oldUpperBound);

    // -----------------------------------------------------------------------
    // simple accessors
    // -----------------------------------------------------------------------

    /**
     * Returns the substitution's position in the rule that owns it.
     *
     * @return The substitution's position in the rule that owns it.
     */
    public final int getPos() {
        return pos;
    }

    /**
     * Returns the character used in the textual representation of substitutions of this type. Used
     * by toString().
     *
     * @return This substitution's token character.
     */
    abstract char tokenChar();

    /**
     * Returns true if this is a modulus substitution. (We didn't do this with instanceof partially
     * because it causes source files to proliferate and partially because we have to port this to
     * C++.)
     *
     * @return true if this object is an instance of ModulusSubstitution
     */
    public boolean isModulusSubstitution() {
        return false;
    }

    public void setDecimalFormatSymbols(DecimalFormatSymbols newSymbols) {
        if (numberFormat != null) {
            numberFormat.setDecimalFormatSymbols(newSymbols);
        }
    }
}

// ===================================================================
// SameValueSubstitution
// ===================================================================

/**
 * A substitution that passes the value passed to it through unchanged. Represented by == in rule
 * descriptions.
 */
class SameValueSubstitution extends NFSubstitution {
    // -----------------------------------------------------------------------
    // construction
    // -----------------------------------------------------------------------

    /**
     * Constructs a SameValueSubstution. This function just uses the superclass constructor, but it
     * performs a check that this substitution doesn't call the rule set that owns it, since that
     * would lead to infinite recursion.
     */
    SameValueSubstitution(int pos, NFRuleSet ruleSet, String description) {
        super(pos, ruleSet, description);
        if (description.equals("==")) {
            throw new IllegalArgumentException("== is not a legal token");
        }
    }

    // -----------------------------------------------------------------------
    // formatting
    // -----------------------------------------------------------------------

    /**
     * Returns "number" unchanged.
     *
     * @return "number"
     */
    @Override
    public long transformNumber(long number) {
        return number;
    }

    /**
     * Returns "number" unchanged.
     *
     * @return "number"
     */
    @Override
    public double transformNumber(double number) {
        return number;
    }

    // -----------------------------------------------------------------------
    // parsing
    // -----------------------------------------------------------------------

    /**
     * Returns newRuleValue and ignores oldRuleValue. (The value we got matching the substitution
     * supersedes the value of the rule that owns the substitution.)
     *
     * @param newRuleValue The value resulting from matching the substitution
     * @param oldRuleValue The value of the rule containing the substitution.
     * @return newRuleValue
     */
    @Override
    public double composeRuleValue(double newRuleValue, double oldRuleValue) {
        return newRuleValue;
    }

    /**
     * SameValueSubstitution doesn't change the upper bound.
     *
     * @param oldUpperBound The current upper bound.
     * @return oldUpperBound
     */
    @Override
    public double calcUpperBound(double oldUpperBound) {
        return oldUpperBound;
    }

    // -----------------------------------------------------------------------
    // simple accessor
    // -----------------------------------------------------------------------

    /**
     * The token character for a SameValueSubstitution is =.
     *
     * @return '='
     */
    @Override
    char tokenChar() {
        return '=';
    }
}

// ===================================================================
// MultiplierSubstitution
// ===================================================================

/**
 * A substitution that divides the number being formatted by the rule's divisor and formats the
 * quotient. Represented by &lt;&lt; in normal rules.
 */
class MultiplierSubstitution extends NFSubstitution {
    // -----------------------------------------------------------------------
    // data members
    // -----------------------------------------------------------------------

    /** The divisor of the rule that owns this substitution. */
    long divisor;

    /**
     * A backpointer to the owning rule. Used in the rounding logic to determine whether the owning
     * rule also has a modulus substitution.
     */
    NFRule owningRule;

    // -----------------------------------------------------------------------
    // construction
    // -----------------------------------------------------------------------

    /**
     * Constructs a MultiplierSubstitution. This uses the superclass constructor to initialize most
     * members, but this substitution also maintains its own copy of its rule's divisor.
     *
     * @param pos The substitution's position in its rule's rule text
     * @param rule The rule that owns this substitution
     * @param ruleSet The ruleSet this substitution uses to format its result
     * @param description The description describing this substitution
     */
    MultiplierSubstitution(int pos, NFRule rule, NFRuleSet ruleSet, String description) {
        super(pos, ruleSet, description);

        // the owning rule's divisor affects the behavior of this
        // substitution.  Rather than keeping a back-pointer to the
        // rule, we keep a copy of the divisor
        this.divisor = rule.getDivisor();
        this.owningRule = rule;

        if (divisor == 0) { // this will cause recursion
            throw new IllegalStateException(
                    "Substitution with divisor 0 "
                            + description.substring(0, pos)
                            + " | "
                            + description.substring(pos));
        }
    }

    /**
     * Sets the substitution's divisor based on the values passed in.
     *
     * @param radix The radix of the divisor.
     * @param exponent The exponent of the divisor.
     */
    @Override
    public void setDivisor(int radix, short exponent) {
        divisor = NFRule.power(radix, exponent);

        if (divisor == 0) {
            throw new IllegalStateException("Substitution with divisor 0");
        }
    }

    // -----------------------------------------------------------------------
    // boilerplate
    // -----------------------------------------------------------------------

    /**
     * Augments the superclass's equals() function by comparing divisors.
     *
     * @param that The other substitution
     * @return true if the two substitutions are functionally equal
     */
    @Override
    public boolean equals(Object that) {
        return super.equals(that) && divisor == ((MultiplierSubstitution) that).divisor;
    }

    // -----------------------------------------------------------------------
    // formatting
    // -----------------------------------------------------------------------

    /**
     * Divides the number by the rule's divisor and returns the quotient.
     *
     * @param number The number being formatted.
     * @return "number" divided by the rule's divisor
     */
    @Override
    public long transformNumber(long number) {
        return (long) Math.floor(number / divisor);
    }

    /**
     * Divides the number by the rule's divisor and returns the quotient. This is an integral
     * quotient if we're filling in the substitution using another rule set, but it's the full
     * quotient (integral and fractional parts) if we're filling in the substitution using a
     * DecimalFormat. (This allows things such as "1.2 million".)
     *
     * @param number The number being formatted
     * @return "number" divided by the rule's divisor
     */
    @Override
    public double transformNumber(double number) {
        // Most of the time, when a number is handled by an NFSubstitution, we do a floor() on it,
        // but if a substitution uses a DecimalFormat to format the number instead of a ruleset,
        // we generally don't want to do a floor()-- we want to keep the value intact so that
        // the DecimalFormat can either include the fractional part or round properly.
        // The big exception to this is here in MultiplierSubstitution. If the rule includes two
        // substitutions, the MultiplierSubstitution (which is handling the larger part of the
        // number) really _does_ want to do a floor(), because the ModulusSubstitution (which is
        // handling the smaller part of the number) will take care of the fractional part. Consider
        // something like `1/12: <0< feet >0.0> inches;`. But if there is no ModulusSubstitution,
        // we're shortening the number in some way-- the "larger part" of the number is the only
        // part we're keeping. Even if the DecimalFormat doesn't include the fractional part in
        // its output, we still want it to round.  (Consider something like `1/1000: <0<K;`.)
        // (TODO: The ROUND_FLOOR thing is a kludge to preserve the previous floor-always behavior.
        // What we probably really want to do is just set the rounding mode on the DecimalFormat
        // to match the rounding mode on the RuleBasedNumberFormat and then pass the number to it
        // whole and let it do its own rounding. But before making that change, we'd have to make
        // sure it didn't have undesirable side effects.)
        if (ruleSet != null
                || owningRule.hasModulusSubstitution()
                || owningRule.formatter.getRoundingMode() == BigDecimal.ROUND_FLOOR) {
            return Math.floor(number / divisor);
        } else {
            return number / divisor;
        }
    }

    // -----------------------------------------------------------------------
    // parsing
    // -----------------------------------------------------------------------

    /**
     * Returns newRuleValue times the divisor. Ignores oldRuleValue. (The result of matching a <<
     * substitution supersedes the base value of the rule that contains it.)
     *
     * @param newRuleValue The result of matching the substitution
     * @param oldRuleValue The base value of the rule containing the substitution
     * @return newRuleValue * divisor
     */
    @Override
    public double composeRuleValue(double newRuleValue, double oldRuleValue) {
        return newRuleValue * divisor;
    }

    /**
     * Sets the upper bound down to the rule's divisor.
     *
     * @param oldUpperBound Ignored.
     * @return The rule's divisor.
     */
    @Override
    public double calcUpperBound(double oldUpperBound) {
        return divisor;
    }

    // -----------------------------------------------------------------------
    // simple accessor
    // -----------------------------------------------------------------------

    /**
     * The token character for a multiplier substitution is &lt;.
     *
     * @return '&lt;'
     */
    @Override
    char tokenChar() {
        return '<';
    }
}

// ===================================================================
// ModulusSubstitution
// ===================================================================

/**
 * A substitution that divides the number being formatted by the its rule's divisor and formats the
 * remainder. Represented by "&gt;&gt;" in a regular rule.
 */
class ModulusSubstitution extends NFSubstitution {
    // -----------------------------------------------------------------------
    // data members
    // -----------------------------------------------------------------------

    /** The divisor of the rule owning this substitution */
    long divisor;

    /**
     * If this is a &gt;&gt;&gt; substitution, the rule to use to format the substitution value.
     * Otherwise, null.
     */
    private final NFRule ruleToUse;

    // -----------------------------------------------------------------------
    // construction
    // -----------------------------------------------------------------------

    /**
     * Constructs a ModulusSubstitution. In addition to the inherited members, a ModulusSubstitution
     * keeps track of the divisor of the rule that owns it, and may also keep a reference to the
     * rule that precedes the rule containing this substitution in the rule set's rule list.
     *
     * @param pos The substitution's position in its rule's rule text
     * @param rule The rule that owns this substitution
     * @param rulePredecessor The rule that precedes this substitution's rule in its rule set's rule
     *     list
     * @param description The description for this substitution
     */
    ModulusSubstitution(
            int pos, NFRule rule, NFRule rulePredecessor, NFRuleSet ruleSet, String description) {
        super(pos, ruleSet, description);

        // the owning rule's divisor controls the behavior of this
        // substitution: rather than keeping a backpointer to the rule,
        // we keep a copy of the divisor
        this.divisor = rule.getDivisor();

        if (divisor == 0) { // this will cause recursion
            throw new IllegalStateException(
                    "Substitution with bad divisor ("
                            + divisor
                            + ") "
                            + description.substring(0, pos)
                            + " | "
                            + description.substring(pos));
        }

        // the >>> token doesn't alter how this substitution calculates the
        // values it uses for formatting and parsing, but it changes
        // what's done with that value after it's obtained: >>> short-
        // circuits the rule-search process and goes straight to the
        // specified rule to format the substitution value
        if (description.equals(">>>")) {
            ruleToUse = rulePredecessor;
        } else {
            ruleToUse = null;
        }
    }

    /**
     * Makes the substitution's divisor conform to that of the rule that owns it. Used when the
     * divisor is determined after creation.
     *
     * @param radix The radix of the divisor.
     * @param exponent The exponent of the divisor.
     */
    @Override
    public void setDivisor(int radix, short exponent) {
        divisor = NFRule.power(radix, exponent);

        if (divisor == 0) { // this will cause recursion
            throw new IllegalStateException("Substitution with bad divisor");
        }
    }

    // -----------------------------------------------------------------------
    // boilerplate
    // -----------------------------------------------------------------------

    /**
     * Augments the inherited equals() function by comparing divisors and ruleToUse.
     *
     * @param that The other substitution
     * @return true if the two substitutions are functionally equivalent
     */
    @Override
    public boolean equals(Object that) {
        if (super.equals(that)) {
            ModulusSubstitution that2 = (ModulusSubstitution) that;

            return divisor == that2.divisor;
        } else {
            return false;
        }
    }

    // -----------------------------------------------------------------------
    // formatting
    // -----------------------------------------------------------------------

    /**
     * If this is a &gt;&gt;&gt; substitution, use ruleToUse to fill in the substitution. Otherwise,
     * just use the superclass function.
     *
     * @param number The number being formatted
     * @param toInsertInto The string to insert the result of this substitution into
     * @param position The position of the rule text in toInsertInto
     */
    @Override
    public void doSubstitution(
            long number, StringBuilder toInsertInto, int position, int recursionCount) {
        // if this isn't a >>> substitution, just use the inherited version
        // of this function (which uses either a rule set or a DecimalFormat
        // to format its substitution value)
        if (ruleToUse == null) {
            super.doSubstitution(number, toInsertInto, position, recursionCount);
        } else {
            // a >>> substitution goes straight to a particular rule to
            // format the substitution value
            long numberToFormat = transformNumber(number);
            ruleToUse.doFormat(numberToFormat, toInsertInto, position + pos, recursionCount);
        }
    }

    /**
     * If this is a &gt;&gt;&gt; substitution, use ruleToUse to fill in the substitution. Otherwise,
     * just use the superclass function.
     *
     * @param number The number being formatted
     * @param toInsertInto The string to insert the result of this substitution into
     * @param position The position of the rule text in toInsertInto
     */
    @Override
    public void doSubstitution(
            double number, StringBuilder toInsertInto, int position, int recursionCount) {
        // if this isn't a >>> substitution, just use the inherited version
        // of this function (which uses either a rule set or a DecimalFormat
        // to format its substitution value)
        if (ruleToUse == null) {
            super.doSubstitution(number, toInsertInto, position, recursionCount);

        } else {
            // a >>> substitution goes straight to a particular rule to
            // format the substitution value
            double numberToFormat = transformNumber(number);

            ruleToUse.doFormat(numberToFormat, toInsertInto, position + pos, recursionCount);
        }
    }

    /**
     * Divides the number being formatted by the rule's divisor and returns the remainder.
     *
     * @param number The number being formatted
     * @return "number" mod divisor
     */
    @Override
    public long transformNumber(long number) {
        return number % divisor;
    }

    /**
     * Divides the number being formatted by the rule's divisor and returns the remainder.
     *
     * @param number The number being formatted
     * @return "number" mod divisor
     */
    @Override
    public double transformNumber(double number) {
        return number % divisor;
    }

    // -----------------------------------------------------------------------
    // parsing
    // -----------------------------------------------------------------------

    /**
     * If this is a &gt;&gt;&gt; substitution, match only against ruleToUse. Otherwise, use the
     * superclass function.
     *
     * @param text The string to parse
     * @param parsePosition Ignored on entry, updated on exit to point to the first unmatched
     *     character.
     * @param baseValue The partial parse result prior to calling this routine.
     */
    @Override
    public Number doParse(
            String text,
            ParsePosition parsePosition,
            double baseValue,
            double upperBound,
            boolean lenientParse,
            int nonNumericalExecutedRuleMask,
            int recursionCount) {
        // if this isn't a >>> substitution, we can just use the
        // inherited parse() routine to do the parsing
        if (ruleToUse == null) {
            return super.doParse(
                    text,
                    parsePosition,
                    baseValue,
                    upperBound,
                    lenientParse,
                    nonNumericalExecutedRuleMask,
                    recursionCount);

        } else {
            // but if it IS a >>> substitution, we have to do it here: we
            // use the specific rule's doParse() method, and then we have to
            // do some of the other work of NFRuleSet.parse()
            Number tempResult =
                    ruleToUse.doParse(
                            text,
                            parsePosition,
                            false,
                            upperBound,
                            nonNumericalExecutedRuleMask,
                            recursionCount);

            if (parsePosition.getIndex() != 0) {
                double result = tempResult.doubleValue();

                result = composeRuleValue(result, baseValue);
                if (result == (long) result) {
                    return (long) result;
                } else {
                    return result;
                }
            } else {
                return tempResult;
            }
        }
    }

    /**
     * Returns the highest multiple of the rule's divisor that its less than or equal to
     * oldRuleValue, plus newRuleValue. (The result is the sum of the result of parsing the
     * substitution plus the base value of the rule containing the substitution, but if the owning
     * rule's base value isn't an even multiple of its divisor, we have to round it down to a
     * multiple of the divisor, or we get unwanted digits in the result.)
     *
     * @param newRuleValue The result of parsing the substitution
     * @param oldRuleValue The base value of the rule containing the substitution
     */
    @Override
    public double composeRuleValue(double newRuleValue, double oldRuleValue) {
        return (oldRuleValue - (oldRuleValue % divisor)) + newRuleValue;
    }

    /**
     * Sets the upper bound down to the owning rule's divisor
     *
     * @param oldUpperBound Ignored
     * @return The owning rule's divisor
     */
    @Override
    public double calcUpperBound(double oldUpperBound) {
        return divisor;
    }

    // -----------------------------------------------------------------------
    // simple accessors
    // -----------------------------------------------------------------------

    /**
     * Returns true. This _is_ a ModulusSubstitution.
     *
     * @return true
     */
    @Override
    public boolean isModulusSubstitution() {
        return true;
    }

    /**
     * The token character of a ModulusSubstitution is &gt;.
     *
     * @return '&gt;'
     */
    @Override
    char tokenChar() {
        return '>';
    }
}

// ===================================================================
// IntegralPartSubstitution
// ===================================================================

/**
 * A substitution that formats the number's integral part. This is represented by &lt;&lt; in a
 * fraction rule.
 */
class IntegralPartSubstitution extends NFSubstitution {
    // -----------------------------------------------------------------------
    // construction
    // -----------------------------------------------------------------------

    /** Constructs an IntegralPartSubstitution. This just calls the superclass constructor. */
    IntegralPartSubstitution(int pos, NFRuleSet ruleSet, String description) {
        super(pos, ruleSet, description);
    }

    // -----------------------------------------------------------------------
    // formatting
    // -----------------------------------------------------------------------

    /**
     * Returns the number's integral part. (For a long, that's just the number unchanged.)
     *
     * @param number The number being formatted
     * @return "number" unchanged
     */
    @Override
    public long transformNumber(long number) {
        return number;
    }

    /**
     * Returns the number's integral part.
     *
     * @param number The integral part of the number being formatted
     * @return floor(number)
     */
    @Override
    public double transformNumber(double number) {
        return Math.floor(number);
    }

    // -----------------------------------------------------------------------
    // parsing
    // -----------------------------------------------------------------------

    /**
     * Returns the sum of the result of parsing the substitution and the owning rule's base value.
     * (The owning rule, at best, has an integral-part substitution and a fractional-part
     * substitution, so we can safely just add them.)
     *
     * @param newRuleValue The result of matching the substitution
     * @param oldRuleValue The partial result of the parse prior to calling this function
     * @return oldRuleValue + newRuleValue
     */
    @Override
    public double composeRuleValue(double newRuleValue, double oldRuleValue) {
        return newRuleValue + oldRuleValue;
    }

    /**
     * An IntegralPartSubstitution sets the upper bound back up so all potentially matching rules
     * are considered.
     *
     * @param oldUpperBound Ignored
     * @return Double.MAX_VALUE
     */
    @Override
    public double calcUpperBound(double oldUpperBound) {
        return Double.MAX_VALUE;
    }

    // -----------------------------------------------------------------------
    // simple accessor
    // -----------------------------------------------------------------------

    /**
     * An IntegralPartSubstitution's token character is &lt;
     *
     * @return '&lt;'
     */
    @Override
    char tokenChar() {
        return '<';
    }
}

// ===================================================================
// FractionalPartSubstitution
// ===================================================================

/**
 * A substitution that formats the fractional part of a number. This is represented by &gt;&gt; in a
 * fraction rule.
 */
class FractionalPartSubstitution extends NFSubstitution {
    // -----------------------------------------------------------------------
    // data members
    // -----------------------------------------------------------------------

    /** true if this substitution should have the default "by digits" behavior, false otherwise */
    private final boolean byDigits;

    /**
     * true if we automatically insert spaces to separate names of digits set to false by '>>>' in
     * fraction rules, used by Thai.
     */
    private final boolean useSpaces;

    // -----------------------------------------------------------------------
    // construction
    // -----------------------------------------------------------------------

    /**
     * Constructs a FractionalPartSubstitution. This object keeps a flag telling whether it should
     * format by digits or not. In addition, it marks the rule set it calls (if any) as a fraction
     * rule set.
     */
    FractionalPartSubstitution(int pos, NFRuleSet ruleSet, String description) {
        super(pos, ruleSet, description);
        if (description.equals(">>") || description.equals(">>>") || ruleSet == this.ruleSet) {
            byDigits = true;
            useSpaces = !description.equals(">>>");
        } else {
            byDigits = false;
            useSpaces = true;
            if (this.ruleSet != null) {
                this.ruleSet.makeIntoFractionRuleSet();
            } else {
                throw new IllegalArgumentException("rule set is missing");
            }
        }
    }

    // -----------------------------------------------------------------------
    // formatting
    // -----------------------------------------------------------------------

    /**
     * If in "by digits" mode, fills in the substitution one decimal digit at a time using the rule
     * set containing this substitution. Otherwise, uses the superclass function.
     *
     * @param number The number being formatted
     * @param toInsertInto The string to insert the result of formatting the substitution into
     * @param position The position of the owning rule's rule text in toInsertInto
     */
    @Override
    public void doSubstitution(
            double number, StringBuilder toInsertInto, int position, int recursionCount) {
        if (!byDigits) {
            // if we're not in "byDigits" mode, just use the inherited
            // doSubstitution() routine
            super.doSubstitution(number, toInsertInto, position, recursionCount);
        } else {
            // if we're in "byDigits" mode, transform the value into an integer
            // by moving the decimal point eight places to the right and
            // pulling digits off the right one at a time, formatting each digit
            // as an integer using this substitution's owning rule set
            // (this is slower, but more accurate, than doing it from the
            // other end)

            DecimalQuantity_DualStorageBCD fq = new DecimalQuantity_DualStorageBCD(number);
            fq.roundToInfinity(); // ensure doubles are resolved using slow path

            boolean pad = false;
            int mag = fq.getLowerDisplayMagnitude();
            while (mag < 0) {
                if (pad && useSpaces) {
                    toInsertInto.insert(position + pos, ' ');
                } else {
                    pad = true;
                }
                ruleSet.format(fq.getDigit(mag++), toInsertInto, position + pos, recursionCount);
            }
        }
    }

    /**
     * Returns the fractional part of the number, which will always be zero if it's a long.
     *
     * @param number The number being formatted
     * @return 0
     */
    @Override
    public long transformNumber(long number) {
        return 0;
    }

    /**
     * Returns the fractional part of the number.
     *
     * @param number The number being formatted.
     * @return number - floor(number)
     */
    @Override
    public double transformNumber(double number) {
        return number - Math.floor(number);
    }

    // -----------------------------------------------------------------------
    // parsing
    // -----------------------------------------------------------------------

    /**
     * If in "by digits" mode, parses the string as if it were a string of individual digits;
     * otherwise, uses the superclass function.
     *
     * @param text The string to parse
     * @param parsePosition Ignored on entry, but updated on exit to point to the first unmatched
     *     character
     * @param baseValue The partial parse result prior to entering this function
     * @param upperBound Only consider rules with base values lower than this when filling in the
     *     substitution
     * @param lenientParse If true, try matching the text as numerals if matching as words doesn't
     *     work
     * @return If the match was successful, the current partial parse result; otherwise
     *     Long.valueOf(0). The result is either a Long or a Double.
     */
    @Override
    public Number doParse(
            String text,
            ParsePosition parsePosition,
            double baseValue,
            double upperBound,
            boolean lenientParse,
            int nonNumericalExecutedRuleMask,
            int recursionCount) {
        // if we're not in byDigits mode, we can just use the inherited
        // doParse()
        if (!byDigits) {
            return super.doParse(
                    text,
                    parsePosition,
                    baseValue,
                    0,
                    lenientParse,
                    nonNumericalExecutedRuleMask,
                    recursionCount);
        } else {
            // if we ARE in byDigits mode, parse the text one digit at a time
            // using this substitution's owning rule set (we do this by setting
            // upperBound to 10 when calling doParse() ) until we reach
            // nonmatching text
            String workText = text;
            ParsePosition workPos = new ParsePosition(1);
            double result;
            int digit;

            DecimalQuantity_DualStorageBCD fq = new DecimalQuantity_DualStorageBCD();
            int totalDigits = 0;
            while (workText.length() > 0 && workPos.getIndex() != 0) {
                workPos.setIndex(0);
                digit =
                        ruleSet.parse(
                                        workText,
                                        workPos,
                                        10,
                                        nonNumericalExecutedRuleMask,
                                        recursionCount)
                                .intValue();
                if (lenientParse && workPos.getIndex() == 0) {
                    Number n = ruleSet.owner.getDecimalFormat().parse(workText, workPos);
                    if (n != null) {
                        digit = n.intValue();
                    }
                }

                if (workPos.getIndex() != 0) {
                    fq.appendDigit((byte) digit, 0, true);
                    totalDigits++;

                    parsePosition.setIndex(parsePosition.getIndex() + workPos.getIndex());
                    workText = workText.substring(workPos.getIndex());
                    while (workText.length() > 0 && workText.charAt(0) == ' ') {
                        workText = workText.substring(1);
                        parsePosition.setIndex(parsePosition.getIndex() + 1);
                    }
                }
            }
            fq.adjustMagnitude(-totalDigits);
            result = fq.toDouble();

            result = composeRuleValue(result, baseValue);
            return result;
        }
    }

    /**
     * Returns the sum of the two partial parse results.
     *
     * @param newRuleValue The result of parsing the substitution
     * @param oldRuleValue The partial parse result prior to calling this function
     * @return newRuleValue + oldRuleValue
     */
    @Override
    public double composeRuleValue(double newRuleValue, double oldRuleValue) {
        return newRuleValue + oldRuleValue;
    }

    /** Not used. */
    @Override
    public double calcUpperBound(double oldUpperBound) {
        return 0; // this value is ignored
    }

    // -----------------------------------------------------------------------
    // simple accessor
    // -----------------------------------------------------------------------

    /**
     * The token character for a FractionalPartSubstitution is &gt;.
     *
     * @return '&gt;'
     */
    @Override
    char tokenChar() {
        return '>';
    }
}

// ===================================================================
// AbsoluteValueSubstitution
// ===================================================================

/**
 * A substitution that formats the absolute value of the number. This substitution is represented by
 * &gt;&gt; in a negative-number rule.
 */
class AbsoluteValueSubstitution extends NFSubstitution {
    // -----------------------------------------------------------------------
    // construction
    // -----------------------------------------------------------------------

    /** Constructs an AbsoluteValueSubstitution. This just uses the superclass constructor. */
    AbsoluteValueSubstitution(int pos, NFRuleSet ruleSet, String description) {
        super(pos, ruleSet, description);
    }

    // -----------------------------------------------------------------------
    // formatting
    // -----------------------------------------------------------------------

    /**
     * Returns the absolute value of the number.
     *
     * @param number The number being formatted.
     * @return abs(number)
     */
    @Override
    public long transformNumber(long number) {
        return Math.abs(number);
    }

    /**
     * Returns the absolute value of the number.
     *
     * @param number The number being formatted.
     * @return abs(number)
     */
    @Override
    public double transformNumber(double number) {
        return Math.abs(number);
    }

    // -----------------------------------------------------------------------
    // parsing
    // -----------------------------------------------------------------------

    /**
     * Returns the additive inverse of the result of parsing the substitution (this supersedes the
     * earlier partial result)
     *
     * @param newRuleValue The result of parsing the substitution
     * @param oldRuleValue The partial parse result prior to calling this function
     * @return -newRuleValue
     */
    @Override
    public double composeRuleValue(double newRuleValue, double oldRuleValue) {
        return -newRuleValue;
    }

    /**
     * Sets the upper bound beck up to consider all rules
     *
     * @param oldUpperBound Ignored.
     * @return Double.MAX_VALUE
     */
    @Override
    public double calcUpperBound(double oldUpperBound) {
        return Double.MAX_VALUE;
    }

    // -----------------------------------------------------------------------
    // simple accessor
    // -----------------------------------------------------------------------

    /**
     * The token character for an AbsoluteValueSubstitution is &gt;
     *
     * @return '&gt;'
     */
    @Override
    char tokenChar() {
        return '>';
    }
}

// ===================================================================
// NumeratorSubstitution
// ===================================================================

/**
 * A substitution that multiplies the number being formatted (which is between 0 and 1) by the base
 * value of the rule that owns it and formats the result. It is represented by &lt;&lt; in the rules
 * in a fraction rule set.
 */
class NumeratorSubstitution extends NFSubstitution {
    // -----------------------------------------------------------------------
    // data members
    // -----------------------------------------------------------------------

    /**
     * The denominator of the fraction we're finding the numerator for. (The base value of the rule
     * that owns this substitution.)
     */
    private final double denominator;

    /** True if we format leading zeros (this is a hack for Hebrew spellout) */
    private final boolean withZeros;

    // -----------------------------------------------------------------------
    // construction
    // -----------------------------------------------------------------------

    /**
     * Constructs a NumeratorSubstitution. In addition to the inherited fields, a
     * NumeratorSubstitution keeps track of a denominator, which is merely the base value of the
     * rule that owns it.
     */
    NumeratorSubstitution(int pos, double denominator, NFRuleSet ruleSet, String description) {
        super(pos, ruleSet, fixdesc(description));

        // this substitution's behavior depends on the rule's base value
        // Rather than keeping a backpointer to the rule, we copy its
        // base value here
        this.denominator = denominator;

        this.withZeros = description.endsWith("<<");
    }

    static String fixdesc(String description) {
        return description.endsWith("<<")
                ? description.substring(0, description.length() - 1)
                : description;
    }

    // -----------------------------------------------------------------------
    // boilerplate
    // -----------------------------------------------------------------------

    /**
     * Tests two NumeratorSubstitutions for equality
     *
     * @param that The other NumeratorSubstitution
     * @return true if the two objects are functionally equivalent
     */
    @Override
    public boolean equals(Object that) {
        if (super.equals(that)) {
            NumeratorSubstitution that2 = (NumeratorSubstitution) that;
            return denominator == that2.denominator && withZeros == that2.withZeros;
        } else {
            return false;
        }
    }

    // -----------------------------------------------------------------------
    // formatting
    // -----------------------------------------------------------------------

    /**
     * Performs a mathematical operation on the number, formats it using either ruleSet or
     * decimalFormat, and inserts the result into toInsertInto.
     *
     * @param number The number being formatted.
     * @param toInsertInto The string we insert the result into
     * @param position The position in toInsertInto where the owning rule's rule text begins (this
     *     value is added to this substitution's position to determine exactly where to insert the
     *     new text)
     */
    @Override
    public void doSubstitution(
            double number, StringBuilder toInsertInto, int position, int recursionCount) {
        // perform a transformation on the number being formatted that
        // is dependent on the type of substitution this is
        // String s = toInsertInto.toString();
        double numberToFormat = transformNumber(number);

        if (withZeros && ruleSet != null) {
            // if there are leading zeros in the decimal expansion then emit them
            long nf = (long) numberToFormat;
            int len = toInsertInto.length();
            while ((nf *= 10) < denominator) {
                toInsertInto.insert(position + pos, ' ');
                ruleSet.format(0, toInsertInto, position + pos, recursionCount);
            }
            position += toInsertInto.length() - len;
        }

        // if the result is an integer, from here on out we work in integer
        // space (saving time and memory and preserving accuracy)
        if (numberToFormat == Math.floor(numberToFormat) && ruleSet != null) {
            ruleSet.format((long) numberToFormat, toInsertInto, position + pos, recursionCount);

            // if the result isn't an integer, then call either our rule set's
            // format() method or our DecimalFormat's format() method to
            // format the result
        } else {
            if (ruleSet != null) {
                ruleSet.format(numberToFormat, toInsertInto, position + pos, recursionCount);
            } else {
                toInsertInto.insert(position + pos, numberFormat.format(numberToFormat));
            }
        }
    }

    /**
     * Returns the number being formatted times the denominator.
     *
     * @param number The number being formatted
     * @return number * denominator
     */
    @Override
    public long transformNumber(long number) {
        return Math.round(number * denominator);
    }

    /**
     * Returns the number being formatted times the denominator.
     *
     * @param number The number being formatted
     * @return number * denominator
     */
    @Override
    public double transformNumber(double number) {
        return Math.round(number * denominator);
    }

    // -----------------------------------------------------------------------
    // parsing
    // -----------------------------------------------------------------------

    /**
     * Dispatches to the inherited version of this function, but makes sure that lenientParse is
     * off.
     */
    @Override
    public Number doParse(
            String text,
            ParsePosition parsePosition,
            double baseValue,
            double upperBound,
            boolean lenientParse,
            int nonNumericalExecutedRuleMask,
            int recursionCount) {
        // we don't have to do anything special to do the parsing here,
        // but we have to turn lenient parsing off-- if we leave it on,
        // it SERIOUSLY messes up the algorithm

        // if withZeros is true, we need to count the zeros
        // and use that to adjust the parse result
        int zeroCount = 0;
        if (withZeros) {
            String workText = text;
            ParsePosition workPos = new ParsePosition(1);

            while (workText.length() > 0 && workPos.getIndex() != 0) {
                workPos.setIndex(0);
                var unused =
                        ruleSet.parse(
                                        workText,
                                        workPos,
                                        1,
                                        nonNumericalExecutedRuleMask,
                                        recursionCount)
                                .intValue(); // parse zero or nothing at all
                if (workPos.getIndex() == 0) {
                    // we failed, either there were no more zeros, or the number was formatted with
                    // digits
                    // either way, we're done
                    break;
                }

                ++zeroCount;
                parsePosition.setIndex(parsePosition.getIndex() + workPos.getIndex());
                workText = workText.substring(workPos.getIndex());
                while (workText.length() > 0 && workText.charAt(0) == ' ') {
                    workText = workText.substring(1);
                    parsePosition.setIndex(parsePosition.getIndex() + 1);
                }
            }

            text = text.substring(parsePosition.getIndex()); // arrgh!
            parsePosition.setIndex(0);
        }

        // we've parsed off the zeros, now let's parse the rest from our current position
        Number result =
                super.doParse(
                        text,
                        parsePosition,
                        withZeros ? 1 : baseValue,
                        upperBound,
                        false,
                        nonNumericalExecutedRuleMask,
                        recursionCount);

        if (withZeros) {
            // any base value will do in this case.  is there a way to
            // force this to not bother trying all the base values?

            // compute the 'effective' base and prescale the value down
            long n = result.longValue();
            long d = 1;
            while (d <= n) {
                d *= 10;
            }
            // now add the zeros
            while (zeroCount > 0) {
                d *= 10;
                --zeroCount;
            }
            // d is now our true denominator
            result = (double) n / (double) d;
        }

        return result;
    }

    /**
     * Divides the result of parsing the substitution by the partial parse result.
     *
     * @param newRuleValue The result of parsing the substitution
     * @param oldRuleValue The owning rule's base value
     * @return newRuleValue / oldRuleValue
     */
    @Override
    public double composeRuleValue(double newRuleValue, double oldRuleValue) {
        return newRuleValue / oldRuleValue;
    }

    /**
     * Sets the upper bound down to this rule's base value
     *
     * @param oldUpperBound Ignored
     * @return The base value of the rule owning this substitution
     */
    @Override
    public double calcUpperBound(double oldUpperBound) {
        return denominator;
    }

    // -----------------------------------------------------------------------
    // simple accessor
    // -----------------------------------------------------------------------

    /**
     * The token character for a NumeratorSubstitution is &lt;
     *
     * @return '&lt;'
     */
    @Override
    char tokenChar() {
        return '<';
    }
}