1/*
2 *******************************************************************************
3 * Copyright (C) 1996-2014, Google, International Business Machines Corporation and
4 * others. All Rights Reserved.                                                *
5 *******************************************************************************
6 */
7
8package com.ibm.icu.text;
9
10import java.io.IOException;
11import java.io.NotSerializableException;
12import java.io.ObjectInputStream;
13import java.io.ObjectOutputStream;
14import java.math.BigDecimal;
15import java.math.BigInteger;
16import java.text.AttributedCharacterIterator;
17import java.text.FieldPosition;
18import java.text.ParsePosition;
19import java.util.Arrays;
20import java.util.Collection;
21import java.util.HashMap;
22import java.util.Locale;
23import java.util.Map;
24import java.util.Map.Entry;
25
26import com.ibm.icu.text.CompactDecimalDataCache.Data;
27import com.ibm.icu.text.PluralRules.FixedDecimal;
28import com.ibm.icu.util.Output;
29import com.ibm.icu.util.ULocale;
30
31/**
32 * The CompactDecimalFormat produces abbreviated numbers, suitable for display in environments will limited real estate.
33 * For example, 'Hits: 1.2B' instead of 'Hits: 1,200,000,000'. The format will be appropriate for the given language,
34 * such as "1,2 Mrd." for German.
35 * <p>
36 * For numbers under 1000 trillion (under 10^15, such as 123,456,789,012,345), the result will be short for supported
37 * languages. However, the result may sometimes exceed 7 characters, such as when there are combining marks or thin
38 * characters. In such cases, the visual width in fonts should still be short.
39 * <p>
40 * By default, there are 2 significant digits. After creation, if more than three significant digits are set (with
41 * setMaximumSignificantDigits), or if a fixed number of digits are set (with setMaximumIntegerDigits or
42 * setMaximumFractionDigits), then result may be wider.
43 * <p>
44 * At this time, negative numbers and parsing are not supported, and will produce an UnsupportedOperationException.
45 * Resetting the pattern prefixes or suffixes is not supported; the method calls are ignored.
46 * <p>
47 * Note that important methods, like setting the number of decimals, will be moved up from DecimalFormat to
48 * NumberFormat.
49 *
50 * @author markdavis
51 * @stable ICU 49
52 */
53public class CompactDecimalFormat extends DecimalFormat {
54
55    private static final long serialVersionUID = 4716293295276629682L;
56
57//    private static final int POSITIVE_PREFIX = 0, POSITIVE_SUFFIX = 1, AFFIX_SIZE = 2;
58    private static final CompactDecimalDataCache cache = new CompactDecimalDataCache();
59
60    private final Map<String, DecimalFormat.Unit[]> units;
61    private final long[] divisor;
62    private final Map<String, Unit> pluralToCurrencyAffixes;
63
64    // null if created internally using explicit prefixes and suffixes.
65    private final PluralRules pluralRules;
66
67    /**
68     * Style parameter for CompactDecimalFormat.
69     * @stable ICU 50
70     */
71    public enum CompactStyle {
72        /**
73         * Short version, like "1.2T"
74         * @stable ICU 50
75         */
76        SHORT,
77        /**
78         * Longer version, like "1.2 trillion", if available. May return same result as SHORT if not.
79         * @stable ICU 50
80         */
81        LONG
82    }
83
84    /**
85     * Create a CompactDecimalFormat appropriate for a locale. The result may
86     * be affected by the number system in the locale, such as ar-u-nu-latn.
87     *
88     * @param locale the desired locale
89     * @param style the compact style
90     * @stable ICU 50
91     */
92    public static CompactDecimalFormat getInstance(ULocale locale, CompactStyle style) {
93        return new CompactDecimalFormat(locale, style);
94    }
95
96    /**
97     * Create a CompactDecimalFormat appropriate for a locale. The result may
98     * be affected by the number system in the locale, such as ar-u-nu-latn.
99     *
100     * @param locale the desired locale
101     * @param style the compact style
102     * @stable ICU 50
103     */
104    public static CompactDecimalFormat getInstance(Locale locale, CompactStyle style) {
105        return new CompactDecimalFormat(ULocale.forLocale(locale), style);
106    }
107
108    /**
109     * The public mechanism is CompactDecimalFormat.getInstance().
110     *
111     * @param locale
112     *            the desired locale
113     * @param style
114     *            the compact style
115     */
116    CompactDecimalFormat(ULocale locale, CompactStyle style) {
117        this.pluralRules = PluralRules.forLocale(locale);
118        DecimalFormat format = (DecimalFormat) NumberFormat.getInstance(locale);
119        CompactDecimalDataCache.Data data = getData(locale, style);
120        this.units = data.units;
121        this.divisor = data.divisors;
122        pluralToCurrencyAffixes = null;
123
124//        DecimalFormat currencyFormat = (DecimalFormat) NumberFormat.getCurrencyInstance(locale);
125//        // TODO fix to use plural-dependent affixes
126//        Unit currency = new Unit(currencyFormat.getPositivePrefix(), currencyFormat.getPositiveSuffix());
127//        pluralToCurrencyAffixes = new HashMap<String,Unit>();
128//        for (String key : pluralRules.getKeywords()) {
129//            pluralToCurrencyAffixes.put(key, currency);
130//        }
131//        // TODO fix to get right symbol for the count
132
133        finishInit(style, format.toPattern(), format.getDecimalFormatSymbols());
134    }
135
136    /**
137     * Create a short number "from scratch". Intended for internal use. The prefix, suffix, and divisor arrays are
138     * parallel, and provide the information for each power of 10. When formatting a value, the correct power of 10 is
139     * found, then the value is divided by the divisor, and the prefix and suffix are set (using
140     * setPositivePrefix/Suffix).
141     *
142     * @param pattern
143     *            A number format pattern. Note that the prefix and suffix are discarded, and the decimals are
144     *            overridden by default.
145     * @param formatSymbols
146     *            Decimal format symbols, typically from a locale.
147     * @param style
148     *            compact style.
149     * @param divisor
150     *            An array of prefix values, one for each power of 10 from 0 to 14
151     * @param pluralAffixes
152     *            A map from plural categories to affixes.
153     * @param currencyAffixes
154     *            A map from plural categories to currency affixes.
155     * @param debugCreationErrors
156     *            A collection of strings for debugging. If null on input, then any errors found will be added to that
157     *            collection instead of throwing exceptions.
158     * @internal
159     * @deprecated This API is ICU internal only.
160     */
161    @Deprecated
162    public CompactDecimalFormat(String pattern, DecimalFormatSymbols formatSymbols,
163            CompactStyle style, PluralRules pluralRules,
164            long[] divisor, Map<String,String[][]> pluralAffixes, Map<String, String[]> currencyAffixes,
165            Collection<String> debugCreationErrors) {
166
167        this.pluralRules = pluralRules;
168        this.units = otherPluralVariant(pluralAffixes, divisor, debugCreationErrors);
169        if (!pluralRules.getKeywords().equals(this.units.keySet())) {
170            debugCreationErrors.add("Missmatch in pluralCategories, should be: " + pluralRules.getKeywords() + ", was actually " + this.units.keySet());
171        }
172        this.divisor = divisor.clone();
173        if (currencyAffixes == null) {
174            pluralToCurrencyAffixes = null;
175        } else {
176            pluralToCurrencyAffixes = new HashMap<String,Unit>();
177            for (Entry<String, String[]> s : currencyAffixes.entrySet()) {
178                String[] pair = s.getValue();
179                pluralToCurrencyAffixes.put(s.getKey(), new Unit(pair[0], pair[1]));
180            }
181        }
182        finishInit(style, pattern, formatSymbols);
183    }
184
185    private void finishInit(CompactStyle style, String pattern, DecimalFormatSymbols formatSymbols) {
186        applyPattern(pattern);
187        setDecimalFormatSymbols(formatSymbols);
188        setMaximumSignificantDigits(2); // default significant digits
189        setSignificantDigitsUsed(true);
190        if (style == CompactStyle.SHORT) {
191            setGroupingUsed(false);
192        }
193        setCurrency(null);
194    }
195
196    /**
197     * {@inheritDoc}
198     * @stable ICU 49
199     */
200    @Override
201    public boolean equals(Object obj) {
202        if (obj == null)
203            return false;
204        if (!super.equals(obj))
205            return false; // super does class check
206        CompactDecimalFormat other = (CompactDecimalFormat) obj;
207        return mapsAreEqual(units, other.units)
208                && Arrays.equals(divisor, other.divisor)
209                && (pluralToCurrencyAffixes == other.pluralToCurrencyAffixes
210                || pluralToCurrencyAffixes != null && pluralToCurrencyAffixes.equals(other.pluralToCurrencyAffixes))
211                && pluralRules.equals(other.pluralRules);
212    }
213
214    private boolean mapsAreEqual(
215            Map<String, DecimalFormat.Unit[]> lhs, Map<String, DecimalFormat.Unit[]> rhs) {
216        if (lhs.size() != rhs.size()) {
217            return false;
218        }
219        // For each MapEntry in lhs, see if there is a matching one in rhs.
220        for (Map.Entry<String, DecimalFormat.Unit[]> entry : lhs.entrySet()) {
221            DecimalFormat.Unit[] value = rhs.get(entry.getKey());
222            if (value == null || !Arrays.equals(entry.getValue(), value)) {
223                return false;
224            }
225        }
226        return true;
227    }
228
229    /**
230     * {@inheritDoc}
231     * @stable ICU 49
232     */
233    @Override
234    public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
235        Output<Unit> currencyUnit = new Output<Unit>();
236        Amount amount = toAmount(number, currencyUnit);
237        if (currencyUnit.value != null) {
238            currencyUnit.value.writePrefix(toAppendTo);
239        }
240        Unit unit = amount.getUnit();
241        unit.writePrefix(toAppendTo);
242        super.format(amount.getQty(), toAppendTo, pos);
243        unit.writeSuffix(toAppendTo);
244        if (currencyUnit.value != null) {
245            currencyUnit.value.writeSuffix(toAppendTo);
246        }
247        return toAppendTo;
248    }
249
250    /**
251     * {@inheritDoc}
252     * @stable ICU 50
253     */
254    @Override
255    public AttributedCharacterIterator formatToCharacterIterator(Object obj) {
256        if (!(obj instanceof Number)) {
257            throw new IllegalArgumentException();
258        }
259        Number number = (Number) obj;
260        Amount amount = toAmount(number.doubleValue(), null);
261        return super.formatToCharacterIterator(amount.getQty(), amount.getUnit());
262    }
263
264    /**
265     * {@inheritDoc}
266     * @stable ICU 49
267     */
268    @Override
269    public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) {
270        return format((double) number, toAppendTo, pos);
271    }
272
273    /**
274     * {@inheritDoc}
275     * @stable ICU 49
276     */
277    @Override
278    public StringBuffer format(BigInteger number, StringBuffer toAppendTo, FieldPosition pos) {
279        return format(number.doubleValue(), toAppendTo, pos);
280    }
281
282    /**
283     * {@inheritDoc}
284     * @stable ICU 49
285     */
286    @Override
287    public StringBuffer format(BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
288        return format(number.doubleValue(), toAppendTo, pos);
289    }
290
291    /**
292     * {@inheritDoc}
293     * @stable ICU 49
294     */
295    @Override
296    public StringBuffer format(com.ibm.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
297        return format(number.doubleValue(), toAppendTo, pos);
298    }
299
300    /**
301     * Parsing is currently unsupported, and throws an UnsupportedOperationException.
302     * @stable ICU 49
303     */
304    @Override
305    public Number parse(String text, ParsePosition parsePosition) {
306        throw new UnsupportedOperationException();
307    }
308
309    // DISALLOW Serialization, at least while draft
310
311    private void writeObject(ObjectOutputStream out) throws IOException {
312        throw new NotSerializableException();
313    }
314
315    private void readObject(ObjectInputStream in) throws IOException {
316        throw new NotSerializableException();
317    }
318
319    /* INTERNALS */
320
321
322    private Amount toAmount(double number, Output<Unit> currencyUnit) {
323        // We do this here so that the prefix or suffix we choose is always consistent
324        // with the rounding we do. This way, 999999 -> 1M instead of 1000K.
325        boolean negative = isNumberNegative(number);
326        number = adjustNumberAsInFormatting(number);
327        int base = number <= 1.0d ? 0 : (int) Math.log10(number);
328        if (base >= CompactDecimalDataCache.MAX_DIGITS) {
329            base = CompactDecimalDataCache.MAX_DIGITS - 1;
330        }
331        number /= divisor[base];
332        String pluralVariant = getPluralForm(getFixedDecimal(number, toDigitList(number)));
333        if (pluralToCurrencyAffixes != null && currencyUnit != null) {
334            currencyUnit.value = pluralToCurrencyAffixes.get(pluralVariant);
335        }
336        if (negative) {
337            number = -number;
338        }
339        return new Amount(
340                number,
341                CompactDecimalDataCache.getUnit(units, pluralVariant, base));
342
343    }
344
345    private void recordError(Collection<String> creationErrors, String errorMessage) {
346        if (creationErrors == null) {
347            throw new IllegalArgumentException(errorMessage);
348        }
349        creationErrors.add(errorMessage);
350    }
351
352    /**
353     * Manufacture the unit list from arrays
354     */
355    private Map<String, DecimalFormat.Unit[]> otherPluralVariant(Map<String, String[][]> pluralCategoryToPower10ToAffix,
356            long[] divisor, Collection<String> debugCreationErrors) {
357
358        // check for bad divisors
359        if (divisor.length < CompactDecimalDataCache.MAX_DIGITS) {
360            recordError(debugCreationErrors, "Must have at least " + CompactDecimalDataCache.MAX_DIGITS + " prefix items.");
361        }
362        long oldDivisor = 0;
363        for (int i = 0; i < divisor.length; ++i) {
364
365            // divisor must be a power of 10, and must be less than or equal to 10^i
366            int log = (int) Math.log10(divisor[i]);
367            if (log > i) {
368                recordError(debugCreationErrors, "Divisor[" + i + "] must be less than or equal to 10^" + i
369                        + ", but is: " + divisor[i]);
370            }
371            long roundTrip = (long) Math.pow(10.0d, log);
372            if (roundTrip != divisor[i]) {
373                recordError(debugCreationErrors, "Divisor[" + i + "] must be a power of 10, but is: " + divisor[i]);
374            }
375
376            if (divisor[i] < oldDivisor) {
377                recordError(debugCreationErrors, "Bad divisor, the divisor for 10E" + i + "(" + divisor[i]
378                        + ") is less than the divisor for the divisor for 10E" + (i - 1) + "(" + oldDivisor + ")");
379            }
380            oldDivisor = divisor[i];
381        }
382
383        Map<String, DecimalFormat.Unit[]> result = new HashMap<String, DecimalFormat.Unit[]>();
384        Map<String,Integer> seen = new HashMap<String,Integer>();
385
386        String[][] defaultPower10ToAffix = pluralCategoryToPower10ToAffix.get("other");
387
388        for (Entry<String, String[][]> pluralCategoryAndPower10ToAffix : pluralCategoryToPower10ToAffix.entrySet()) {
389            String pluralCategory = pluralCategoryAndPower10ToAffix.getKey();
390            String[][] power10ToAffix = pluralCategoryAndPower10ToAffix.getValue();
391
392            // we can't have one of the arrays be of different length
393            if (power10ToAffix.length != divisor.length) {
394                recordError(debugCreationErrors, "Prefixes & suffixes must be present for all divisors " + pluralCategory);
395            }
396            DecimalFormat.Unit[] units = new DecimalFormat.Unit[power10ToAffix.length];
397            for (int i = 0; i < power10ToAffix.length; i++) {
398                String[] pair = power10ToAffix[i];
399                if (pair == null) {
400                    pair = defaultPower10ToAffix[i];
401                }
402
403                // we can't have bad pair
404                if (pair.length != 2 || pair[0] == null || pair[1] == null) {
405                    recordError(debugCreationErrors, "Prefix or suffix is null for " + pluralCategory + ", " + i + ", " + Arrays.asList(pair));
406                    continue;
407                }
408
409                // we can't have two different indexes with the same display
410                int log = (int) Math.log10(divisor[i]);
411                String key = pair[0] + "\uFFFF" + pair[1] + "\uFFFF" + (i - log);
412                Integer old = seen.get(key);
413                if (old == null) {
414                    seen.put(key, i);
415                } else if (old != i) {
416                    recordError(debugCreationErrors, "Collision between values for " + i + " and " + old
417                            + " for [prefix/suffix/index-log(divisor)" + key.replace('\uFFFF', ';'));
418                }
419
420                units[i] = new Unit(pair[0], pair[1]);
421            }
422            result.put(pluralCategory, units);
423        }
424        return result;
425    }
426
427    private String getPluralForm(FixedDecimal fixedDecimal) {
428        if (pluralRules == null) {
429            return CompactDecimalDataCache.OTHER;
430        }
431        return pluralRules.select(fixedDecimal);
432    }
433
434    /**
435     * Gets the data for a particular locale and style. If style is unrecognized,
436     * we just return data for CompactStyle.SHORT.
437     * @param locale The locale.
438     * @param style The style.
439     * @return The data which must not be modified.
440     */
441    private Data getData(ULocale locale, CompactStyle style) {
442        CompactDecimalDataCache.DataBundle bundle = cache.get(locale);
443        switch (style) {
444        case SHORT:
445            return bundle.shortData;
446        case LONG:
447            return bundle.longData;
448        default:
449            return bundle.shortData;
450        }
451    }
452
453    private static class Amount {
454        private final double qty;
455        private final Unit unit;
456
457        public Amount(double qty, Unit unit) {
458            this.qty = qty;
459            this.unit = unit;
460        }
461
462        public double getQty() {
463            return qty;
464        }
465
466        public Unit getUnit() {
467            return unit;
468        }
469    }
470}
471