MutablePatternModifier.java

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

import com.ibm.icu.impl.FormattedStringBuilder;
import com.ibm.icu.impl.StandardPlural;
import com.ibm.icu.impl.number.AffixUtils.SymbolProvider;
import com.ibm.icu.number.NumberFormatter.SignDisplay;
import com.ibm.icu.number.NumberFormatter.UnitWidth;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.NumberFormat.Field;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.Currency;

/**
 * This class is a {@link Modifier} that wraps a decimal format pattern. It applies the pattern's
 * affixes in {@link Modifier#apply}.
 *
 * <p>In addition to being a Modifier, this class contains the business logic for substituting the
 * correct locale symbols into the affixes of the decimal format pattern.
 *
 * <p>In order to use this class, create a new instance and call the following four setters: {@link
 * #setPatternInfo}, {@link #setPatternAttributes}, {@link #setSymbols}, and {@link
 * #setNumberProperties}. After calling these four setters, the instance will be ready for use as a
 * Modifier.
 *
 * <p>This is a MUTABLE, NON-THREAD-SAFE class designed for performance. Do NOT save references to
 * this or attempt to use it from multiple threads! Instead, you can obtain a safe, immutable
 * decimal format pattern modifier by calling {@link MutablePatternModifier#createImmutable}, in
 * effect treating this instance as a builder for the immutable variant.
 */
public class MutablePatternModifier implements Modifier, SymbolProvider, MicroPropsGenerator {

    // Modifier details
    final boolean isStrong;

    // Pattern details
    AffixPatternProvider patternInfo;
    Field field;
    SignDisplay signDisplay;
    boolean perMilleReplacesPercent;
    boolean approximately;

    // Symbol details
    DecimalFormatSymbols symbols;
    UnitWidth unitWidth;
    Currency currency;
    PluralRules rules;

    // Number details
    Signum signum;
    StandardPlural plural;

    // QuantityChain details
    MicroPropsGenerator parent;

    // Transient fields for rendering
    StringBuilder currentAffix;

    /**
     * @param isStrong Whether the modifier should be considered strong. For more information, see
     *     {@link Modifier#isStrong()}. Most of the time, decimal format pattern modifiers should be
     *     considered as non-strong.
     */
    public MutablePatternModifier(boolean isStrong) {
        this.isStrong = isStrong;
    }

    /**
     * Sets a reference to the parsed decimal format pattern, usually obtained from {@link
     * PatternStringParser#parseToPatternInfo(String)}, but any implementation of {@link
     * AffixPatternProvider} is accepted.
     *
     * @param field Which field to use for literal characters in the pattern.
     */
    public void setPatternInfo(AffixPatternProvider patternInfo, Field field) {
        this.patternInfo = patternInfo;
        this.field = field;
    }

    /**
     * Sets attributes that imply changes to the literal interpretation of the pattern string
     * affixes.
     *
     * @param signDisplay Whether to force a plus sign on positive numbers.
     * @param perMille Whether to substitute the percent sign in the pattern with a permille sign.
     * @param approximately Whether to prepend approximately to the sign
     */
    public void setPatternAttributes(
            SignDisplay signDisplay, boolean perMille, boolean approximately) {
        this.signDisplay = signDisplay;
        this.perMilleReplacesPercent = perMille;
        this.approximately = approximately;
    }

    /**
     * Sets locale-specific details that affect the symbols substituted into the pattern string
     * affixes.
     *
     * @param symbols The desired instance of DecimalFormatSymbols.
     * @param currency The currency to be used when substituting currency values into the affixes.
     * @param unitWidth The width used to render currencies.
     * @param rules Required if the triple currency sign, "¤¤¤", appears in the pattern, which can
     *     be determined from the convenience method {@link #needsPlurals()}.
     */
    public void setSymbols(
            DecimalFormatSymbols symbols,
            Currency currency,
            UnitWidth unitWidth,
            PluralRules rules) {
        assert (rules != null) == needsPlurals();
        this.symbols = symbols;
        this.currency = currency;
        this.unitWidth = unitWidth;
        this.rules = rules;
    }

