ICU-12422 Fixing affixes for CompactDecimalFormat for locales with different positive/negative formats

X-SVN-Rev: 39208
This commit is contained in:
Shane Carr 2016-09-13 19:01:56 +00:00
parent 7429cff9c7
commit b2b4154a9d
3 changed files with 174 additions and 90 deletions

View file

@ -24,6 +24,7 @@ import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import com.ibm.icu.text.CompactDecimalDataCache.Data;
import com.ibm.icu.text.PluralRules.FixedDecimal;
@ -49,7 +50,7 @@ import com.ibm.icu.util.ULocale;
* "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.
* <p>
@ -136,7 +137,7 @@ public class CompactDecimalFormat extends DecimalFormat {
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());
@ -145,7 +146,7 @@ public class CompactDecimalFormat extends DecimalFormat {
// pluralToCurrencyAffixes.put(key, currency);
// }
// // TODO fix to get right symbol for the count
finishInit(style, format.toPattern(), format.getDecimalFormatSymbols());
}
@ -167,7 +168,7 @@ public class CompactDecimalFormat extends DecimalFormat {
* @param pluralAffixes
* A map from plural categories to affixes.
* @param currencyAffixes
* A map from plural categories to currency affixes.
* 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.
@ -175,11 +176,11 @@ public class CompactDecimalFormat extends DecimalFormat {
* @deprecated This API is ICU internal only.
*/
@Deprecated
public CompactDecimalFormat(String pattern, DecimalFormatSymbols formatSymbols,
public CompactDecimalFormat(String pattern, DecimalFormatSymbols formatSymbols,
CompactStyle style, PluralRules pluralRules,
long[] divisor, Map<String,String[][]> pluralAffixes, Map<String, String[]> currencyAffixes,
long[] divisor, Map<String,String[][]> pluralAffixes, Map<String, String[]> currencyAffixes,
Collection<String> debugCreationErrors) {
this.pluralRules = pluralRules;
this.units = otherPluralVariant(pluralAffixes, divisor, debugCreationErrors);
this.currencyUnits = otherPluralVariant(pluralAffixes, divisor, debugCreationErrors);
@ -224,8 +225,8 @@ public class CompactDecimalFormat extends DecimalFormat {
CompactDecimalFormat other = (CompactDecimalFormat) obj;
return mapsAreEqual(units, other.units)
&& Arrays.equals(divisor, other.divisor)
&& (pluralToCurrencyAffixes == other.pluralToCurrencyAffixes
|| pluralToCurrencyAffixes != null && pluralToCurrencyAffixes.equals(other.pluralToCurrencyAffixes))
&& (pluralToCurrencyAffixes == other.pluralToCurrencyAffixes
|| pluralToCurrencyAffixes != null && pluralToCurrencyAffixes.equals(other.pluralToCurrencyAffixes))
&& pluralRules.equals(other.pluralRules);
}
@ -334,35 +335,80 @@ 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();
if (curr != null && style == CompactStyle.LONG) {
throw new UnsupportedOperationException("CompactDecimalFormat does not support LONG style for currency.");
}
// Compute the scaled amount, prefix, and suffix appropriate for the number's magnitude.
Output<Unit> currencyUnit = new Output<Unit>();
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);
// Note that currencyUnit is a remnant. In almost all cases, it will be null.
StringBuffer prefix = new StringBuffer();
StringBuffer suffix = new StringBuffer();
if (currencyUnit.value != null) {
currencyUnit.value.writeSuffix(toAppendTo);
currencyUnit.value.writePrefix(prefix);
}
unit.writePrefix(prefix);
unit.writeSuffix(suffix);
if (currencyUnit.value != null) {
currencyUnit.value.writeSuffix(suffix);
}
if (curr == null) {
// Prevent locking when not formatting a currency number.
toAppendTo.append(escape(prefix.toString()));
super.format(amount.getQty(), toAppendTo, pos);
toAppendTo.append(escape(suffix.toString()));
} else {
// To perform the formatting, we set this DecimalFormat's pattern to have the correct prefix, suffix,
// and currency, and then reset it back to what it was before.
// This has to be synchronized since this information is held in the state of the DecimalFormat object.
synchronized(this) {
String originalPattern = this.toPattern();
Currency originalCurrency = this.getCurrency();
StringBuffer newPattern = new StringBuffer();
// Write prefixes and suffixes to the pattern. Note that we have to apply it to both halves of a
// positive/negative format (separated by ';')
int semicolonPos = originalPattern.indexOf(';');
newPattern.append(prefix);
if (semicolonPos != -1) {
newPattern.append(originalPattern, 0, semicolonPos);
newPattern.append(suffix);
newPattern.append(';');
newPattern.append(prefix);
}
newPattern.append(originalPattern, semicolonPos + 1, originalPattern.length());
newPattern.append(suffix);
// Overwrite the pattern and currency.
setCurrency(curr);
applyPattern(newPattern.toString());
// Actually perform the formatting.
super.format(amount.getQty(), toAppendTo, pos);
// Reset the pattern and currency.
setCurrency(originalCurrency);
applyPattern(originalPattern);
}
}
return toAppendTo;
}
private static final Pattern UNESCAPE_QUOTE = Pattern.compile("((?<!'))'");
private static String escape(String string) {
if (string.indexOf('\'') >= 0) {
return UNESCAPE_QUOTE.matcher(string).replaceAll("$1");
}
return string;
}
private Amount toAmount(double number, Currency curr, Output<Unit> 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.
@ -401,7 +447,7 @@ public class CompactDecimalFormat extends DecimalFormat {
/**
* Manufacture the unit list from arrays
*/
private Map<String, DecimalFormat.Unit[]> otherPluralVariant(Map<String, String[][]> pluralCategoryToPower10ToAffix,
private Map<String, DecimalFormat.Unit[]> otherPluralVariant(Map<String, String[][]> pluralCategoryToPower10ToAffix,
long[] divisor, Collection<String> debugCreationErrors) {
// check for bad divisors
@ -431,7 +477,7 @@ public class CompactDecimalFormat extends DecimalFormat {
Map<String, DecimalFormat.Unit[]> result = new HashMap<String, DecimalFormat.Unit[]>();
Map<String,Integer> seen = new HashMap<String,Integer>();
String[][] defaultPower10ToAffix = pluralCategoryToPower10ToAffix.get("other");
for (Entry<String, String[][]> pluralCategoryAndPower10ToAffix : pluralCategoryToPower10ToAffix.entrySet()) {

View file

@ -232,7 +232,7 @@ public abstract class NumberFormat extends UFormat {
* negative values (e.g. minus sign).
* Overrides any style specified using -cf- key in locale.
* @draft ICU 56
* @provisional This API might change or be removed in a future release.
* @provisional This API might change or be removed in a future release.
*/
public static final int STANDARDCURRENCYSTYLE = 9;
@ -251,7 +251,7 @@ public abstract class NumberFormat extends UFormat {
* @stable ICU 2.0
*/
public static final int FRACTION_FIELD = 1;
/**
* Formats a number and appends the resulting text to the given string buffer.
* {@icunote} recognizes <code>BigInteger</code>
@ -402,11 +402,13 @@ public abstract class NumberFormat extends UFormat {
StringBuffer toAppendTo,
FieldPosition pos) {
// Default implementation -- subclasses may override
Currency save = getCurrency(), curr = currAmt.getCurrency();
boolean same = curr.equals(save);
if (!same) setCurrency(curr);
format(currAmt.getNumber(), toAppendTo, pos);
if (!same) setCurrency(save);
synchronized(this) {
Currency save = getCurrency(), curr = currAmt.getCurrency();
boolean same = curr.equals(save);
if (!same) setCurrency(curr);
format(currAmt.getNumber(), toAppendTo, pos);
if (!same) setCurrency(save);
}
return toAppendTo;
}
@ -528,9 +530,9 @@ public abstract class NumberFormat extends UFormat {
/**
* {@icu} Set a particular DisplayContext value in the formatter,
* such as CAPITALIZATION_FOR_STANDALONE.
*
* @param context The DisplayContext value to set.
* such as CAPITALIZATION_FOR_STANDALONE.
*
* @param context The DisplayContext value to set.
* @stable ICU 53
*/
public void setContext(DisplayContext context) {
@ -542,7 +544,7 @@ public abstract class NumberFormat extends UFormat {
/**
* {@icu} Get the formatter's DisplayContext value for the specified DisplayContext.Type,
* such as CAPITALIZATION.
*
*
* @param type the DisplayContext.Type whose value to return
* @return the current DisplayContext setting for the specified type
* @stable ICU 53
@ -1000,11 +1002,11 @@ public abstract class NumberFormat extends UFormat {
* {@icu} Registers a new NumberFormatFactory. The factory is adopted by
* the service and must not be modified. The returned object is a
* key that can be used to unregister this factory.
*
*
* <p>Because ICU may choose to cache NumberFormat objects internally, this must
* be called at application startup, prior to any calls to
* NumberFormat.getInstance to avoid undefined behavior.
*
*
* @param factory the factory to register
* @return a key with which to unregister the factory
* @stable ICU 2.6
@ -1258,7 +1260,7 @@ public abstract class NumberFormat extends UFormat {
public Currency getCurrency() {
return currency;
}
/**
* Returns the currency in effect for this formatter. Subclasses
* should override this method as needed. Unlike getCurrency(),
@ -1414,7 +1416,7 @@ public abstract class NumberFormat extends UFormat {
f.setDecimalSeparatorAlwaysShown(false);
f.setParseIntegerOnly(true);
}
if (choice == CASHCURRENCYSTYLE) {
f.setCurrencyUsage(CurrencyUsage.CASH);
}

View file

@ -17,7 +17,6 @@ import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map;
import org.junit.Ignore;
import org.junit.Test;
import com.ibm.icu.dev.test.TestFmwk;
@ -129,7 +128,7 @@ public class CompactDecimalFormatTest extends TestFmwk {
};
Object[][] ChineseCurrencyTestData = {
// The first one should really have a in front, but the CLDR data is
// 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"},
@ -245,18 +244,18 @@ public class CompactDecimalFormatTest extends TestFmwk {
public void TestACoreCompactFormat() {
Map<String,String[][]> affixes = new HashMap();
affixes.put("one", new String[][] {
{"","",}, {"","",}, {"","",},
{"","",}, {"","",}, {"","",},
{"","K"}, {"","K"}, {"","K"},
{"","M"}, {"","M"}, {"","M"},
{"","B"}, {"","B"}, {"","B"},
{"","T"}, {"","T"}, {"","T"},
{"","B"}, {"","B"}, {"","B"},
{"","T"}, {"","T"}, {"","T"},
});
affixes.put("other", new String[][] {
{"","",}, {"","",}, {"","",},
{"","",}, {"","",}, {"","",},
{"","Ks"}, {"","Ks"}, {"","Ks"},
{"","Ms"}, {"","Ms"}, {"","Ms"},
{"","Bs"}, {"","Bs"}, {"","Bs"},
{"","Ts"}, {"","Ts"}, {"","Ts"},
{"","Bs"}, {"","Bs"}, {"","Bs"},
{"","Ts"}, {"","Ts"}, {"","Ts"},
});
Map<String,String[]> currencyAffixes = new HashMap();
@ -264,10 +263,10 @@ public class CompactDecimalFormatTest extends TestFmwk {
currencyAffixes.put("other", new String[] {"", "$s"});
long[] divisors = new long[] {
0,0,0,
1000, 1000, 1000,
1000000, 1000000, 1000000,
1000000000L, 1000000000L, 1000000000L,
0,0,0,
1000, 1000, 1000,
1000000, 1000000, 1000000,
1000000000L, 1000000000L, 1000000000L,
1000000000000L, 1000000000000L, 1000000000000L};
checkCore(affixes, null, divisors, TestACoreCompactFormatList);
checkCore(affixes, currencyAffixes, divisors, TestACoreCompactFormatListCurrency);
@ -276,7 +275,7 @@ public class CompactDecimalFormatTest extends TestFmwk {
private void checkCore(Map<String, String[][]> affixes, Map<String, String[]> currencyAffixes, long[] divisors, Object[][] testItems) {
Collection<String> debugCreationErrors = new LinkedHashSet();
CompactDecimalFormat cdf = new CompactDecimalFormat(
"#,###.00",
"#,###.00",
DecimalFormatSymbols.getInstance(new ULocale("fr")),
CompactStyle.SHORT, PluralRules.createRules("one: j is 1 or f is 1"),
divisors, affixes, currencyAffixes,
@ -319,17 +318,8 @@ 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.
@Test
@Ignore
public void TestArabicLongStyle() {
if (logKnownIssue("12181","No support for negative numbers in CDF")) {
return;
}
NumberFormat cdf =
CompactDecimalFormat.getInstance(new Locale("ar"), CompactStyle.LONG);
assertEquals("Arabic Long", "\u200F-\u0665\u066B\u0663 \u0623\u0644\u0641", cdf.format(-5300));
@ -355,17 +345,8 @@ 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.
//
@Test
@Ignore
public void TestSerbianLongNegative() {
if (logKnownIssue("12181","No support for negative numbers in CDF")) {
return;
}
checkLocale(ULocale.forLanguageTag("sr"), CompactStyle.LONG, SerbianTestDataLongNegative);
}
@ -378,18 +359,9 @@ 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.
//
@Test
@Ignore
public void TestSwahiliShortNegative() {
if (logKnownIssue("12181","No support for negative numbers in CDF")) {
return;
}
checkLocale(ULocale.forLanguageTag("sw"), CompactStyle.SHORT, SwahiliTestDataNegative);
}
@ -397,12 +369,12 @@ public class CompactDecimalFormatTest extends TestFmwk {
public void TestEnglishCurrency() {
checkLocale(ULocale.ENGLISH, CompactStyle.SHORT, EnglishCurrencyTestData);
}
@Test
public void TestGermanCurrency() {
checkLocale(ULocale.GERMAN, CompactStyle.SHORT, GermanCurrencyTestData);
}
@Test
public void TestChineseCurrency() {
checkLocale(ULocale.CHINESE, CompactStyle.SHORT, ChineseCurrencyTestData);
@ -442,7 +414,7 @@ public class CompactDecimalFormatTest extends TestFmwk {
for (Object[] row : testData) {
Object source = row[0];
Object expected = row[1];
assertEquals(title + source, expected,
assertEquals(title + source, expected,
cdf.format(source));
}
}
@ -472,4 +444,68 @@ public class CompactDecimalFormatTest extends TestFmwk {
CompactDecimalFormat.CompactStyle.SHORT ).format(12000);
assertNotEquals("CDF(12,000) for no_NO shouldn't be 12 (12K or similar)", "12", result);
}
@Test
public void TestBug12422() {
CompactDecimalFormat cdf;
String result;
// Bug #12422
cdf = CompactDecimalFormat.getInstance(new ULocale("ar", "SA"), CompactDecimalFormat.CompactStyle.LONG);
result = cdf.format(43000);
assertEquals("CDF should correctly format 43000 in 'ar'", "٤٣ ألف", result);
// Bug #12449
cdf = CompactDecimalFormat.getInstance(new ULocale("ar"), CompactDecimalFormat.CompactStyle.SHORT);
cdf.setMaximumSignificantDigits(3);
result = cdf.format(1234);
assertEquals("CDF should correctly format 1234 with 3 significant digits in 'ar'", "١٫٢٣ ألف", result);
// Check currency formatting as well
cdf = CompactDecimalFormat.getInstance(new ULocale("ar"), CompactDecimalFormat.CompactStyle.SHORT);
result = cdf.format(new CurrencyAmount(43000f, Currency.getInstance("USD")));
assertEquals("CDF should correctly format 43000 with currency in 'ar'", "US$ ٤٣ ألف", result);
result = cdf.format(new CurrencyAmount(-43000f, Currency.getInstance("USD")));
assertEquals("CDF should correctly format -43000 with currency in 'ar'", "US$ -٤٣ ألف", result);
// Extra locale with different positive/negative formats
cdf = CompactDecimalFormat.getInstance(new ULocale("fi"), CompactDecimalFormat.CompactStyle.SHORT);
result = cdf.format(new CurrencyAmount(43000f, Currency.getInstance("USD")));
assertEquals("CDF should correctly format 43000 with currency in 'fi'", "43 t. $", result);
result = cdf.format(new CurrencyAmount(-43000f, Currency.getInstance("USD")));
assertEquals("CDF should correctly format -43000 with currency in 'fi'", "43 t. $", result);
}
@Test
public void TestBug12689() {
if (logKnownIssue("12689", "CDF fails for numbers less than 1 thousand in most locales")) {
return;
}
CompactDecimalFormat cdf;
String result;
cdf = CompactDecimalFormat.getInstance(ULocale.forLanguageTag("en"), CompactStyle.SHORT);
result = cdf.format(new CurrencyAmount(123, Currency.getInstance("USD")));
assertEquals("CDF should correctly format 123 with currency in 'en'", "$120", result);
cdf = CompactDecimalFormat.getInstance(ULocale.forLanguageTag("it"), CompactStyle.SHORT);
result = cdf.format(new CurrencyAmount(123, Currency.getInstance("EUR")));
assertEquals("CDF should correctly format 123 with currency in 'it'", "120 €", result);
}
@Test
public void TestBug12688() {
if (logKnownIssue("12688", "CDF fails for numbers less than 1 million in 'it'")) {
return;
}
CompactDecimalFormat cdf;
String result;
cdf = CompactDecimalFormat.getInstance(ULocale.forLanguageTag("it"), CompactStyle.SHORT);
result = cdf.format(new CurrencyAmount(123000, Currency.getInstance("EUR")));
assertEquals("CDF should correctly format 123000 with currency in 'it'", "120000 €", result);
}
}