NumberPropertyMapper.java revision dcdfae8fec2b6153a1674e58a02144ca6ab477e8
1// © 2017 and later: Unicode, Inc. and others.
2// License & terms of use: http://www.unicode.org/copyright.html#License
3package com.ibm.icu.number;
4
5import java.math.BigDecimal;
6import java.math.MathContext;
7
8import com.ibm.icu.impl.StandardPlural;
9import com.ibm.icu.impl.number.AffixPatternProvider;
10import com.ibm.icu.impl.number.AffixUtils;
11import com.ibm.icu.impl.number.CustomSymbolCurrency;
12import com.ibm.icu.impl.number.DecimalFormatProperties;
13import com.ibm.icu.impl.number.MacroProps;
14import com.ibm.icu.impl.number.MultiplierImpl;
15import com.ibm.icu.impl.number.Padder;
16import com.ibm.icu.impl.number.PatternStringParser;
17import com.ibm.icu.impl.number.PatternStringParser.ParsedPatternInfo;
18import com.ibm.icu.impl.number.RoundingUtils;
19import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay;
20import com.ibm.icu.number.NumberFormatter.SignDisplay;
21import com.ibm.icu.number.Rounder.FractionRounderImpl;
22import com.ibm.icu.number.Rounder.IncrementRounderImpl;
23import com.ibm.icu.number.Rounder.SignificantRounderImpl;
24import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
25import com.ibm.icu.text.CurrencyPluralInfo;
26import com.ibm.icu.text.DecimalFormatSymbols;
27import com.ibm.icu.util.Currency;
28import com.ibm.icu.util.Currency.CurrencyUsage;
29import com.ibm.icu.util.ULocale;
30
31/**
32 * <p>
33 * This class, as well as NumberFormatterImpl, could go into the impl package, but they depend on too many
34 * package-private members of the public APIs.
35 */
36final class NumberPropertyMapper {
37
38    /** Convenience method to create a NumberFormatter directly from Properties. */
39    public static UnlocalizedNumberFormatter create(DecimalFormatProperties properties, DecimalFormatSymbols symbols) {
40        MacroProps macros = oldToNew(properties, symbols, null);
41        return NumberFormatter.with().macros(macros);
42    }
43
44    /**
45     * Convenience method to create a NumberFormatter directly from a pattern string. Something like this could become
46     * public API if there is demand.
47     */
48    public static UnlocalizedNumberFormatter create(String pattern, DecimalFormatSymbols symbols) {
49        DecimalFormatProperties properties = PatternStringParser.parseToProperties(pattern);
50        return create(properties, symbols);
51    }
52
53    /**
54     * Creates a new {@link MacroProps} object based on the content of a {@link DecimalFormatProperties} object. In
55     * other words, maps Properties to MacroProps. This function is used by the JDK-compatibility API to call into the
56     * ICU 60 fluent number formatting pipeline.
57     *
58     * @param properties
59     *            The property bag to be mapped.
60     * @param symbols
61     *            The symbols associated with the property bag.
62     * @param exportedProperties
63     *            A property bag in which to store validated properties.
64     * @return A new MacroProps containing all of the information in the Properties.
65     */
66    public static MacroProps oldToNew(DecimalFormatProperties properties, DecimalFormatSymbols symbols,
67            DecimalFormatProperties exportedProperties) {
68        MacroProps macros = new MacroProps();
69        ULocale locale = symbols.getULocale();
70
71        /////////////
72        // SYMBOLS //
73        /////////////
74
75        macros.symbols = symbols;
76
77        //////////////////
78        // PLURAL RULES //
79        //////////////////
80
81        macros.rules = properties.getPluralRules();
82
83        /////////////
84        // AFFIXES //
85        /////////////
86
87        AffixPatternProvider affixProvider;
88        if (properties.getCurrencyPluralInfo() == null) {
89            affixProvider = new PropertiesAffixPatternProvider(properties);
90        } else {
91            affixProvider = new CurrencyPluralInfoAffixProvider(properties.getCurrencyPluralInfo());
92        }
93        macros.affixProvider = affixProvider;
94
95        ///////////
96        // UNITS //
97        ///////////
98
99        boolean useCurrency = ((properties.getCurrency() != null) || properties.getCurrencyPluralInfo() != null
100                || properties.getCurrencyUsage() != null || affixProvider.hasCurrencySign());
101        Currency currency = CustomSymbolCurrency.resolve(properties.getCurrency(), locale, symbols);
102        CurrencyUsage currencyUsage = properties.getCurrencyUsage();
103        boolean explicitCurrencyUsage = currencyUsage != null;
104        if (!explicitCurrencyUsage) {
105            currencyUsage = CurrencyUsage.STANDARD;
106        }
107        if (useCurrency) {
108            macros.unit = currency;
109        }
110
111        ///////////////////////
112        // ROUNDING STRATEGY //
113        ///////////////////////
114
115        int maxInt = properties.getMaximumIntegerDigits();
116        int minInt = properties.getMinimumIntegerDigits();
117        int maxFrac = properties.getMaximumFractionDigits();
118        int minFrac = properties.getMinimumFractionDigits();
119        int minSig = properties.getMinimumSignificantDigits();
120        int maxSig = properties.getMaximumSignificantDigits();
121        BigDecimal roundingIncrement = properties.getRoundingIncrement();
122        MathContext mathContext = RoundingUtils.getMathContextOrUnlimited(properties);
123        boolean explicitMinMaxFrac = minFrac != -1 || maxFrac != -1;
124        boolean explicitMinMaxSig = minSig != -1 || maxSig != -1;
125        // Resolve min/max frac for currencies, required for the validation logic and for when minFrac or maxFrac was
126        // set (but not both) on a currency instance.
127        // NOTE: Increments are handled in "Rounder.constructCurrency()".
128        if (useCurrency) {
129            if (minFrac == -1 && maxFrac == -1) {
130                minFrac = currency.getDefaultFractionDigits(currencyUsage);
131                maxFrac = currency.getDefaultFractionDigits(currencyUsage);
132            } else if (minFrac == -1) {
133                minFrac = Math.min(maxFrac, currency.getDefaultFractionDigits(currencyUsage));
134            } else if (maxFrac == -1) {
135                maxFrac = Math.max(minFrac, currency.getDefaultFractionDigits(currencyUsage));
136            } else {
137                // No-op: user override for both minFrac and maxFrac
138            }
139        }
140        // Validate min/max int/frac.
141        // For backwards compatibility, minimum overrides maximum if the two conflict.
142        // The following logic ensures that there is always a minimum of at least one digit.
143        if (minInt == 0 && maxFrac != 0) {
144            // Force a digit after the decimal point.
145            minFrac = minFrac <= 0 ? 1 : minFrac;
146            maxFrac = maxFrac < 0 ? Integer.MAX_VALUE : maxFrac < minFrac ? minFrac : maxFrac;
147            minInt = 0;
148            maxInt = maxInt < 0 ? -1 : maxInt > RoundingUtils.MAX_INT_FRAC_SIG ? -1 : maxInt;
149        } else {
150            // Force a digit before the decimal point.
151            minFrac = minFrac < 0 ? 0 : minFrac;
152            maxFrac = maxFrac < 0 ? Integer.MAX_VALUE : maxFrac < minFrac ? minFrac : maxFrac;
153            minInt = minInt <= 0 ? 1 : minInt > RoundingUtils.MAX_INT_FRAC_SIG ? 1 : minInt;
154            maxInt = maxInt < 0 ? -1 : maxInt < minInt ? minInt : maxInt > RoundingUtils.MAX_INT_FRAC_SIG ? -1 : maxInt;
155        }
156        Rounder rounding = null;
157        if (explicitCurrencyUsage) {
158            rounding = Rounder.constructCurrency(currencyUsage).withCurrency(currency);
159        } else if (roundingIncrement != null) {
160            rounding = Rounder.constructIncrement(roundingIncrement);
161        } else if (explicitMinMaxSig) {
162            minSig = minSig < 1 ? 1 : minSig > RoundingUtils.MAX_INT_FRAC_SIG ? RoundingUtils.MAX_INT_FRAC_SIG : minSig;
163            maxSig = maxSig < 0 ? RoundingUtils.MAX_INT_FRAC_SIG
164                    : maxSig < minSig ? minSig
165                            : maxSig > RoundingUtils.MAX_INT_FRAC_SIG ? RoundingUtils.MAX_INT_FRAC_SIG : maxSig;
166            rounding = Rounder.constructSignificant(minSig, maxSig);
167        } else if (explicitMinMaxFrac) {
168            rounding = Rounder.constructFraction(minFrac, maxFrac);
169        } else if (useCurrency) {
170            rounding = Rounder.constructCurrency(currencyUsage);
171        }
172        if (rounding != null) {
173            rounding = rounding.withMode(mathContext);
174            macros.rounder = rounding;
175        }
176
177        ///////////////////
178        // INTEGER WIDTH //
179        ///////////////////
180
181        macros.integerWidth = IntegerWidth.zeroFillTo(minInt).truncateAt(maxInt);
182
183        ///////////////////////
184        // GROUPING STRATEGY //
185        ///////////////////////
186
187        int grouping1 = properties.getGroupingSize();
188        int grouping2 = properties.getSecondaryGroupingSize();
189        int minGrouping = properties.getMinimumGroupingDigits();
190        assert grouping1 >= -2; // value of -2 means to forward no grouping information
191        grouping1 = grouping1 > 0 ? grouping1 : grouping2 > 0 ? grouping2 : grouping1;
192        grouping2 = grouping2 > 0 ? grouping2 : grouping1;
193        // TODO: Is it important to handle minGrouping > 2?
194        macros.grouper = Grouper.getInstance((byte) grouping1, (byte) grouping2, minGrouping == 2);
195
196        /////////////
197        // PADDING //
198        /////////////
199
200        if (properties.getFormatWidth() != -1) {
201            macros.padder = new Padder(properties.getPadString(), properties.getFormatWidth(),
202                    properties.getPadPosition());
203        }
204
205        ///////////////////////////////
206        // DECIMAL MARK ALWAYS SHOWN //
207        ///////////////////////////////
208
209        macros.decimal = properties.getDecimalSeparatorAlwaysShown() ? DecimalSeparatorDisplay.ALWAYS
210                : DecimalSeparatorDisplay.AUTO;
211
212        ///////////////////////
213        // SIGN ALWAYS SHOWN //
214        ///////////////////////
215
216        macros.sign = properties.getSignAlwaysShown() ? SignDisplay.ALWAYS : SignDisplay.AUTO;
217
218        /////////////////////////
219        // SCIENTIFIC NOTATION //
220        /////////////////////////
221
222        if (properties.getMinimumExponentDigits() != -1) {
223            // Scientific notation is required.
224            // This whole section feels like a hack, but it is needed for regression tests.
225            // The mapping from property bag to scientific notation is nontrivial due to LDML rules.
226            if (maxInt > 8) {
227                // But #13110: The maximum of 8 digits has unknown origins and is not in the spec.
228                // If maxInt is greater than 8, it is set to minInt, even if minInt is greater than 8.
229                maxInt = minInt;
230                macros.integerWidth = IntegerWidth.zeroFillTo(minInt).truncateAt(maxInt);
231            } else if (maxInt > minInt && minInt > 1) {
232                // Bug #13289: if maxInt > minInt > 1, then minInt should be 1.
233                minInt = 1;
234                macros.integerWidth = IntegerWidth.zeroFillTo(minInt).truncateAt(maxInt);
235            }
236            int engineering = maxInt < 0 ? -1 : maxInt;
237            macros.notation = new ScientificNotation(
238                    // Engineering interval:
239                    engineering,
240                    // Enforce minimum integer digits (for patterns like "000.00E0"):
241                    (engineering == minInt),
242                    // Minimum exponent digits:
243                    properties.getMinimumExponentDigits(),
244                    // Exponent sign always shown:
245                    properties.getExponentSignAlwaysShown() ? SignDisplay.ALWAYS : SignDisplay.AUTO);
246            // Scientific notation also involves overriding the rounding mode.
247            // TODO: Overriding here is a bit of a hack. Should this logic go earlier?
248            if (macros.rounder instanceof FractionRounder) {
249                // For the purposes of rounding, get the original min/max int/frac, since the local variables
250                // have been manipulated for display purposes.
251                int minInt_ = properties.getMinimumIntegerDigits();
252                int minFrac_ = properties.getMinimumFractionDigits();
253                int maxFrac_ = properties.getMaximumFractionDigits();
254                if (minInt_ == 0 && maxFrac_ == 0) {
255                    // Patterns like "#E0" and "##E0", which mean no rounding!
256                    macros.rounder = Rounder.constructInfinite().withMode(mathContext);
257                } else if (minInt_ == 0 && minFrac_ == 0) {
258                    // Patterns like "#.##E0" (no zeros in the mantissa), which mean round to maxFrac+1
259                    macros.rounder = Rounder.constructSignificant(1, maxFrac_ + 1).withMode(mathContext);
260                } else {
261                    // All other scientific patterns, which mean round to minInt+maxFrac
262                    macros.rounder = Rounder.constructSignificant(minInt_ + minFrac_, minInt_ + maxFrac_)
263                            .withMode(mathContext);
264                }
265            }
266        }
267
268        //////////////////////
269        // COMPACT NOTATION //
270        //////////////////////
271
272        if (properties.getCompactStyle() != null) {
273            if (properties.getCompactCustomData() != null) {
274                macros.notation = new CompactNotation(properties.getCompactCustomData());
275            } else if (properties.getCompactStyle() == CompactStyle.LONG) {
276                macros.notation = Notation.compactLong();
277            } else {
278                macros.notation = Notation.compactShort();
279            }
280            // Do not forward the affix provider.
281            macros.affixProvider = null;
282        }
283
284        /////////////////
285        // MULTIPLIERS //
286        /////////////////
287
288        if (properties.getMagnitudeMultiplier() != 0) {
289            macros.multiplier = new MultiplierImpl(properties.getMagnitudeMultiplier());
290        } else if (properties.getMultiplier() != null) {
291            macros.multiplier = new MultiplierImpl(properties.getMultiplier());
292        }
293
294        //////////////////////
295        // PROPERTY EXPORTS //
296        //////////////////////
297
298        if (exportedProperties != null) {
299
300            exportedProperties.setMathContext(mathContext);
301            exportedProperties.setRoundingMode(mathContext.getRoundingMode());
302            exportedProperties.setMinimumIntegerDigits(minInt);
303            exportedProperties.setMaximumIntegerDigits(maxInt == -1 ? Integer.MAX_VALUE : maxInt);
304
305            Rounder rounding_;
306            if (rounding instanceof CurrencyRounder) {
307                rounding_ = ((CurrencyRounder) rounding).withCurrency(currency);
308            } else {
309                rounding_ = rounding;
310            }
311            int minFrac_ = minFrac;
312            int maxFrac_ = maxFrac;
313            int minSig_ = minSig;
314            int maxSig_ = maxSig;
315            BigDecimal increment_ = null;
316            if (rounding_ instanceof FractionRounderImpl) {
317                minFrac_ = ((FractionRounderImpl) rounding_).minFrac;
318                maxFrac_ = ((FractionRounderImpl) rounding_).maxFrac;
319            } else if (rounding_ instanceof IncrementRounderImpl) {
320                increment_ = ((IncrementRounderImpl) rounding_).increment;
321                minFrac_ = increment_.scale();
322                maxFrac_ = increment_.scale();
323            } else if (rounding_ instanceof SignificantRounderImpl) {
324                minSig_ = ((SignificantRounderImpl) rounding_).minSig;
325                maxSig_ = ((SignificantRounderImpl) rounding_).maxSig;
326            }
327
328            exportedProperties.setMinimumFractionDigits(minFrac_);
329            exportedProperties.setMaximumFractionDigits(maxFrac_);
330            exportedProperties.setMinimumSignificantDigits(minSig_);
331            exportedProperties.setMaximumSignificantDigits(maxSig_);
332            exportedProperties.setRoundingIncrement(increment_);
333        }
334
335        return macros;
336    }
337
338    private static class PropertiesAffixPatternProvider implements AffixPatternProvider {
339        private final String posPrefix;
340        private final String posSuffix;
341        private final String negPrefix;
342        private final String negSuffix;
343
344        public PropertiesAffixPatternProvider(DecimalFormatProperties properties) {
345            // There are two ways to set affixes in DecimalFormat: via the pattern string (applyPattern), and via the
346            // explicit setters (setPositivePrefix and friends).  The way to resolve the settings is as follows:
347            //
348            // 1) If the explicit setting is present for the field, use it.
349            // 2) Otherwise, follows UTS 35 rules based on the pattern string.
350            //
351            // Importantly, the explicit setters affect only the one field they override.  If you set the positive
352            // prefix, that should not affect the negative prefix.  Since it is impossible for the user of this class
353            // to know whether the origin for a string was the override or the pattern, we have to say that we always
354            // have a negative subpattern and perform all resolution logic here.
355
356            // Convenience: Extract the properties into local variables.
357            // Variables are named with three chars: [p/n][p/s][o/p]
358            // [p/n] => p for positive, n for negative
359            // [p/s] => p for prefix, s for suffix
360            // [o/p] => o for escaped custom override string, p for pattern string
361            String ppo = AffixUtils.escape(properties.getPositivePrefix());
362            String pso = AffixUtils.escape(properties.getPositiveSuffix());
363            String npo = AffixUtils.escape(properties.getNegativePrefix());
364            String nso = AffixUtils.escape(properties.getNegativeSuffix());
365            String ppp = properties.getPositivePrefixPattern();
366            String psp = properties.getPositiveSuffixPattern();
367            String npp = properties.getNegativePrefixPattern();
368            String nsp = properties.getNegativeSuffixPattern();
369
370            if (ppo != null) {
371                posPrefix = ppo;
372            } else if (ppp != null) {
373                posPrefix = ppp;
374            } else {
375                // UTS 35: Default positive prefix is empty string.
376                posPrefix = "";
377            }
378
379            if (pso != null) {
380                posSuffix = pso;
381            } else if (psp != null) {
382                posSuffix = psp;
383            } else {
384                // UTS 35: Default positive suffix is empty string.
385                posSuffix = "";
386            }
387
388            if (npo != null) {
389                negPrefix = npo;
390            } else if (npp != null) {
391                negPrefix = npp;
392            } else {
393                // UTS 35: Default negative prefix is "-" with positive prefix.
394                // Important: We prepend the "-" to the pattern, not the override!
395                negPrefix = ppp == null ? "-" : "-" + ppp;
396            }
397
398            if (nso != null) {
399                negSuffix = nso;
400            } else if (nsp != null) {
401                negSuffix = nsp;
402            } else {
403                // UTS 35: Default negative prefix is the positive prefix.
404                negSuffix = psp == null ? "" : psp;
405            }
406        }
407
408        @Override
409        public char charAt(int flags, int i) {
410            return getStringForFlags(flags).charAt(i);
411        }
412
413        @Override
414        public int length(int flags) {
415            return getStringForFlags(flags).length();
416        }
417
418        private String getStringForFlags(int flags) {
419            boolean prefix = (flags & Flags.PREFIX) != 0;
420            boolean negative = (flags & Flags.NEGATIVE_SUBPATTERN) != 0;
421            if (prefix && negative) {
422                return negPrefix;
423            } else if (prefix) {
424                return posPrefix;
425            } else if (negative) {
426                return negSuffix;
427            } else {
428                return posSuffix;
429            }
430        }
431
432        @Override
433        public boolean positiveHasPlusSign() {
434            return AffixUtils.containsType(posPrefix, AffixUtils.TYPE_PLUS_SIGN)
435                    || AffixUtils.containsType(posSuffix, AffixUtils.TYPE_PLUS_SIGN);
436        }
437
438        @Override
439        public boolean hasNegativeSubpattern() {
440            // See comments in the constructor for more information on why this is always true.
441            return true;
442        }
443
444        @Override
445        public boolean negativeHasMinusSign() {
446            return AffixUtils.containsType(negPrefix, AffixUtils.TYPE_MINUS_SIGN)
447                    || AffixUtils.containsType(negSuffix, AffixUtils.TYPE_MINUS_SIGN);
448        }
449
450        @Override
451        public boolean hasCurrencySign() {
452            return AffixUtils.hasCurrencySymbols(posPrefix) || AffixUtils.hasCurrencySymbols(posSuffix)
453                    || AffixUtils.hasCurrencySymbols(negPrefix) || AffixUtils.hasCurrencySymbols(negSuffix);
454        }
455
456        @Override
457        public boolean containsSymbolType(int type) {
458            return AffixUtils.containsType(posPrefix, type) || AffixUtils.containsType(posSuffix, type)
459                    || AffixUtils.containsType(negPrefix, type) || AffixUtils.containsType(negSuffix, type);
460        }
461    }
462
463    private static class CurrencyPluralInfoAffixProvider implements AffixPatternProvider {
464        private final AffixPatternProvider[] affixesByPlural;
465
466        public CurrencyPluralInfoAffixProvider(CurrencyPluralInfo cpi) {
467            affixesByPlural = new ParsedPatternInfo[StandardPlural.COUNT];
468            for (StandardPlural plural : StandardPlural.VALUES) {
469                affixesByPlural[plural.ordinal()] = PatternStringParser
470                        .parseToPatternInfo(cpi.getCurrencyPluralPattern(plural.getKeyword()));
471            }
472        }
473
474        @Override
475        public char charAt(int flags, int i) {
476            int pluralOrdinal = (flags & Flags.PLURAL_MASK);
477            return affixesByPlural[pluralOrdinal].charAt(flags, i);
478        }
479
480        @Override
481        public int length(int flags) {
482            int pluralOrdinal = (flags & Flags.PLURAL_MASK);
483            return affixesByPlural[pluralOrdinal].length(flags);
484        }
485
486        @Override
487        public boolean positiveHasPlusSign() {
488            return affixesByPlural[StandardPlural.OTHER.ordinal()].positiveHasPlusSign();
489        }
490
491        @Override
492        public boolean hasNegativeSubpattern() {
493            return affixesByPlural[StandardPlural.OTHER.ordinal()].hasNegativeSubpattern();
494        }
495
496        @Override
497        public boolean negativeHasMinusSign() {
498            return affixesByPlural[StandardPlural.OTHER.ordinal()].negativeHasMinusSign();
499        }
500
501        @Override
502        public boolean hasCurrencySign() {
503            return affixesByPlural[StandardPlural.OTHER.ordinal()].hasCurrencySign();
504        }
505
506        @Override
507        public boolean containsSymbolType(int type) {
508            return affixesByPlural[StandardPlural.OTHER.ordinal()].containsSymbolType(type);
509        }
510    }
511}
512