All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ibm.icu.text.CompactDecimalFormat Maven / Gradle / Ivy

There is a newer version: 4.0.52
Show newest version
/*
 *******************************************************************************
 * Copyright (C) 1996-2016, Google, International Business Machines Corporation and
 * others. All Rights Reserved.                                                *
 *******************************************************************************
 */

package com.ibm.icu.text;

import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.AttributedCharacterIterator;
import java.text.FieldPosition;
import java.text.ParsePosition;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;

import com.ibm.icu.text.CompactDecimalDataCache.Data;
import com.ibm.icu.text.PluralRules.FixedDecimal;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.CurrencyAmount;
import com.ibm.icu.util.Output;
import com.ibm.icu.util.ULocale;

/**
 * The CompactDecimalFormat produces abbreviated numbers, suitable for display in environments will limited real estate.
 * For example, 'Hits: 1.2B' instead of 'Hits: 1,200,000,000'. The format will be appropriate for the given language,
 * such as "1,2 Mrd." for German.
 * 

* For numbers under 1000 trillion (under 10^15, such as 123,456,789,012,345), the result will be short for supported * languages. However, the result may sometimes exceed 7 characters, such as when there are combining marks or thin * characters. In such cases, the visual width in fonts should still be short. *

* By default, there are 2 significant digits. After creation, if more than three significant digits are set (with * setMaximumSignificantDigits), or if a fixed number of digits are set (with setMaximumIntegerDigits or * setMaximumFractionDigits), then result may be wider. *

* The "short" style is also capable of formatting currency amounts, such as "$1.2M" instead of "$1,200,000.00" (English) or * "5,3 Mio. €" instead of "5.300.000,00 €" (German). Localized data concerning longer formats is not available yet in * the Unicode CLDR. Because of this, attempting to format a currency amount using the "long" style will produce * an UnsupportedOperationException. * * At this time, negative numbers and parsing are not supported, and will produce an UnsupportedOperationException. * Resetting the pattern prefixes or suffixes is not supported; the method calls are ignored. *

* Note that important methods, like setting the number of decimals, will be moved up from DecimalFormat to * NumberFormat. * * @author markdavis * @stable ICU 49 */ public class CompactDecimalFormat extends DecimalFormat { private static final long serialVersionUID = 4716293295276629682L; // private static final int POSITIVE_PREFIX = 0, POSITIVE_SUFFIX = 1, AFFIX_SIZE = 2; private static final CompactDecimalDataCache cache = new CompactDecimalDataCache(); private final Map units; private final Map currencyUnits; private final long[] divisor; private final long[] currencyDivisor; private final Map pluralToCurrencyAffixes; private CompactStyle style; // null if created internally using explicit prefixes and suffixes. private final PluralRules pluralRules; /** * Style parameter for CompactDecimalFormat. * @stable ICU 50 */ public enum CompactStyle { /** * Short version, like "1.2T" * @stable ICU 50 */ SHORT, /** * Longer version, like "1.2 trillion", if available. May return same result as SHORT if not. * @stable ICU 50 */ LONG } /** * Create a CompactDecimalFormat appropriate for a locale. The result may * be affected by the number system in the locale, such as ar-u-nu-latn. * * @param locale the desired locale * @param style the compact style * @stable ICU 50 */ public static CompactDecimalFormat getInstance(ULocale locale, CompactStyle style) { return new CompactDecimalFormat(locale, style); } /** * Create a CompactDecimalFormat appropriate for a locale. The result may * be affected by the number system in the locale, such as ar-u-nu-latn. * * @param locale the desired locale * @param style the compact style * @stable ICU 50 */ public static CompactDecimalFormat getInstance(Locale locale, CompactStyle style) { return new CompactDecimalFormat(ULocale.forLocale(locale), style); } /** * The public mechanism is CompactDecimalFormat.getInstance(). * * @param locale * the desired locale * @param style * the compact style */ CompactDecimalFormat(ULocale locale, CompactStyle style) { this.pluralRules = PluralRules.forLocale(locale); DecimalFormat format = (DecimalFormat) NumberFormat.getInstance(locale); CompactDecimalDataCache.Data data = getData(locale, style); CompactDecimalDataCache.Data currencyData = getCurrencyData(locale); this.units = data.units; this.divisor = data.divisors; this.currencyUnits = currencyData.units; this.currencyDivisor = currencyData.divisors; this.style = style; pluralToCurrencyAffixes = null; // DecimalFormat currencyFormat = (DecimalFormat) NumberFormat.getCurrencyInstance(locale); // // TODO fix to use plural-dependent affixes // Unit currency = new Unit(currencyFormat.getPositivePrefix(), currencyFormat.getPositiveSuffix()); // pluralToCurrencyAffixes = new HashMap(); // for (String key : pluralRules.getKeywords()) { // pluralToCurrencyAffixes.put(key, currency); // } // // TODO fix to get right symbol for the count finishInit(style, format.toPattern(), format.getDecimalFormatSymbols()); } /** * Create a short number "from scratch". Intended for internal use. The prefix, suffix, and divisor arrays are * parallel, and provide the information for each power of 10. When formatting a value, the correct power of 10 is * found, then the value is divided by the divisor, and the prefix and suffix are set (using * setPositivePrefix/Suffix). * * @param pattern * A number format pattern. Note that the prefix and suffix are discarded, and the decimals are * overridden by default. * @param formatSymbols * Decimal format symbols, typically from a locale. * @param style * compact style. * @param divisor * An array of prefix values, one for each power of 10 from 0 to 14 * @param pluralAffixes * A map from plural categories to affixes. * @param currencyAffixes * A map from plural categories to currency affixes. * @param debugCreationErrors * A collection of strings for debugging. If null on input, then any errors found will be added to that * collection instead of throwing exceptions. * @internal * @deprecated This API is ICU internal only. */ @Deprecated public CompactDecimalFormat(String pattern, DecimalFormatSymbols formatSymbols, CompactStyle style, PluralRules pluralRules, long[] divisor, Map pluralAffixes, Map currencyAffixes, Collection debugCreationErrors) { this.pluralRules = pluralRules; this.units = otherPluralVariant(pluralAffixes, divisor, debugCreationErrors); this.currencyUnits = otherPluralVariant(pluralAffixes, divisor, debugCreationErrors); if (!pluralRules.getKeywords().equals(this.units.keySet())) { debugCreationErrors.add("Missmatch in pluralCategories, should be: " + pluralRules.getKeywords() + ", was actually " + this.units.keySet()); } this.divisor = divisor.clone(); this.currencyDivisor = divisor.clone(); if (currencyAffixes == null) { pluralToCurrencyAffixes = null; } else { pluralToCurrencyAffixes = new HashMap(); for (Entry s : currencyAffixes.entrySet()) { String[] pair = s.getValue(); pluralToCurrencyAffixes.put(s.getKey(), new Unit(pair[0], pair[1])); } } finishInit(style, pattern, formatSymbols); } private void finishInit(CompactStyle style, String pattern, DecimalFormatSymbols formatSymbols) { applyPattern(pattern); setDecimalFormatSymbols(formatSymbols); setMaximumSignificantDigits(2); // default significant digits setSignificantDigitsUsed(true); if (style == CompactStyle.SHORT) { setGroupingUsed(false); } setCurrency(null); } /** * {@inheritDoc} * @stable ICU 49 */ @Override public boolean equals(Object obj) { if (obj == null) return false; if (!super.equals(obj)) return false; // super does class check CompactDecimalFormat other = (CompactDecimalFormat) obj; return mapsAreEqual(units, other.units) && Arrays.equals(divisor, other.divisor) && (pluralToCurrencyAffixes == other.pluralToCurrencyAffixes || pluralToCurrencyAffixes != null && pluralToCurrencyAffixes.equals(other.pluralToCurrencyAffixes)) && pluralRules.equals(other.pluralRules); } private boolean mapsAreEqual( Map lhs, Map rhs) { if (lhs.size() != rhs.size()) { return false; } // For each MapEntry in lhs, see if there is a matching one in rhs. for (Map.Entry entry : lhs.entrySet()) { DecimalFormat.Unit[] value = rhs.get(entry.getKey()); if (value == null || !Arrays.equals(entry.getValue(), value)) { return false; } } return true; } /** * {@inheritDoc} * @stable ICU 49 */ @Override public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) { return format(number, null, toAppendTo, pos); } /** * {@inheritDoc} * @stable ICU 50 */ @Override public AttributedCharacterIterator formatToCharacterIterator(Object obj) { if (!(obj instanceof Number)) { throw new IllegalArgumentException(); } Number number = (Number) obj; Amount amount = toAmount(number.doubleValue(), null, null); return super.formatToCharacterIterator(amount.getQty(), amount.getUnit()); } /** * {@inheritDoc} * @stable ICU 49 */ @Override public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) { return format((double) number, toAppendTo, pos); } /** * {@inheritDoc} * @stable ICU 49 */ @Override public StringBuffer format(BigInteger number, StringBuffer toAppendTo, FieldPosition pos) { return format(number.doubleValue(), toAppendTo, pos); } /** * {@inheritDoc} * @stable ICU 49 */ @Override public StringBuffer format(BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) { return format(number.doubleValue(), toAppendTo, pos); } /** * {@inheritDoc} * @stable ICU 49 */ @Override public StringBuffer format(com.ibm.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) { return format(number.doubleValue(), toAppendTo, pos); } /** * {@inheritDoc} * @internal ICU 57 technology preview * @deprecated This API might change or be removed in a future release. */ @Override @Deprecated public StringBuffer format(CurrencyAmount currAmt, StringBuffer toAppendTo, FieldPosition pos) { return format(currAmt.getNumber().doubleValue(), currAmt.getCurrency(), toAppendTo, pos); } /** * Parsing is currently unsupported, and throws an UnsupportedOperationException. * @stable ICU 49 */ @Override public Number parse(String text, ParsePosition parsePosition) { throw new UnsupportedOperationException(); } // DISALLOW Serialization, at least while draft private void writeObject(ObjectOutputStream out) throws IOException { throw new NotSerializableException(); } private void readObject(ObjectInputStream in) throws IOException { throw new NotSerializableException(); } /* INTERNALS */ private StringBuffer format(double number, Currency curr, StringBuffer toAppendTo, FieldPosition pos) { if (number < 0 || (curr != null && style == CompactStyle.LONG)) { throw new UnsupportedOperationException(); } Output currencyUnit = new Output(); Amount amount = toAmount(number, curr, currencyUnit); if (currencyUnit.value != null) { currencyUnit.value.writePrefix(toAppendTo); } Unit unit = amount.getUnit(); String originalPattern = this.toPattern(); StringBuffer newPattern = new StringBuffer(); unit.writePrefix(newPattern); newPattern.append(this.toPattern()); unit.writeSuffix(newPattern); applyPattern(newPattern.toString()); if (curr == null) { super.format(amount.getQty(), toAppendTo, pos); } else { CurrencyAmount currAmt = new CurrencyAmount(amount.getQty(),curr); super.format(currAmt, toAppendTo, pos); } applyPattern(originalPattern); if (currencyUnit.value != null) { currencyUnit.value.writeSuffix(toAppendTo); } return toAppendTo; } private Amount toAmount(double number, Currency curr, Output currencyUnit) { // We do this here so that the prefix or suffix we choose is always consistent // with the rounding we do. This way, 999999 -> 1M instead of 1000K. boolean negative = isNumberNegative(number); number = adjustNumberAsInFormatting(number); int base = number <= 1.0d ? 0 : (int) Math.log10(number); if (base >= CompactDecimalDataCache.MAX_DIGITS) { base = CompactDecimalDataCache.MAX_DIGITS - 1; } if (curr != null) { number /= currencyDivisor[base]; } else { number /= divisor[base]; } String pluralVariant = getPluralForm(getFixedDecimal(number, toDigitList(number))); if (pluralToCurrencyAffixes != null && currencyUnit != null) { currencyUnit.value = pluralToCurrencyAffixes.get(pluralVariant); } if (negative) { number = -number; } if ( curr != null ) { return new Amount(number, CompactDecimalDataCache.getUnit(currencyUnits, pluralVariant, base)); } else { return new Amount(number, CompactDecimalDataCache.getUnit(units, pluralVariant, base)); } } private void recordError(Collection creationErrors, String errorMessage) { if (creationErrors == null) { throw new IllegalArgumentException(errorMessage); } creationErrors.add(errorMessage); } /** * Manufacture the unit list from arrays */ private Map otherPluralVariant(Map pluralCategoryToPower10ToAffix, long[] divisor, Collection debugCreationErrors) { // check for bad divisors if (divisor.length < CompactDecimalDataCache.MAX_DIGITS) { recordError(debugCreationErrors, "Must have at least " + CompactDecimalDataCache.MAX_DIGITS + " prefix items."); } long oldDivisor = 0; for (int i = 0; i < divisor.length; ++i) { // divisor must be a power of 10, and must be less than or equal to 10^i int log = (int) Math.log10(divisor[i]); if (log > i) { recordError(debugCreationErrors, "Divisor[" + i + "] must be less than or equal to 10^" + i + ", but is: " + divisor[i]); } long roundTrip = (long) Math.pow(10.0d, log); if (roundTrip != divisor[i]) { recordError(debugCreationErrors, "Divisor[" + i + "] must be a power of 10, but is: " + divisor[i]); } if (divisor[i] < oldDivisor) { recordError(debugCreationErrors, "Bad divisor, the divisor for 10E" + i + "(" + divisor[i] + ") is less than the divisor for the divisor for 10E" + (i - 1) + "(" + oldDivisor + ")"); } oldDivisor = divisor[i]; } Map result = new HashMap(); Map seen = new HashMap(); String[][] defaultPower10ToAffix = pluralCategoryToPower10ToAffix.get("other"); for (Entry pluralCategoryAndPower10ToAffix : pluralCategoryToPower10ToAffix.entrySet()) { String pluralCategory = pluralCategoryAndPower10ToAffix.getKey(); String[][] power10ToAffix = pluralCategoryAndPower10ToAffix.getValue(); // we can't have one of the arrays be of different length if (power10ToAffix.length != divisor.length) { recordError(debugCreationErrors, "Prefixes & suffixes must be present for all divisors " + pluralCategory); } DecimalFormat.Unit[] units = new DecimalFormat.Unit[power10ToAffix.length]; for (int i = 0; i < power10ToAffix.length; i++) { String[] pair = power10ToAffix[i]; if (pair == null) { pair = defaultPower10ToAffix[i]; } // we can't have bad pair if (pair.length != 2 || pair[0] == null || pair[1] == null) { recordError(debugCreationErrors, "Prefix or suffix is null for " + pluralCategory + ", " + i + ", " + Arrays.asList(pair)); continue; } // we can't have two different indexes with the same display int log = (int) Math.log10(divisor[i]); String key = pair[0] + "\uFFFF" + pair[1] + "\uFFFF" + (i - log); Integer old = seen.get(key); if (old == null) { seen.put(key, i); } else if (old != i) { recordError(debugCreationErrors, "Collision between values for " + i + " and " + old + " for [prefix/suffix/index-log(divisor)" + key.replace('\uFFFF', ';')); } units[i] = new Unit(pair[0], pair[1]); } result.put(pluralCategory, units); } return result; } private String getPluralForm(FixedDecimal fixedDecimal) { if (pluralRules == null) { return CompactDecimalDataCache.OTHER; } return pluralRules.select(fixedDecimal); } /** * Gets the data for a particular locale and style. If style is unrecognized, * we just return data for CompactStyle.SHORT. * @param locale The locale. * @param style The style. * @return The data which must not be modified. */ private Data getData(ULocale locale, CompactStyle style) { CompactDecimalDataCache.DataBundle bundle = cache.get(locale); switch (style) { case SHORT: return bundle.shortData; case LONG: return bundle.longData; default: return bundle.shortData; } } /** * Gets the currency data for a particular locale. * Currently only short currency format is supported, since that is * the only form in CLDR. * @param locale The locale. * @return The data which must not be modified. */ private Data getCurrencyData(ULocale locale) { CompactDecimalDataCache.DataBundle bundle = cache.get(locale); return bundle.shortCurrencyData; } private static class Amount { private final double qty; private final Unit unit; public Amount(double qty, Unit unit) { this.qty = qty; this.unit = unit; } public double getQty() { return qty; } public Unit getUnit() { return unit; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy