// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
package com.ibm.icu.number;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;

import com.ibm.icu.impl.number.DecimalQuantity;
import com.ibm.icu.impl.number.MultiplierProducer;
import com.ibm.icu.impl.number.RoundingUtils;
import com.ibm.icu.number.NumberFormatter.RoundingPriority;
import com.ibm.icu.number.NumberFormatter.TrailingZeroDisplay;
import com.ibm.icu.text.PluralRules.Operand;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Currency.CurrencyUsage;

/**
 * A class that defines the rounding precision to be used when formatting numbers in NumberFormatter.
 *
 * <p>
 * To create a Precision, use one of the factory methods.
 *
 * @stable ICU 62
 * @see NumberFormatter
 */
public abstract class Precision {

    /* package-private final */ MathContext mathContext;
    /* package-private final */ TrailingZeroDisplay trailingZeroDisplay;

    /* package-private */ Precision() {
        mathContext = RoundingUtils.DEFAULT_MATH_CONTEXT_UNLIMITED;
    }

    /**
     * Show all available digits to full precision.
     *
     * <p>
     * <strong>NOTE:</strong> When formatting a <em>double</em>, this method, along with
     * {@link #minFraction} and {@link #minSignificantDigits}, will trigger complex algorithm similar to
     * <em>Dragon4</em> to determine the low-order digits and the number of digits to display based on
     * the value of the double. If the number of fraction places or significant digits can be bounded,
     * consider using {@link #maxFraction} or {@link #maxSignificantDigits} instead to maximize performance.
     * For more information, read the following blog post.
     *
     * <p>
     * http://www.serpentine.com/blog/2011/06/29/here-be-dragons-advances-in-problems-you-didnt-even-know-you-had/
     *
     * @return A Precision for chaining or passing to the NumberFormatter precision() setter.
     * @stable ICU 60
     * @see NumberFormatter
     */
    public static Precision unlimited() {
        return constructInfinite();
    }

    /**
     * Show numbers rounded if necessary to the nearest integer.
     *
     * @return A FractionPrecision for chaining or passing to the NumberFormatter precision() setter.
     * @stable ICU 60
     * @see NumberFormatter
     */
    public static FractionPrecision integer() {
        return constructFraction(0, 0);
    }

    /**
     * Show numbers rounded if necessary to a certain number of fraction places (numerals after the
     * decimal separator). Additionally, pad with zeros to ensure that this number of places are always
     * shown.
     *
     * <p>
     * Example output with minMaxFractionPlaces = 3:
     *
     * <p>
     * 87,650.000<br>
     * 8,765.000<br>
     * 876.500<br>
     * 87.650<br>
     * 8.765<br>
     * 0.876<br>
     * 0.088<br>
     * 0.009<br>
     * 0.000 (zero)
     *
     * <p>
     * This method is equivalent to {@link #minMaxFraction} with both arguments equal.
     *
     * @param minMaxFractionPlaces
     *            The minimum and maximum number of numerals to display after the decimal separator
     *            (rounding if too long or padding with zeros if too short).
     * @return A FractionPrecision for chaining or passing to the NumberFormatter precision() setter.
     * @throws IllegalArgumentException if the input number is too big or smaller than 0.
     * @stable ICU 60
     * @see NumberFormatter
     */
    public static FractionPrecision fixedFraction(int minMaxFractionPlaces) {
        if (minMaxFractionPlaces >= 0 && minMaxFractionPlaces <= RoundingUtils.MAX_INT_FRAC_SIG) {
            return constructFraction(minMaxFractionPlaces, minMaxFractionPlaces);
        } else {
            throw new IllegalArgumentException("Fraction length must be between 0 and "
                    + RoundingUtils.MAX_INT_FRAC_SIG
                    + " (inclusive)");
        }
    }