    /**
     * Sets attributes of the current number being processed.
     *
     * @param signum -1 if negative; +1 if positive; or 0 if zero.
     * @param plural The plural form of the number, required only if the pattern contains the triple
     *     currency sign, "¤¤¤" (and as indicated by {@link #needsPlurals()}).
     */
    public void setNumberProperties(Signum signum, StandardPlural plural) {
        assert (plural != null) == needsPlurals();
        this.signum = signum;
        this.plural = plural;
    }

    /**
     * Returns true if the pattern represented by this MurkyModifier requires a plural keyword in
     * order to localize. This is currently true only if there is a currency long name placeholder
     * in the pattern ("¤¤¤").
     */
    public boolean needsPlurals() {
        return patternInfo.containsSymbolType(AffixUtils.TYPE_CURRENCY_TRIPLE);
    }

    /**
     * Creates a new quantity-dependent Modifier that behaves the same as the current instance, but
     * which is immutable and can be saved for future use. The number properties in the current
     * instance are mutated; all other properties are left untouched.
     *
     * <p>The resulting modifier cannot be used in a QuantityChain.
     *
     * @return An immutable that supports both positive and negative numbers.
     */
    public ImmutablePatternModifier createImmutable() {
        FormattedStringBuilder a = new FormattedStringBuilder();
        FormattedStringBuilder b = new FormattedStringBuilder();
        if (needsPlurals()) {
            // Slower path when we require the plural keyword.
            AdoptingModifierStore pm = new AdoptingModifierStore();
            for (StandardPlural plural : StandardPlural.VALUES) {
                setNumberProperties(Signum.POS, plural);
                pm.setModifier(Signum.POS, plural, createConstantModifier(a, b));
                setNumberProperties(Signum.POS_ZERO, plural);
                pm.setModifier(Signum.POS_ZERO, plural, createConstantModifier(a, b));
                setNumberProperties(Signum.NEG_ZERO, plural);
                pm.setModifier(Signum.NEG_ZERO, plural, createConstantModifier(a, b));
                setNumberProperties(Signum.NEG, plural);
                pm.setModifier(Signum.NEG, plural, createConstantModifier(a, b));
            }
            pm.freeze();
            return new ImmutablePatternModifier(pm, rules);
        } else {
            // Faster path when plural keyword is not needed.
            setNumberProperties(Signum.POS, null);
            Modifier positive = createConstantModifier(a, b);
            setNumberProperties(Signum.POS_ZERO, null);
            Modifier posZero = createConstantModifier(a, b);
            setNumberProperties(Signum.NEG_ZERO, null);
            Modifier negZero = createConstantModifier(a, b);
            setNumberProperties(Signum.NEG, null);
            Modifier negative = createConstantModifier(a, b);
            AdoptingModifierStore pm =
                    new AdoptingModifierStore(positive, posZero, negZero, negative);
            return new ImmutablePatternModifier(pm, null);
        }
    }

    /**
     * Uses the current properties to create a single {@link ConstantMultiFieldModifier} with
     * currency spacing support if required.
     *
     * @param a A working FormattedStringBuilder object; passed from the outside to prevent the need
     *     to create many new instances if this method is called in a loop.
     * @param b Another working FormattedStringBuilder object.
     * @return The constant modifier object.
     */
    private ConstantMultiFieldModifier createConstantModifier(
            FormattedStringBuilder a, FormattedStringBuilder b) {
        insertPrefix(a.clear(), 0);
        insertSuffix(b.clear(), 0);
        if (patternInfo.hasCurrencySign()) {
            return new CurrencySpacingEnabledModifier(
                    a, b, !patternInfo.hasBody(), isStrong, symbols);
        } else {
            return new ConstantMultiFieldModifier(a, b, !patternInfo.hasBody(), isStrong);
        }
    }

