1// © 2017 and later: Unicode, Inc. and others.
2// License & terms of use: http://www.unicode.org/copyright.html#License
3package com.ibm.icu.impl.number;
4
5import com.ibm.icu.impl.StandardPlural;
6import com.ibm.icu.impl.number.AffixUtils.SymbolProvider;
7import com.ibm.icu.number.NumberFormatter.SignDisplay;
8import com.ibm.icu.number.NumberFormatter.UnitWidth;
9import com.ibm.icu.text.DecimalFormatSymbols;
10import com.ibm.icu.text.PluralRules;
11import com.ibm.icu.util.Currency;
12
13/**
14 * This class is a {@link Modifier} that wraps a decimal format pattern. It applies the pattern's affixes in
15 * {@link Modifier#apply}.
16 *
17 * <p>
18 * In addition to being a Modifier, this class contains the business logic for substituting the correct locale symbols
19 * into the affixes of the decimal format pattern.
20 *
21 * <p>
22 * In order to use this class, create a new instance and call the following four setters: {@link #setPatternInfo},
23 * {@link #setPatternAttributes}, {@link #setSymbols}, and {@link #setNumberProperties}. After calling these four
24 * setters, the instance will be ready for use as a Modifier.
25 *
26 * <p>
27 * This is a MUTABLE, NON-THREAD-SAFE class designed for performance. Do NOT save references to this or attempt to use
28 * it from multiple threads! Instead, you can obtain a safe, immutable decimal format pattern modifier by calling
29 * {@link MutablePatternModifier#createImmutable}, in effect treating this instance as a builder for the immutable
30 * variant.
31 */
32public class MutablePatternModifier implements Modifier, SymbolProvider, CharSequence, MicroPropsGenerator {
33
34    // Modifier details
35    final boolean isStrong;
36
37    // Pattern details
38    AffixPatternProvider patternInfo;
39    SignDisplay signDisplay;
40    boolean perMilleReplacesPercent;
41
42    // Symbol details
43    DecimalFormatSymbols symbols;
44    UnitWidth unitWidth;
45    Currency currency;
46    PluralRules rules;
47
48    // Number details
49    boolean isNegative;
50    StandardPlural plural;
51
52    // QuantityChain details
53    MicroPropsGenerator parent;
54
55    // Transient CharSequence fields
56    boolean inCharSequenceMode;
57    int flags;
58    int length;
59    boolean prependSign;
60    boolean plusReplacesMinusSign;
61
62    /**
63     * @param isStrong
64     *            Whether the modifier should be considered strong. For more information, see
65     *            {@link Modifier#isStrong()}. Most of the time, decimal format pattern modifiers should be considered
66     *            as non-strong.
67     */
68    public MutablePatternModifier(boolean isStrong) {
69        this.isStrong = isStrong;
70    }
71
72    /**
73     * Sets a reference to the parsed decimal format pattern, usually obtained from
74     * {@link PatternStringParser#parseToPatternInfo(String)}, but any implementation of {@link AffixPatternProvider} is
75     * accepted.
76     */
77    public void setPatternInfo(AffixPatternProvider patternInfo) {
78        this.patternInfo = patternInfo;
79    }
80
81    /**
82     * Sets attributes that imply changes to the literal interpretation of the pattern string affixes.
83     *
84     * @param signDisplay
85     *            Whether to force a plus sign on positive numbers.
86     * @param perMille
87     *            Whether to substitute the percent sign in the pattern with a permille sign.
88     */
89    public void setPatternAttributes(SignDisplay signDisplay, boolean perMille) {
90        this.signDisplay = signDisplay;
91        this.perMilleReplacesPercent = perMille;
92    }
93
94    /**
95     * Sets locale-specific details that affect the symbols substituted into the pattern string affixes.
96     *
97     * @param symbols
98     *            The desired instance of DecimalFormatSymbols.
99     * @param currency
100     *            The currency to be used when substituting currency values into the affixes.
101     * @param unitWidth
102     *            The width used to render currencies.
103     * @param rules
104     *            Required if the triple currency sign, "¤¤¤", appears in the pattern, which can be determined from the
105     *            convenience method {@link #needsPlurals()}.
106     */
107    public void setSymbols(DecimalFormatSymbols symbols, Currency currency, UnitWidth unitWidth, PluralRules rules) {
108        assert (rules != null) == needsPlurals();
109        this.symbols = symbols;
110        this.currency = currency;
111        this.unitWidth = unitWidth;
112        this.rules = rules;
113    }
114
115    /**
116     * Sets attributes of the current number being processed.
117     *
118     * @param isNegative
119     *            Whether the number is negative.
120     * @param plural
121     *            The plural form of the number, required only if the pattern contains the triple currency sign, "¤¤¤"
122     *            (and as indicated by {@link #needsPlurals()}).
123     */
124    public void setNumberProperties(boolean isNegative, StandardPlural plural) {
125        assert (plural != null) == needsPlurals();
126        this.isNegative = isNegative;
127        this.plural = plural;
128    }
129
130    /**
131     * Returns true if the pattern represented by this MurkyModifier requires a plural keyword in order to localize.
132     * This is currently true only if there is a currency long name placeholder in the pattern ("¤¤¤").
133     */
134    public boolean needsPlurals() {
135        return patternInfo.containsSymbolType(AffixUtils.TYPE_CURRENCY_TRIPLE);
136    }
137
138    /**
139     * Creates a new quantity-dependent Modifier that behaves the same as the current instance, but which is immutable
140     * and can be saved for future use. The number properties in the current instance are mutated; all other properties
141     * are left untouched.
142     *
143     * <p>
144     * The resulting modifier cannot be used in a QuantityChain.
145     *
146     * @return An immutable that supports both positive and negative numbers.
147     */
148    public ImmutablePatternModifier createImmutable() {
149        return createImmutableAndChain(null);
150    }
151
152    /**
153     * Creates a new quantity-dependent Modifier that behaves the same as the current instance, but which is immutable
154     * and can be saved for future use. The number properties in the current instance are mutated; all other properties
155     * are left untouched.
156     *
157     * @param parent
158     *            The QuantityChain to which to chain this immutable.
159     * @return An immutable that supports both positive and negative numbers.
160     */
161    public ImmutablePatternModifier createImmutableAndChain(MicroPropsGenerator parent) {
162        NumberStringBuilder a = new NumberStringBuilder();
163        NumberStringBuilder b = new NumberStringBuilder();
164        if (needsPlurals()) {
165            // Slower path when we require the plural keyword.
166            ParameterizedModifier pm = new ParameterizedModifier();
167            for (StandardPlural plural : StandardPlural.VALUES) {
168                setNumberProperties(false, plural);
169                pm.setModifier(false, plural, createConstantModifier(a, b));
170                setNumberProperties(true, plural);
171                pm.setModifier(true, plural, createConstantModifier(a, b));
172            }
173            pm.freeze();
174            return new ImmutablePatternModifier(pm, rules, parent);
175        } else {
176            // Faster path when plural keyword is not needed.
177            setNumberProperties(false, null);
178            Modifier positive = createConstantModifier(a, b);
179            setNumberProperties(true, null);
180            Modifier negative = createConstantModifier(a, b);
181            ParameterizedModifier pm = new ParameterizedModifier(positive, negative);
182            return new ImmutablePatternModifier(pm, null, parent);
183        }
184    }
185
186    /**
187     * Uses the current properties to create a single {@link ConstantMultiFieldModifier} with currency spacing support
188     * if required.
189     *
190     * @param a
191     *            A working NumberStringBuilder object; passed from the outside to prevent the need to create many new
192     *            instances if this method is called in a loop.
193     * @param b
194     *            Another working NumberStringBuilder object.
195     * @return The constant modifier object.
196     */
197    private ConstantMultiFieldModifier createConstantModifier(NumberStringBuilder a, NumberStringBuilder b) {
198        insertPrefix(a.clear(), 0);
199        insertSuffix(b.clear(), 0);
200        if (patternInfo.hasCurrencySign()) {
201            return new CurrencySpacingEnabledModifier(a, b, isStrong, symbols);
202        } else {
203            return new ConstantMultiFieldModifier(a, b, isStrong);
204        }
205    }
206
207    public static class ImmutablePatternModifier implements MicroPropsGenerator {
208        final ParameterizedModifier pm;
209        final PluralRules rules;
210        final MicroPropsGenerator parent;
211
212        ImmutablePatternModifier(ParameterizedModifier pm, PluralRules rules, MicroPropsGenerator parent) {
213            this.pm = pm;
214            this.rules = rules;
215            this.parent = parent;
216        }
217
218        @Override
219        public MicroProps processQuantity(DecimalQuantity quantity) {
220            MicroProps micros = parent.processQuantity(quantity);
221            applyToMicros(micros, quantity);
222            return micros;
223        }
224
225        public void applyToMicros(MicroProps micros, DecimalQuantity quantity) {
226            if (rules == null) {
227                micros.modMiddle = pm.getModifier(quantity.isNegative());
228            } else {
229                // TODO: Fix this. Avoid the copy.
230                DecimalQuantity copy = quantity.createCopy();
231                copy.roundToInfinity();
232                StandardPlural plural = copy.getStandardPlural(rules);
233                micros.modMiddle = pm.getModifier(quantity.isNegative(), plural);
234            }
235        }
236    }
237
238    /** Used by the unsafe code path. */
239    public MicroPropsGenerator addToChain(MicroPropsGenerator parent) {
240        this.parent = parent;
241        return this;
242    }
243
244    @Override
245    public MicroProps processQuantity(DecimalQuantity fq) {
246        MicroProps micros = parent.processQuantity(fq);
247        if (needsPlurals()) {
248            // TODO: Fix this. Avoid the copy.
249            DecimalQuantity copy = fq.createCopy();
250            micros.rounding.apply(copy);
251            setNumberProperties(fq.isNegative(), copy.getStandardPlural(rules));
252        } else {
253            setNumberProperties(fq.isNegative(), null);
254        }
255        micros.modMiddle = this;
256        return micros;
257    }
258
259    @Override
260    public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
261        int prefixLen = insertPrefix(output, leftIndex);
262        int suffixLen = insertSuffix(output, rightIndex + prefixLen);
263        CurrencySpacingEnabledModifier.applyCurrencySpacing(output, leftIndex, prefixLen, rightIndex + prefixLen,
264                suffixLen, symbols);
265        return prefixLen + suffixLen;
266    }
267
268    @Override
269    public int getPrefixLength() {
270        // Enter and exit CharSequence Mode to get the length.
271        enterCharSequenceMode(true);
272        int result = AffixUtils.unescapedCodePointCount(this, this);  // prefix length
273        exitCharSequenceMode();
274        return result;
275    }
276
277    @Override
278    public int getCodePointCount() {
279        // Enter and exit CharSequence Mode to get the length.
280        enterCharSequenceMode(true);
281        int result = AffixUtils.unescapedCodePointCount(this, this);  // prefix length
282        exitCharSequenceMode();
283        enterCharSequenceMode(false);
284        result += AffixUtils.unescapedCodePointCount(this, this);  // suffix length
285        exitCharSequenceMode();
286        return result;
287    }
288
289    @Override
290    public boolean isStrong() {
291        return isStrong;
292    }
293
294    private int insertPrefix(NumberStringBuilder sb, int position) {
295        enterCharSequenceMode(true);
296        int length = AffixUtils.unescape(this, sb, position, this);
297        exitCharSequenceMode();
298        return length;
299    }
300
301    private int insertSuffix(NumberStringBuilder sb, int position) {
302        enterCharSequenceMode(false);
303        int length = AffixUtils.unescape(this, sb, position, this);
304        exitCharSequenceMode();
305        return length;
306    }
307
308    /**
309     * Returns the string that substitutes a given symbol type in a pattern.
310     */
311    @Override
312    public CharSequence getSymbol(int type) {
313        switch (type) {
314        case AffixUtils.TYPE_MINUS_SIGN:
315            return symbols.getMinusSignString();
316        case AffixUtils.TYPE_PLUS_SIGN:
317            return symbols.getPlusSignString();
318        case AffixUtils.TYPE_PERCENT:
319            return symbols.getPercentString();
320        case AffixUtils.TYPE_PERMILLE:
321            return symbols.getPerMillString();
322        case AffixUtils.TYPE_CURRENCY_SINGLE:
323            // UnitWidth ISO, HIDDEN, or NARROW overrides the singular currency symbol.
324            if (unitWidth == UnitWidth.ISO_CODE) {
325                return currency.getCurrencyCode();
326            } else if (unitWidth == UnitWidth.HIDDEN) {
327                return "";
328            } else if (unitWidth == UnitWidth.NARROW) {
329                return currency.getName(symbols.getULocale(), Currency.NARROW_SYMBOL_NAME, null);
330            } else {
331                return currency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null);
332            }
333        case AffixUtils.TYPE_CURRENCY_DOUBLE:
334            return currency.getCurrencyCode();
335        case AffixUtils.TYPE_CURRENCY_TRIPLE:
336            // NOTE: This is the code path only for patterns containing "¤¤¤".
337            // Plural currencies set via the API are formatted in LongNameHandler.
338            // This code path is used by DecimalFormat via CurrencyPluralInfo.
339            assert plural != null;
340            return currency.getName(symbols.getULocale(), Currency.PLURAL_LONG_NAME, plural.getKeyword(), null);
341        case AffixUtils.TYPE_CURRENCY_QUAD:
342            return "\uFFFD";
343        case AffixUtils.TYPE_CURRENCY_QUINT:
344            return currency.getName(symbols.getULocale(), Currency.NARROW_SYMBOL_NAME, null);
345        default:
346            throw new AssertionError();
347        }
348    }
349
350    /** This method contains the heart of the logic for rendering LDML affix strings. */
351    private void enterCharSequenceMode(boolean isPrefix) {
352        assert !inCharSequenceMode;
353        inCharSequenceMode = true;
354
355        // Should the output render '+' where '-' would normally appear in the pattern?
356        plusReplacesMinusSign = !isNegative
357                && (signDisplay == SignDisplay.ALWAYS || signDisplay == SignDisplay.ACCOUNTING_ALWAYS)
358                && patternInfo.positiveHasPlusSign() == false;
359
360        // Should we use the affix from the negative subpattern? (If not, we will use the positive subpattern.)
361        boolean useNegativeAffixPattern = patternInfo.hasNegativeSubpattern()
362                && (isNegative || (patternInfo.negativeHasMinusSign() && plusReplacesMinusSign));
363
364        // Resolve the flags for the affix pattern.
365        flags = 0;
366        if (useNegativeAffixPattern) {
367            flags |= AffixPatternProvider.Flags.NEGATIVE_SUBPATTERN;
368        }
369        if (isPrefix) {
370            flags |= AffixPatternProvider.Flags.PREFIX;
371        }
372        if (plural != null) {
373            assert plural.ordinal() == (AffixPatternProvider.Flags.PLURAL_MASK & plural.ordinal());
374            flags |= plural.ordinal();
375        }
376
377        // Should we prepend a sign to the pattern?
378        if (!isPrefix || useNegativeAffixPattern) {
379            prependSign = false;
380        } else if (isNegative) {
381            prependSign = signDisplay != SignDisplay.NEVER;
382        } else {
383            prependSign = plusReplacesMinusSign;
384        }
385
386        // Finally, compute the length of the affix pattern.
387        length = patternInfo.length(flags) + (prependSign ? 1 : 0);
388    }
389
390    private void exitCharSequenceMode() {
391        assert inCharSequenceMode;
392        inCharSequenceMode = false;
393    }
394
395    @Override
396    public int length() {
397        assert inCharSequenceMode;
398        return length;
399    }
400
401    @Override
402    public char charAt(int index) {
403        assert inCharSequenceMode;
404        char candidate;
405        if (prependSign && index == 0) {
406            candidate = '-';
407        } else if (prependSign) {
408            candidate = patternInfo.charAt(flags, index - 1);
409        } else {
410            candidate = patternInfo.charAt(flags, index);
411        }
412        if (plusReplacesMinusSign && candidate == '-') {
413            return '+';
414        }
415        if (perMilleReplacesPercent && candidate == '%') {
416            return '‰';
417        }
418        return candidate;
419    }
420
421    @Override
422    public CharSequence subSequence(int start, int end) {
423        // Never called by AffixUtils
424        throw new AssertionError();
425    }
426}
427