    /**
     * Always show at least a certain number of fraction places after the decimal separator, padding with
     * zeros if necessary. Do not perform rounding (display numbers to their full precision).
     *
     * <p>
     * <strong>NOTE:</strong> If you are formatting <em>doubles</em>, see the performance note in
     * {@link #unlimited}.
     *
     * @param minFractionPlaces
     *            The minimum number of numerals to display after the decimal separator (padding with
     *            zeros if necessary).
     * @return A FractionPrecision for chaining or passing to the NumberFormatter precision() setter.
     * @throws IllegalArgumentException if the input number is too big or smaller than 0.
     * @stable ICU 60
     * @see NumberFormatter
     */
    public static FractionPrecision minFraction(int minFractionPlaces) {
        if (minFractionPlaces >= 0 && minFractionPlaces <= RoundingUtils.MAX_INT_FRAC_SIG) {
            return constructFraction(minFractionPlaces, -1);
        } else {
            throw new IllegalArgumentException("Fraction length must be between 0 and "
                    + RoundingUtils.MAX_INT_FRAC_SIG
                    + " (inclusive)");
        }
    }

    /**
     * Show numbers rounded if necessary to a certain number of fraction places (numerals after the
     * decimal separator). Unlike the other fraction rounding strategies, this strategy does <em>not</em>
     * pad zeros to the end of the number.
     *
     * @param maxFractionPlaces
     *            The maximum number of numerals to display after the decimal mark (rounding if
     *            necessary).
     * @return A FractionPrecision for chaining or passing to the NumberFormatter precision() setter.
     * @throws IllegalArgumentException if the input number is too big or smaller than 0.
     * @stable ICU 60
     * @see NumberFormatter
     */
    public static FractionPrecision maxFraction(int maxFractionPlaces) {
        if (maxFractionPlaces >= 0 && maxFractionPlaces <= RoundingUtils.MAX_INT_FRAC_SIG) {
            return constructFraction(0, maxFractionPlaces);
        } else {
            throw new IllegalArgumentException("Fraction length must be between 0 and "
                    + RoundingUtils.MAX_INT_FRAC_SIG
                    + " (inclusive)");
        }
    }

    /**
     * Show numbers rounded if necessary to a certain number of fraction places (numerals after the
     * decimal separator); in addition, always show at least a certain number of places after the decimal
     * separator, padding with zeros if necessary.
     *
     * @param minFractionPlaces
     *            The minimum number of numerals to display after the decimal separator (padding with
     *            zeros if necessary).
     * @param maxFractionPlaces
     *            The maximum number of numerals to display after the decimal separator (rounding if
     *            necessary).
     * @return A FractionPrecision for chaining or passing to the NumberFormatter precision() setter.
     * @throws IllegalArgumentException if the input number is too big or smaller than 0.
     * @stable ICU 60
     * @see NumberFormatter
     */
    public static FractionPrecision minMaxFraction(int minFractionPlaces, int maxFractionPlaces) {
        if (minFractionPlaces >= 0
                && maxFractionPlaces <= RoundingUtils.MAX_INT_FRAC_SIG
                && minFractionPlaces <= maxFractionPlaces) {
            return constructFraction(minFractionPlaces, maxFractionPlaces);
        } else {
            throw new IllegalArgumentException("Fraction length must be between 0 and "
                    + RoundingUtils.MAX_INT_FRAC_SIG
                    + " (inclusive)");
        }
    }

    /**
     * Show numbers rounded if necessary to a certain number of significant digits or significant
     * figures. Additionally, pad with zeros to ensure that this number of significant digits/figures are
     * always shown.
     *
     * <p>
     * This method is equivalent to {@link #minMaxSignificantDigits} with both arguments equal.
     *
     * @param minMaxSignificantDigits
     *            The minimum and maximum number of significant digits to display (rounding if too long
     *            or padding with zeros if too short).
     * @return A Precision for chaining or passing to the NumberFormatter precision() setter.
     * @throws IllegalArgumentException if the input number is too big or smaller than 1.
     * @stable ICU 62
     * @see NumberFormatter
     */
    public static Precision fixedSignificantDigits(int minMaxSignificantDigits) {
        if (minMaxSignificantDigits >= 1 && minMaxSignificantDigits <= RoundingUtils.MAX_INT_FRAC_SIG) {
            return constructSignificant(minMaxSignificantDigits, minMaxSignificantDigits);
        } else {
            throw new IllegalArgumentException("Significant digits must be between 1 and "
                    + RoundingUtils.MAX_INT_FRAC_SIG
                    + " (inclusive)");
        }
    }