    public static class ImmutablePatternModifier implements MicroPropsGenerator {
        final AdoptingModifierStore pm;
        final PluralRules rules;
        /* final */ MicroPropsGenerator parent;

        ImmutablePatternModifier(AdoptingModifierStore pm, PluralRules rules) {
            this.pm = pm;
            this.rules = rules;
            this.parent = null;
        }

        public ImmutablePatternModifier addToChain(MicroPropsGenerator parent) {
            this.parent = parent;
            return this;
        }

        @Override
        public MicroProps processQuantity(DecimalQuantity quantity) {
            MicroProps micros = parent.processQuantity(quantity);
            if (micros.rounder != null) {
                micros.rounder.apply(quantity);
            }
            if (micros.modMiddle != null) {
                return micros;
            }
            applyToMicros(micros, quantity);
            return micros;
        }

        public void applyToMicros(MicroProps micros, DecimalQuantity quantity) {
            if (rules == null) {
                micros.modMiddle = pm.getModifierWithoutPlural(quantity.signum());
            } else {
                StandardPlural pluralForm =
                        RoundingUtils.getPluralSafe(micros.rounder, rules, quantity);
                micros.modMiddle = pm.getModifier(quantity.signum(), pluralForm);
            }
        }

        // NOTE: This method is not used in ICU4J right now.
        // In ICU4C, it is used by getPrefixSuffix().
        // Un-comment this method when getPrefixSuffix() is cleaned up in ICU4J.
        // public Modifier getModifier(byte signum, StandardPlural plural) {
        // if (rules == null) {
        // return pm.getModifier(signum);
        // } else {
        // return pm.getModifier(signum, plural);
        // }
        // }
    }

    /** Used by the unsafe code path. */
    public MicroPropsGenerator addToChain(MicroPropsGenerator parent) {
        this.parent = parent;
        return this;
    }

    @Override
    public MicroProps processQuantity(DecimalQuantity fq) {
        MicroProps micros = parent.processQuantity(fq);
        if (micros.rounder != null) {
            micros.rounder.apply(fq);
        }
        if (micros.modMiddle != null) {
            return micros;
        }
        if (needsPlurals()) {
            StandardPlural pluralForm = RoundingUtils.getPluralSafe(micros.rounder, rules, fq);
            setNumberProperties(fq.signum(), pluralForm);
        } else {
            setNumberProperties(fq.signum(), null);
        }
        micros.modMiddle = this;
        return micros;
    }

    @Override
    public int apply(FormattedStringBuilder output, int leftIndex, int rightIndex) {
        int prefixLen = insertPrefix(output, leftIndex);
        int suffixLen = insertSuffix(output, rightIndex + prefixLen);
        // If the pattern had no decimal stem body (like #,##0.00), overwrite the value.
        int overwriteLen = 0;
        if (!patternInfo.hasBody()) {
            overwriteLen =
                    output.splice(leftIndex + prefixLen, rightIndex + prefixLen, "", 0, 0, null);
        }
        CurrencySpacingEnabledModifier.applyCurrencySpacing(
                output,
                leftIndex,
                prefixLen,
                rightIndex + prefixLen + overwriteLen,
                suffixLen,
                symbols);
        return prefixLen + overwriteLen + suffixLen;
    }

    @Override
    public int getPrefixLength() {
        // Render the affix to get the length
        prepareAffix(true);
        int result = AffixUtils.unescapedCount(currentAffix, true, this); // prefix length
        return result;
    }

    @Override
    public int getCodePointCount() {
        // Render the affixes to get the length
        prepareAffix(true);
        int result = AffixUtils.unescapedCount(currentAffix, false, this); // prefix length
        prepareAffix(false);
        result += AffixUtils.unescapedCount(currentAffix, false, this); // suffix length
        return result;
    }

    @Override
    public boolean isStrong() {
        return isStrong;
    }

    @Override
    public boolean containsField(java.text.Format.Field field) {
        // This method is not currently used. (unsafe path not used in range formatting)
        assert false;
        return false;
    }

