From d24c5401961efb072caf55751bba8fe49d57afd9 Mon Sep 17 00:00:00 2001 From: John Emmons Date: Sat, 27 Feb 2016 02:47:37 +0000 Subject: [PATCH] ICU-10910 Compact currency formatting support X-SVN-Rev: 38414 --- .../ibm/icu/text/CompactDecimalDataCache.java | 68 ++++-------- .../ibm/icu/text/CompactDecimalFormat.java | 101 ++++++++++++++---- .../test/format/CompactDecimalFormatTest.java | 85 ++++++++++++++- 3 files changed, 182 insertions(+), 72 deletions(-) diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalDataCache.java b/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalDataCache.java index 6af4eeacf26..6aa386bd1bb 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalDataCache.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalDataCache.java @@ -1,6 +1,6 @@ /* ******************************************************************************* - * Copyright (C) 2012-2013, International Business Machines Corporation and * + * Copyright (C) 2012-2016, International Business Machines Corporation and * * others. All Rights Reserved. * ******************************************************************************* */ @@ -25,9 +25,11 @@ class CompactDecimalDataCache { private static final String SHORT_STYLE = "short"; private static final String LONG_STYLE = "long"; + private static final String SHORT_CURRENCY_STYLE = "shortCurrency"; private static final String NUMBER_ELEMENTS = "NumberElements"; private static final String PATTERN_LONG_PATH = "patternsLong/decimalFormat"; private static final String PATTERNS_SHORT_PATH = "patternsShort/decimalFormat"; + private static final String PATTERNS_SHORT_CURRENCY_PATH = "patternsShort/currencyFormat"; static final String OTHER = "other"; @@ -65,7 +67,8 @@ class CompactDecimalDataCache { long[] divisors; Map units; - Data(long[] divisors, Map units) { + Data(long[] divisors, Map units) + { this.divisors = divisors; this.units = units; } @@ -73,32 +76,23 @@ class CompactDecimalDataCache { /** * DataBundle contains compact decimal data for all the styles in a particular - * locale. Currently available styles are short and long. + * locale. Currently available styles are short and long for decimals, and + * short only for currencies. * * @author Travis Keep */ static class DataBundle { Data shortData; Data longData; + Data shortCurrencyData; - DataBundle(Data shortData, Data longData) { + DataBundle(Data shortData, Data longData, Data shortCurrencyData) { this.shortData = shortData; this.longData = longData; + this.shortCurrencyData = shortCurrencyData; } } - private static enum QuoteState { - OUTSIDE, // Outside single quote - INSIDE_EMPTY, // Just inside single quote - INSIDE_FULL // Inside single quote along with characters - } - -// private static enum DataLocation { // Don't change order -// LOCAL, // In local numbering system -// LATIN, // In latin numbering system -// ROOT // In root locale -// } - private static enum UResFlags { ANY, // Any locale will do. NOT_ROOT // Locale cannot be root. @@ -140,10 +134,12 @@ class CompactDecimalDataCache { ICUResourceBundle shortDataBundle = null; ICUResourceBundle longDataBundle = null; + ICUResourceBundle shortCurrencyDataBundle = null; if (!LATIN_NUMBERING_SYSTEM.equals(numberingSystemName)) { ICUResourceBundle bundle = findWithFallback(r, numberingSystemName, UResFlags.NOT_ROOT); shortDataBundle = findWithFallback(bundle, PATTERNS_SHORT_PATH, UResFlags.NOT_ROOT); longDataBundle = findWithFallback(bundle, PATTERN_LONG_PATH, UResFlags.NOT_ROOT); + shortCurrencyDataBundle = findWithFallback(bundle, PATTERNS_SHORT_CURRENCY_PATH, UResFlags.NOT_ROOT); } // If we haven't found, look in latin numbering system. @@ -157,6 +153,10 @@ class CompactDecimalDataCache { } } } + if (shortCurrencyDataBundle == null) { + ICUResourceBundle bundle = getWithFallback(r, LATIN_NUMBERING_SYSTEM, UResFlags.ANY); + shortCurrencyDataBundle = getWithFallback(bundle, PATTERNS_SHORT_CURRENCY_PATH, UResFlags.ANY); + } Data shortData = loadStyle(shortDataBundle, ulocale, SHORT_STYLE); Data longData; if (longDataBundle == null) { @@ -164,7 +164,8 @@ class CompactDecimalDataCache { } else { longData = loadStyle(longDataBundle, ulocale, LONG_STYLE); } - return new DataBundle(shortData, longData); + Data shortCurrencyData = loadStyle(shortCurrencyDataBundle, ulocale, SHORT_CURRENCY_STYLE); + return new DataBundle(shortData, longData, shortCurrencyData); } /** @@ -348,8 +349,8 @@ class CompactDecimalDataCache { "' for variant '" +pluralVariant + "' for 10^" + idx + " in " + localeAndStyle(locale, style)); } - String prefix = fixQuotes(template.substring(0, firstIdx)); - String suffix = fixQuotes(template.substring(lastIdx + 1)); + String prefix = template.substring(0, firstIdx); + String suffix = template.substring(lastIdx + 1); saveUnit(new DecimalFormat.Unit(prefix, suffix), pluralVariant, idx, result.units); // If there is effectively no prefix or suffix, ignore the actual @@ -367,35 +368,6 @@ class CompactDecimalDataCache { return i - firstIdx; } - private static String fixQuotes(String prefixOrSuffix) { - StringBuilder result = new StringBuilder(); - int len = prefixOrSuffix.length(); - QuoteState state = QuoteState.OUTSIDE; - for (int idx = 0; idx < len; idx++) { - char ch = prefixOrSuffix.charAt(idx); - if (ch == '\'') { - if (state == QuoteState.INSIDE_EMPTY) { - result.append('\''); - } - } else { - result.append(ch); - } - - // Update state - switch (state) { - case OUTSIDE: - state = ch == '\'' ? QuoteState.INSIDE_EMPTY : QuoteState.OUTSIDE; - break; - case INSIDE_EMPTY: - case INSIDE_FULL: - state = ch == '\'' ? QuoteState.OUTSIDE : QuoteState.INSIDE_FULL; - break; - default: - throw new IllegalStateException(); - } - } - return result.toString(); - } /** * Returns locale and style. Used to form useful messages in thrown diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalFormat.java index 2fd8804d828..9751f3a9295 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalFormat.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalFormat.java @@ -1,6 +1,6 @@ /* ******************************************************************************* - * Copyright (C) 1996-2014, Google, International Business Machines Corporation and + * Copyright (C) 1996-2016, Google, International Business Machines Corporation and * others. All Rights Reserved. * ******************************************************************************* */ @@ -25,6 +25,8 @@ 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; @@ -41,6 +43,11 @@ import com.ibm.icu.util.ULocale; * 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. *

@@ -58,8 +65,11 @@ public class CompactDecimalFormat extends DecimalFormat { 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; @@ -117,8 +127,12 @@ public class CompactDecimalFormat extends DecimalFormat { 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); @@ -166,10 +180,12 @@ public class CompactDecimalFormat extends DecimalFormat { 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 { @@ -232,19 +248,7 @@ public class CompactDecimalFormat extends DecimalFormat { */ @Override public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) { - Output currencyUnit = new Output(); - Amount amount = toAmount(number, currencyUnit); - if (currencyUnit.value != null) { - currencyUnit.value.writePrefix(toAppendTo); - } - Unit unit = amount.getUnit(); - unit.writePrefix(toAppendTo); - super.format(amount.getQty(), toAppendTo, pos); - unit.writeSuffix(toAppendTo); - if (currencyUnit.value != null) { - currencyUnit.value.writeSuffix(toAppendTo); - } - return toAppendTo; + return format(number, null, toAppendTo, pos); } /** @@ -257,7 +261,7 @@ public class CompactDecimalFormat extends DecimalFormat { throw new IllegalArgumentException(); } Number number = (Number) obj; - Amount amount = toAmount(number.doubleValue(), null); + Amount amount = toAmount(number.doubleValue(), null, null); return super.formatToCharacterIterator(amount.getQty(), amount.getUnit()); } @@ -296,6 +300,15 @@ public class CompactDecimalFormat extends DecimalFormat { 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 + 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. @@ -317,9 +330,37 @@ public class CompactDecimalFormat extends DecimalFormat { } /* 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, Output currencyUnit) { + 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); @@ -328,7 +369,11 @@ public class CompactDecimalFormat extends DecimalFormat { if (base >= CompactDecimalDataCache.MAX_DIGITS) { base = CompactDecimalDataCache.MAX_DIGITS - 1; } - number /= divisor[base]; + 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); @@ -336,10 +381,11 @@ public class CompactDecimalFormat extends DecimalFormat { if (negative) { number = -number; } - return new Amount( - number, - CompactDecimalDataCache.getUnit(units, pluralVariant, base)); - + 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) { @@ -449,6 +495,17 @@ public class CompactDecimalFormat extends DecimalFormat { 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; diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/CompactDecimalFormatTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/CompactDecimalFormatTest.java index 2511cd0109c..bc28061e297 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/CompactDecimalFormatTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/CompactDecimalFormatTest.java @@ -1,6 +1,6 @@ /* ******************************************************************************* - * Copyright (C) 1996-2015, Google, International Business Machines Corporation and + * Copyright (C) 1996-2016, Google, International Business Machines Corporation and * others. All Rights Reserved. * ******************************************************************************* */ @@ -20,6 +20,8 @@ import com.ibm.icu.text.CompactDecimalFormat.CompactStyle; import com.ibm.icu.text.DecimalFormatSymbols; import com.ibm.icu.text.NumberFormat; import com.ibm.icu.text.PluralRules; +import com.ibm.icu.util.Currency; +import com.ibm.icu.util.CurrencyAmount; import com.ibm.icu.util.ULocale; public class CompactDecimalFormatTest extends TestFmwk { @@ -125,6 +127,52 @@ public class CompactDecimalFormatTest extends TestFmwk { {123456789012345f, "120兆"}, }; + Object[][] ChineseCurrencyTestData = { + // The first one should really have a ¥ in front, but the CLDR data is + // incorrect. See http://unicode.org/cldr/trac/ticket/9298 and update + // this test case when the CLDR ticket is fixed. + {new CurrencyAmount(1234f, Currency.getInstance("CNY")), "1200"}, + {new CurrencyAmount(12345f, Currency.getInstance("CNY")), "¥1.2万"}, + {new CurrencyAmount(123456f, Currency.getInstance("CNY")), "¥12万"}, + {new CurrencyAmount(1234567f, Currency.getInstance("CNY")), "¥120万"}, + {new CurrencyAmount(12345678f, Currency.getInstance("CNY")), "¥1200万"}, + {new CurrencyAmount(123456789f, Currency.getInstance("CNY")), "¥1.2亿"}, + {new CurrencyAmount(1234567890f, Currency.getInstance("CNY")), "¥12亿"}, + {new CurrencyAmount(12345678901f, Currency.getInstance("CNY")), "¥120亿"}, + {new CurrencyAmount(123456789012f, Currency.getInstance("CNY")), "¥1200亿"}, + {new CurrencyAmount(1234567890123f, Currency.getInstance("CNY")), "¥1.2兆"}, + {new CurrencyAmount(12345678901234f, Currency.getInstance("CNY")), "¥12兆"}, + {new CurrencyAmount(123456789012345f, Currency.getInstance("CNY")), "¥120兆"}, + }; + Object[][] GermanCurrencyTestData = { + {new CurrencyAmount(1234f, Currency.getInstance("EUR")), "1,2 Tsd. €"}, + {new CurrencyAmount(12345f, Currency.getInstance("EUR")), "12 Tsd. €"}, + {new CurrencyAmount(123456f, Currency.getInstance("EUR")), "120 Tsd. €"}, + {new CurrencyAmount(1234567f, Currency.getInstance("EUR")), "1,2 Mio. €"}, + {new CurrencyAmount(12345678f, Currency.getInstance("EUR")), "12 Mio. €"}, + {new CurrencyAmount(123456789f, Currency.getInstance("EUR")), "120 Mio. €"}, + {new CurrencyAmount(1234567890f, Currency.getInstance("EUR")), "1,2 Mrd. €"}, + {new CurrencyAmount(12345678901f, Currency.getInstance("EUR")), "12 Mrd. €"}, + {new CurrencyAmount(123456789012f, Currency.getInstance("EUR")), "120 Mrd. €"}, + {new CurrencyAmount(1234567890123f, Currency.getInstance("EUR")), "1,2 Bio. €"}, + {new CurrencyAmount(12345678901234f, Currency.getInstance("EUR")), "12 Bio. €"}, + {new CurrencyAmount(123456789012345f, Currency.getInstance("EUR")), "120 Bio. €"}, + }; + Object[][] EnglishCurrencyTestData = { + {new CurrencyAmount(1234f, Currency.getInstance("USD")), "$1.2K"}, + {new CurrencyAmount(12345f, Currency.getInstance("USD")), "$12K"}, + {new CurrencyAmount(123456f, Currency.getInstance("USD")), "$120K"}, + {new CurrencyAmount(1234567f, Currency.getInstance("USD")), "$1.2M"}, + {new CurrencyAmount(12345678f, Currency.getInstance("USD")), "$12M"}, + {new CurrencyAmount(123456789f, Currency.getInstance("USD")), "$120M"}, + {new CurrencyAmount(1234567890f, Currency.getInstance("USD")), "$1.2B"}, + {new CurrencyAmount(12345678901f, Currency.getInstance("USD")), "$12B"}, + {new CurrencyAmount(123456789012f, Currency.getInstance("USD")), "$120B"}, + {new CurrencyAmount(1234567890123f, Currency.getInstance("USD")), "$1.2T"}, + {new CurrencyAmount(12345678901234f, Currency.getInstance("USD")), "$12T"}, + {new CurrencyAmount(123456789012345f, Currency.getInstance("USD")), "$120T"}, + }; + Object[][] SwahiliTestData = { {1234f, "elfu\u00a01.2"}, {12345f, "elfu\u00a012"}, @@ -266,7 +314,15 @@ public class CompactDecimalFormatTest extends TestFmwk { checkLocale(ULocale.ENGLISH, CompactStyle.SHORT, EnglishTestData); } +// JCE: 2016-02-26: This test is logKnownIssue because CompactDecimalFormat cannot properly format +// negative quantities until we implement support for positive/negative subpatterns within CDF. +// So, in the meantime, we are making any format of a negative throw an UnsupportedOperationException +// as the original JavaDoc states. +// public void TestArabicLongStyle() { + if (logKnownIssue("12181","No support for negative numbers in CDF")) { + return; + } NumberFormat cdf = CompactDecimalFormat.getInstance( ULocale.forLanguageTag("ar"), CompactStyle.LONG); @@ -289,7 +345,15 @@ public class CompactDecimalFormatTest extends TestFmwk { checkLocale(ULocale.forLanguageTag("sr"), CompactStyle.LONG, SerbianTestDataLong); } +// JCE: 2016-02-26: This test is logKnownIssue because CompactDecimalFormat cannot properly format +// negative quantities until we implement support for positive/negative subpatterns within CDF. +// So, in the meantime, we are making any format of a negative throw an UnsupportedOperationException +// as the original JavaDoc states. +// public void TestSerbianLongNegative() { + if (logKnownIssue("12181","No support for negative numbers in CDF")) { + return; + } checkLocale(ULocale.forLanguageTag("sr"), CompactStyle.LONG, SerbianTestDataLongNegative); } @@ -300,11 +364,28 @@ public class CompactDecimalFormatTest extends TestFmwk { public void TestSwahiliShort() { checkLocale(ULocale.forLanguageTag("sw"), CompactStyle.SHORT, SwahiliTestData); } - +// JCE: 2016-02-26: This test is logKnownIssue because CompactDecimalFormat cannot properly format +// negative quantities until we implement support for positive/negative subpatterns within CDF. +// So, in the meantime, we are making any format of a negative throw an UnsupportedOperationException +// as the original JavaDoc states. +// public void TestSwahiliShortNegative() { + if (logKnownIssue("12181","No support for negative numbers in CDF")) { + return; + } checkLocale(ULocale.forLanguageTag("sw"), CompactStyle.SHORT, SwahiliTestDataNegative); } + public void TestEnglishCurrency() { + checkLocale(ULocale.ENGLISH, CompactStyle.SHORT, EnglishCurrencyTestData); + } + public void TestGermanCurrency() { + checkLocale(ULocale.GERMAN, CompactStyle.SHORT, GermanCurrencyTestData); + } + public void TestChineseCurrency() { + checkLocale(ULocale.CHINESE, CompactStyle.SHORT, ChineseCurrencyTestData); + } + public void TestFieldPosition() { CompactDecimalFormat cdf = getCDFInstance( ULocale.forLanguageTag("sw"), CompactStyle.SHORT); -- 2.40.0