    /**
     * Always show at least a certain number of significant digits/figures, padding with zeros if
     * necessary. Do not perform rounding (display numbers to their full precision).
     *
     * <p>
     * <strong>NOTE:</strong> If you are formatting <em>doubles</em>, see the performance note in
     * {@link #unlimited}.
     *
     * @param minSignificantDigits
     *            The minimum number of significant digits to display (padding with zeros if too short).
     * @return A Precision for chaining or passing to the NumberFormatter precision() setter.
     * @throws IllegalArgumentException if the input number is too big or smaller than 1.
     * @stable ICU 62
     * @see NumberFormatter
     */
    public static Precision minSignificantDigits(int minSignificantDigits) {
        if (minSignificantDigits >= 1 && minSignificantDigits <= RoundingUtils.MAX_INT_FRAC_SIG) {
            return constructSignificant(minSignificantDigits, -1);
        } else {
            throw new IllegalArgumentException("Significant digits must be between 1 and "
                    + RoundingUtils.MAX_INT_FRAC_SIG
                    + " (inclusive)");
        }
    }

    /**
     * Show numbers rounded if necessary to a certain number of significant digits/figures.
     *
     * @param maxSignificantDigits
     *            The maximum number of significant digits to display (rounding if too long).
     * @return A Precision for chaining or passing to the NumberFormatter precision() setter.
     * @throws IllegalArgumentException if the input number is too big or smaller than 1.
     * @stable ICU 62
     * @see NumberFormatter
     */
    public static Precision maxSignificantDigits(int maxSignificantDigits) {
        if (maxSignificantDigits >= 1 && maxSignificantDigits <= RoundingUtils.MAX_INT_FRAC_SIG) {
            return constructSignificant(1, maxSignificantDigits);
        } else {
            throw new IllegalArgumentException("Significant digits must be between 1 and "
                    + RoundingUtils.MAX_INT_FRAC_SIG
                    + " (inclusive)");
        }
    }

    /**
     * Show numbers rounded if necessary to a certain number of significant digits/figures; in addition,
     * always show at least a certain number of significant digits, padding with zeros if necessary.
     *
     * @param minSignificantDigits
     *            The minimum number of significant digits to display (padding with zeros if necessary).
     * @param maxSignificantDigits
     *            The maximum number of significant digits to display (rounding if necessary).
     * @return A Precision for chaining or passing to the NumberFormatter precision() setter.
     * @throws IllegalArgumentException if the input number is too big or smaller than 1.
     * @stable ICU 62
     * @see NumberFormatter
     */
    public static Precision minMaxSignificantDigits(int minSignificantDigits, int maxSignificantDigits) {
        if (minSignificantDigits >= 1
                && maxSignificantDigits <= RoundingUtils.MAX_INT_FRAC_SIG
                && minSignificantDigits <= maxSignificantDigits) {
            return constructSignificant(minSignificantDigits, maxSignificantDigits);
        } else {
            throw new IllegalArgumentException("Significant digits must be between 1 and "
                    + RoundingUtils.MAX_INT_FRAC_SIG
                    + " (inclusive)");
        }
    }

    /**
     * Show numbers rounded if necessary to the closest multiple of a certain rounding increment. For
     * example, if the rounding increment is 0.5, then round 1.2 to 1 and round 1.3 to 1.5.
     *
     * <p>
     * In order to ensure that numbers are padded to the appropriate number of fraction places, set the
     * scale on the rounding increment BigDecimal. For example, to round to the nearest 0.5 and always
     * display 2 numerals after the decimal separator (to display 1.2 as "1.00" and 1.3 as "1.50"), you
     * can run:
     *
     * <pre>
     * Precision.increment(new BigDecimal("0.50"))
     * </pre>
     *
     * <p>
     * For more information on the scale of Java BigDecimal, see {@link java.math.BigDecimal#scale()}.
     *
     * @param roundingIncrement
     *            The increment to which to round numbers.
     * @return A Precision for chaining or passing to the NumberFormatter precision() setter.
     * @throws IllegalArgumentException if the rounding increment is null or non-positive.
     * @stable ICU 60
     * @see NumberFormatter
     */
    public static Precision increment(BigDecimal roundingIncrement) {
        if (roundingIncrement != null && roundingIncrement.compareTo(BigDecimal.ZERO) > 0) {
            return constructIncrement(roundingIncrement);
        } else {
            throw new IllegalArgumentException("Rounding increment must be positive and non-null");
        }
    }