    @Override
    public Parameters getParameters() {
        // This method is not currently used.
        assert false;
        return null;
    }

    @Override
    public boolean strictEquals(Modifier other) {
        // This method is not currently used. (unsafe path not used in range formatting)
        assert false;
        return false;
    }

    private int insertPrefix(FormattedStringBuilder sb, int position) {
        prepareAffix(true);
        int length = AffixUtils.unescape(currentAffix, sb, position, this, field);
        return length;
    }

    private int insertSuffix(FormattedStringBuilder sb, int position) {
        prepareAffix(false);
        int length = AffixUtils.unescape(currentAffix, sb, position, this, field);
        return length;
    }

    /**
     * Pre-processes the prefix or suffix into the currentAffix field, creating and mutating that
     * field if necessary. Calls down to {@link
     * PatternStringUtils#affixPatternProviderToStringBuilder}.
     *
     * @param isPrefix true to prepare the prefix; false to prepare the suffix.
     */
    private void prepareAffix(boolean isPrefix) {
        if (currentAffix == null) {
            currentAffix = new StringBuilder();
        }
        PatternStringUtils.patternInfoToStringBuilder(
                patternInfo,
                isPrefix,
                PatternStringUtils.resolveSignDisplay(signDisplay, signum),
                approximately,
                plural,
                perMilleReplacesPercent,
                currentAffix);
    }

    /** Returns the string that substitutes a given symbol type in a pattern. */
    @Override
    public CharSequence getSymbol(int type) {
        switch (type) {
            case AffixUtils.TYPE_MINUS_SIGN:
                return symbols.getMinusSignString();
            case AffixUtils.TYPE_PLUS_SIGN:
                return symbols.getPlusSignString();
            case AffixUtils.TYPE_APPROXIMATELY_SIGN:
                return symbols.getApproximatelySignString();
            case AffixUtils.TYPE_PERCENT:
                return symbols.getPercentString();
            case AffixUtils.TYPE_PERMILLE:
                return symbols.getPerMillString();
            case AffixUtils.TYPE_CURRENCY_SINGLE:
                return getCurrencySymbolForUnitWidth();
            case AffixUtils.TYPE_CURRENCY_DOUBLE:
                return currency.getCurrencyCode();
            case AffixUtils.TYPE_CURRENCY_TRIPLE:
                // NOTE: This is the code path only for patterns containing "¤¤¤".
                // Plural currencies set via the API are formatted in LongNameHandler.
                // This code path is used by DecimalFormat via CurrencyPluralInfo.
                assert plural != null;
                return currency.getName(
                        symbols.getULocale(), Currency.PLURAL_LONG_NAME, plural.getKeyword(), null);
            case AffixUtils.TYPE_CURRENCY_QUAD:
                return "\uFFFD";
            case AffixUtils.TYPE_CURRENCY_QUINT:
                return currency.getName(symbols.getULocale(), Currency.NARROW_SYMBOL_NAME, null);
            default:
                throw new AssertionError();
        }
    }

    /** Returns the currency symbol for the unit width specified in setSymbols() */
    public String getCurrencySymbolForUnitWidth() {
        // UnitWidth ISO, HIDDEN, or NARROW overrides the singular currency symbol.
        if (unitWidth == UnitWidth.ISO_CODE) {
            return currency.getCurrencyCode();
        } else if (unitWidth == UnitWidth.HIDDEN) {
            return "";
        } else {
            int selector;
            switch (unitWidth) {
                case SHORT:
                    selector = Currency.SYMBOL_NAME;
                    break;
                case NARROW:
                    selector = Currency.NARROW_SYMBOL_NAME;
                    break;
                case FORMAL:
                    selector = Currency.FORMAL_SYMBOL_NAME;
                    break;
                case VARIANT:
                    selector = Currency.VARIANT_SYMBOL_NAME;
                    break;
                default:
                    throw new AssertionError();
            }
            return currency.getName(symbols.getULocale(), selector, null);
        }
    }
}