    /**
     * Show numbers rounded and padded according to the rules for the currency unit. The most common
     * rounding precision settings for currencies include <code>Precision.fixedFraction(2)</code>,
     * <code>Precision.integer()</code>, and <code>Precision.increment(0.05)</code> for cash transactions
     * ("nickel rounding").
     *
     * <p>
     * The exact rounding details will be resolved at runtime based on the currency unit specified in the
     * NumberFormatter chain. To round according to the rules for one currency while displaying the
     * symbol for another currency, the withCurrency() method can be called on the return value of this
     * method.
     *
     * @param currencyUsage
     *            Either STANDARD (for digital transactions) or CASH (for transactions where the rounding
     *            increment may be limited by the available denominations of cash or coins).
     * @return A CurrencyPrecision for chaining or passing to the NumberFormatter precision() setter.
     * @throws IllegalArgumentException if currencyUsage is null.
     * @stable ICU 60
     * @see NumberFormatter
     */
    public static CurrencyPrecision currency(CurrencyUsage currencyUsage) {
        if (currencyUsage != null) {
            return constructCurrency(currencyUsage);
        } else {
            throw new IllegalArgumentException("CurrencyUsage must be non-null");
        }
    }

    /**
     * Configure how trailing zeros are displayed on numbers. For example, to hide trailing zeros
     * when the number is an integer, use HIDE_IF_WHOLE.
     *
     * @param trailingZeroDisplay Option to configure the display of trailing zeros.
     * @stable ICU 69
     */
    public Precision trailingZeroDisplay(TrailingZeroDisplay trailingZeroDisplay) {
        Precision result = this.createCopy();
        result.trailingZeroDisplay = trailingZeroDisplay;
        return result;
    }

    /**
     * Sets a MathContext to use on this Precision.
     *
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public Precision withMode(MathContext mathContext) {
        if (this.mathContext.equals(mathContext)) {
            return this;
        }
        Precision other = createCopy();
        other.mathContext = mathContext;
        return other;
    }

    /** Package-private clone method */
    abstract Precision createCopy();

    /**
     * Call this function to copy the fields from the Precision base class.
     *
     * Note: It would be nice if this returned the copy, but most impls return the child class, not Precision.
     */
    /* package-private */ void createCopyHelper(Precision copy) {
        copy.mathContext = mathContext;
        copy.trailingZeroDisplay = trailingZeroDisplay;
    }

    /**
     * @internal
     * @deprecated ICU 60 This API is ICU internal only.
     */
    @Deprecated
    public abstract void apply(DecimalQuantity value);

    //////////////////////////
    // PACKAGE-PRIVATE APIS //
    //////////////////////////

    /**
     * @internal
     * @deprecated ICU internal only.
     */
    @Deprecated
    public static final BogusRounder BOGUS_PRECISION = new BogusRounder();

    static final InfiniteRounderImpl NONE = new InfiniteRounderImpl();

    static final FractionRounderImpl FIXED_FRAC_0 = new FractionRounderImpl(0, 0);
    static final FractionRounderImpl FIXED_FRAC_2 = new FractionRounderImpl(2, 2);
    static final FractionRounderImpl DEFAULT_MAX_FRAC_6 = new FractionRounderImpl(0, 6);

    static final SignificantRounderImpl FIXED_SIG_2 = new SignificantRounderImpl(2, 2);
    static final SignificantRounderImpl FIXED_SIG_3 = new SignificantRounderImpl(3, 3);
    static final SignificantRounderImpl RANGE_SIG_2_3 = new SignificantRounderImpl(2, 3);

    static final FracSigRounderImpl COMPACT_STRATEGY = new FracSigRounderImpl(0, 0, 1, 2, RoundingPriority.RELAXED,
            false);

    static final IncrementFiveRounderImpl NICKEL = new IncrementFiveRounderImpl(new BigDecimal("0.05"), 2, 2);

    static final CurrencyRounderImpl MONETARY_STANDARD = new CurrencyRounderImpl(CurrencyUsage.STANDARD);
    static final CurrencyRounderImpl MONETARY_CASH = new CurrencyRounderImpl(CurrencyUsage.CASH);

    static Precision constructInfinite() {
        return NONE;
    }

    static FractionPrecision constructFraction(int minFrac, int maxFrac) {
        if (minFrac == 0 && maxFrac == 0) {
            return FIXED_FRAC_0;
        } else if (minFrac == 2 && maxFrac == 2) {
            return FIXED_FRAC_2;
        } else if (minFrac == 0 && maxFrac == 6) {
            return DEFAULT_MAX_FRAC_6;
        } else {
            return new FractionRounderImpl(minFrac, maxFrac);
        }
    }

    /** Assumes that minSig <= maxSig. */
    static Precision constructSignificant(int minSig, int maxSig) {
        if (minSig == 2 && maxSig == 2) {
            return FIXED_SIG_2;
        } else if (minSig == 3 && maxSig == 3) {
            return FIXED_SIG_3;
        } else if (minSig == 2 && maxSig == 3) {
            return RANGE_SIG_2_3;
        } else {
            return new SignificantRounderImpl(minSig, maxSig);
        }
    }

    static Precision constructFractionSignificant(FractionPrecision base_, int minSig, int maxSig,
            RoundingPriority priority, boolean retain) {
        assert base_ instanceof FractionRounderImpl;
        FractionRounderImpl base = (FractionRounderImpl) base_;
        Precision returnValue;
        if (base.minFrac == 0 && base.maxFrac == 0 && minSig == 1 && maxSig == 2 && priority == RoundingPriority.RELAXED
                && !retain) {
            returnValue = COMPACT_STRATEGY;
        } else {
            returnValue = new FracSigRounderImpl(base.minFrac, base.maxFrac, minSig, maxSig, priority, retain);
        }
        return returnValue.withMode(base.mathContext);
    }

    static Precision constructIncrement(BigDecimal increment) {
        // NOTE: .equals() is what we want, not .compareTo()
        if (increment.equals(NICKEL.increment)) {
            return NICKEL;
        }
        // Note: For number formatting, the BigDecimal increment is used for IncrementRounderImpl
        // but not mIncrementOneRounderImpl or IncrementFiveRounderImpl. However, fIncrement is
        // used in all three when constructing a skeleton.
        BigDecimal reduced = increment.stripTrailingZeros();
        if (reduced.precision() == 1) {
            int minFrac = increment.scale();
            int maxFrac = reduced.scale();
            BigInteger digit = reduced.unscaledValue();
            if (digit.intValue() == 1) {
                return new IncrementOneRounderImpl(increment, minFrac, maxFrac);
            } else if (digit.intValue() == 5) {
                return new IncrementFiveRounderImpl(increment, minFrac, maxFrac);
            }
        }
        return new IncrementRounderImpl(increment);
    }

    static CurrencyPrecision constructCurrency(CurrencyUsage usage) {
        if (usage == CurrencyUsage.STANDARD) {
            return MONETARY_STANDARD;
        } else if (usage == CurrencyUsage.CASH) {
            return MONETARY_CASH;
        } else {
            throw new AssertionError();
        }
    }

    static Precision constructFromCurrency(CurrencyPrecision base_, Currency currency) {
        assert base_ instanceof CurrencyRounderImpl;
        CurrencyRounderImpl base = (CurrencyRounderImpl) base_;
        double incrementDouble = currency.getRoundingIncrement(base.usage);
        Precision returnValue;
        if (incrementDouble != 0.0) {
            BigDecimal increment = BigDecimal.valueOf(incrementDouble);
            returnValue = constructIncrement(increment);
        } else {
            int minMaxFrac = currency.getDefaultFractionDigits(base.usage);
            returnValue = constructFraction(minMaxFrac, minMaxFrac);
        }
        return returnValue.withMode(base.mathContext);
    }

    /**
     * Returns a valid working Rounder. If the Rounder is a CurrencyRounder, applies the given currency.
     * Otherwise, simply passes through the argument.
     *
     * @param currency
     *            A currency object to use in case the input object needs it.
     * @return A Rounder object ready for use.
     */
    Precision withLocaleData(Currency currency) {
        if (this instanceof CurrencyPrecision) {
            return ((CurrencyPrecision) this).withCurrency(currency);
        } else {
            return this;
        }
    }

    /**
     * Rounding endpoint used by Engineering and Compact notation. Chooses the most appropriate
     * multiplier (magnitude adjustment), applies the adjustment, rounds, and returns the chosen
     * multiplier.
     *
     * <p>
     * In most cases, this is simple. However, when rounding the number causes it to cross a multiplier
     * boundary, we need to re-do the rounding. For example, to display 999,999 in Engineering notation
     * with 2 sigfigs, first you guess the multiplier to be -3. However, then you end up getting 1000E3,
     * which is not the correct output. You then change your multiplier to be -6, and you get 1.0E6,
     * which is correct.
     *
     * @param input
     *            The quantity to process.
     * @param producer
     *            Function to call to return a multiplier based on a magnitude.
     * @return The number of orders of magnitude the input was adjusted by this method.
     */
    int chooseMultiplierAndApply(DecimalQuantity input, MultiplierProducer producer) {
        // Do not call this method with zero, NaN, or infinity.
        assert !input.isZeroish();

        // Perform the first attempt at rounding.
        int magnitude = input.getMagnitude();
        int multiplier = producer.getMultiplier(magnitude);
        input.adjustMagnitude(multiplier);
        apply(input);

        // If the number rounded to zero, exit.
        if (input.isZeroish()) {
            return multiplier;
        }

        // If the new magnitude after rounding is the same as it was before rounding, then we are done.
        // This case applies to most numbers.
        if (input.getMagnitude() == magnitude + multiplier) {
            return multiplier;
        }

        // If the above case DIDN'T apply, then we have a case like 99.9 -> 100 or 999.9 -> 1000:
        // The number rounded up to the next magnitude. Check if the multiplier changes; if it doesn't,
        // we do not need to make any more adjustments.
        int _multiplier = producer.getMultiplier(magnitude + 1);
        if (multiplier == _multiplier) {
            return multiplier;
        }

        // We have a case like 999.9 -> 1000, where the correct output is "1K", not "1000".
        // Fix the magnitude and re-apply the rounding strategy.
        input.adjustMagnitude(_multiplier - multiplier);
        apply(input);
        return _multiplier;
    }

    ///////////////
    // INTERNALS //
    ///////////////

    /**
     * An BogusRounder's MathContext into precision.
     *
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static class BogusRounder extends Precision {
        /**
         * Default constructor.
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        public BogusRounder() {
        }

        /**
         * {@inheritDoc}
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Override
        @Deprecated
        public void apply(DecimalQuantity value) {
            throw new AssertionError("BogusRounder must not be applied");
        }

        @Override
        BogusRounder createCopy() {
            BogusRounder copy = new BogusRounder();
            createCopyHelper(copy);
            return copy;
        }

        /**
         * Copies the BogusRounder's MathContext into precision.
         *
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        public Precision into(Precision precision) {
            Precision copy = precision.createCopy();
            createCopyHelper(copy);
            return copy;
        }
    }

    static class InfiniteRounderImpl extends Precision {

        public InfiniteRounderImpl() {
        }

        @Override
        public void apply(DecimalQuantity value) {
            value.roundToInfinity();
            setResolvedMinFraction(value, 0);
        }

        @Override
        InfiniteRounderImpl createCopy() {
            InfiniteRounderImpl copy = new InfiniteRounderImpl();
            createCopyHelper(copy);
            return copy;
        }
    }

    static class FractionRounderImpl extends FractionPrecision {
        final int minFrac;
        final int maxFrac;

        public FractionRounderImpl(int minFrac, int maxFrac) {
            this.minFrac = minFrac;
            this.maxFrac = maxFrac;
        }

        @Override
        public void apply(DecimalQuantity value) {
            value.roundToMagnitude(getRoundingMagnitudeFraction(maxFrac), mathContext);
            setResolvedMinFraction(value, Math.max(0, -getDisplayMagnitudeFraction(minFrac)));
        }

        @Override
        FractionRounderImpl createCopy() {
            FractionRounderImpl copy = new FractionRounderImpl(minFrac, maxFrac);
            createCopyHelper(copy);
            return copy;
        }
    }

    static class SignificantRounderImpl extends Precision {
        final int minSig;
        final int maxSig;

        public SignificantRounderImpl(int minSig, int maxSig) {
            this.minSig = minSig;
            this.maxSig = maxSig;
        }

        @Override
        public void apply(DecimalQuantity value) {
            value.roundToMagnitude(getRoundingMagnitudeSignificant(value, maxSig), mathContext);
            setResolvedMinFraction(value, Math.max(0, -getDisplayMagnitudeSignificant(value, minSig)));
            // Make sure that digits are displayed on zero.
            if (value.isZeroish() && minSig > 0) {
                value.setMinInteger(1);
            }
        }

        /**
         * Version of {@link #apply} that obeys minInt constraints. Used for scientific notation
         * compatibility mode.
         */
        public void apply(DecimalQuantity quantity, int minInt) {
            assert quantity.isZeroish();
            setResolvedMinFraction(quantity, minSig - minInt);
        }

        @Override
        SignificantRounderImpl createCopy() {
            SignificantRounderImpl copy = new SignificantRounderImpl(minSig, maxSig);
            createCopyHelper(copy);
            return copy;
        }
    }

    static class FracSigRounderImpl extends Precision {
        final int minFrac;
        final int maxFrac;
        final int minSig;
        final int maxSig;
        final RoundingPriority priority;
        final boolean retain;

        public FracSigRounderImpl(int minFrac, int maxFrac, int minSig, int maxSig, RoundingPriority priority,
                boolean retain) {
            this.minFrac = minFrac;
            this.maxFrac = maxFrac;
            this.minSig = minSig;
            this.maxSig = maxSig;
            this.priority = priority;
            this.retain = retain;
        }

        @Override
        public void apply(DecimalQuantity value) {
            int roundingMag1 = getRoundingMagnitudeFraction(maxFrac);
            int roundingMag2 = getRoundingMagnitudeSignificant(value, maxSig);
            int roundingMag;
            if (priority == RoundingPriority.RELAXED) {
                roundingMag = Math.min(roundingMag1, roundingMag2);
            } else {
                roundingMag = Math.max(roundingMag1, roundingMag2);
            }
            if (!value.isZeroish()) {
                int upperMag = value.getMagnitude();
                value.roundToMagnitude(roundingMag, mathContext);
                if (!value.isZeroish() && value.getMagnitude() != upperMag && roundingMag1 == roundingMag2) {
                    // roundingMag2 needs to be the magnitude after rounding
                    roundingMag2 += 1;
                }
            }

            int displayMag1 = getDisplayMagnitudeFraction(minFrac);
            int displayMag2 = getDisplayMagnitudeSignificant(value, minSig);
            int displayMag;
            if (retain) {
                // withMinDigits + withMaxDigits
                displayMag = Math.min(displayMag1, displayMag2);
            } else if (priority == RoundingPriority.RELAXED) {
                if (roundingMag2 <= roundingMag1) {
                    displayMag = displayMag2;
                } else {
                    displayMag = displayMag1;
                }
            } else {
                assert(priority == RoundingPriority.STRICT);
                if (roundingMag2 <= roundingMag1) {
                    displayMag = displayMag1;
                } else {
                    displayMag = displayMag2;
                }
            }
            setResolvedMinFraction(value, Math.max(0, -displayMag));
        }

        @Override
        FracSigRounderImpl createCopy() {
            FracSigRounderImpl copy = new FracSigRounderImpl(minFrac, maxFrac, minSig, maxSig, priority, retain);
            createCopyHelper(copy);
            return copy;
        }
    }

    /**
     * Used for strange increments like 3.14.
     */
    static class IncrementRounderImpl extends Precision {
        final BigDecimal increment;

        public IncrementRounderImpl(BigDecimal increment) {
            this.increment = increment;
        }

        @Override
        public void apply(DecimalQuantity value) {
            value.roundToIncrement(increment, mathContext);
            setResolvedMinFraction(value, Math.max(0, increment.scale()));
        }

        @Override
        IncrementRounderImpl createCopy() {
            IncrementRounderImpl copy = new IncrementRounderImpl(increment);
            createCopyHelper(copy);
            return copy;
        }
    }

    /**
     * Used for increments with 1 as the only digit. This is different than fraction
     * rounding because it supports having additional trailing zeros. For example, this
     * class is used to round with the increment 0.010.
     */
    static class IncrementOneRounderImpl extends IncrementRounderImpl {
        final int minFrac;
        final int maxFrac;

        public IncrementOneRounderImpl(BigDecimal increment, int minFrac, int maxFrac) {
            super(increment);
            this.minFrac = minFrac;
            this.maxFrac = maxFrac;
        }

        @Override
        public void apply(DecimalQuantity value) {
            value.roundToMagnitude(-maxFrac, mathContext);
            setResolvedMinFraction(value, minFrac);
        }

        @Override
        IncrementOneRounderImpl createCopy() {
            IncrementOneRounderImpl copy = new IncrementOneRounderImpl(increment, minFrac, maxFrac);
            createCopyHelper(copy);
            return copy;
        }
    }

    /**
     * Used for increments with 5 as the only digit (nickel rounding).
     */
    static class IncrementFiveRounderImpl extends IncrementRounderImpl {
        final int minFrac;
        final int maxFrac;

        public IncrementFiveRounderImpl(BigDecimal increment, int minFrac, int maxFrac) {
            super(increment);
            this.minFrac = minFrac;
            this.maxFrac = maxFrac;
        }

        @Override
        public void apply(DecimalQuantity value) {
            value.roundToNickel(-maxFrac, mathContext);
            setResolvedMinFraction(value, minFrac);
        }

        @Override
        IncrementFiveRounderImpl createCopy() {
            IncrementFiveRounderImpl copy = new IncrementFiveRounderImpl(increment, minFrac, maxFrac);
            createCopyHelper(copy);
            return copy;
        }
    }

    static class CurrencyRounderImpl extends CurrencyPrecision {
        final CurrencyUsage usage;

        public CurrencyRounderImpl(CurrencyUsage usage) {
            this.usage = usage;
        }

        @Override
        public void apply(DecimalQuantity value) {
            // Call .withCurrency() before .apply()!
            throw new AssertionError();
        }

        @Override
        CurrencyRounderImpl createCopy() {
            CurrencyRounderImpl copy = new CurrencyRounderImpl(usage);
            createCopyHelper(copy);
            return copy;
        }
    }

    private static int getRoundingMagnitudeFraction(int maxFrac) {
        if (maxFrac == -1) {
            return Integer.MIN_VALUE;
        }
        return -maxFrac;
    }

    private static int getRoundingMagnitudeSignificant(DecimalQuantity value, int maxSig) {
        if (maxSig == -1) {
            return Integer.MIN_VALUE;
        }
        int magnitude = value.isZeroish() ? 0 : value.getMagnitude();
        return magnitude - maxSig + 1;
    }

    private static int getDisplayMagnitudeFraction(int minFrac) {
        if (minFrac == 0) {
            return Integer.MAX_VALUE;
        }
        return -minFrac;
    }

    void setResolvedMinFraction(DecimalQuantity value, int resolvedMinFraction) {
        if (trailingZeroDisplay == null ||
                trailingZeroDisplay == TrailingZeroDisplay.AUTO ||
                // PLURAL_OPERAND_T returns fraction digits as an integer
                value.getPluralOperand(Operand.t) != 0) {
            value.setMinFraction(resolvedMinFraction);
        }
    }

    private static int getDisplayMagnitudeSignificant(DecimalQuantity value, int minSig) {
        // Question: Is it useful to look at trailingZeroDisplay here?
        int magnitude = value.isZeroish() ? 0 : value.getMagnitude();
        return magnitude - minSig + 1;
    }
}
