import java.lang.ref.SoftReference;
import java.util.Collections;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.Map;
import java.util.MissingResourceException;
-import java.util.Set;
-import java.util.TreeMap;
import com.ibm.icu.impl.CurrencyData.CurrencyDisplayInfo;
import com.ibm.icu.impl.CurrencyData.CurrencyDisplayInfoProvider;
import com.ibm.icu.impl.CurrencyData.CurrencyFormatInfo;
import com.ibm.icu.impl.CurrencyData.CurrencySpacingInfo;
import com.ibm.icu.impl.ICUResourceBundle.OpenType;
+import com.ibm.icu.util.ICUException;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
static class ICUCurrencyDisplayInfo extends CurrencyDisplayInfo {
private final boolean fallback;
private final ICUResourceBundle rb;
- private final ICUResourceBundle currencies;
- private final ICUResourceBundle plurals;
- private SoftReference<Map<String, String>> _symbolMapRef;
- private SoftReference<Map<String, String>> _nameMapRef;
+ private volatile SoftReference<RawCurrencyData> rawDataCache;
+
+ /**
+ * The primary data structure is isoCodeToCurrencyStrings. In that structure,
+ * the String arrays contain the following elements:
+ *
+ * [DISPLAY_NAME] => display name
+ * [SYMBOL] => symbol
+ * [NARROW_SYMBOL] => narrow symbol
+ * [FORMAT_PATTERN] => currency format pattern
+ * [DECIMAL_SEPARATOR] => currency decimal separator
+ * [GROUPING_SEPARATOR] => currency grouping separator
+ * [PLURALS_OFFSET+p] => plural name where p=standardPlural.ordinal()
+ */
+ private static class RawCurrencyData {
+ static final int DISPLAY_NAME = 0;
+ static final int SYMBOL = 1;
+ static final int NARROW_SYMBOL = 2;
+ static final int FORMAT_PATTERN = 3;
+ static final int DECIMAL_SEPARATOR = 4;
+ static final int GROUPING_SEPARATOR = 5;
+ static final int PLURALS_OFFSET = 6;
+ static final int CURRENCY_STRINGS_LENGTH = 6 + StandardPlural.COUNT;
+
+ Map<String, String[]> isoCodeToCurrencyStrings = new HashMap<String, String[]>();
+
+ // The following maps are redundant data with the above map, but the API for CurrencyDisplayNames
+ // restricts us to using these data structures.
+ Map<String, String> symbolToIsoCode = new HashMap<String, String>();
+ Map<String, String> nameToIsoCode = new HashMap<String, String>();
+
+ // Other currency-related data
+ CurrencySpacingInfo spacingInfo = new CurrencySpacingInfo();
+ Map<String, String> currencyUnitPatterns = new HashMap<String, String>();
+
+ /**
+ * Gets an entry out of isoCodeToCurrencyStrings or creates it if it does not exist yet.
+ */
+ String[] getOrCreateCurrencyStrings(String isoCode) {
+ String[] currencyStrings = isoCodeToCurrencyStrings.get(isoCode);
+ if (currencyStrings == null) {
+ currencyStrings = new String[CURRENCY_STRINGS_LENGTH];
+ isoCodeToCurrencyStrings.put(isoCode, currencyStrings);
+ }
+ return currencyStrings;
+ }
+
+ /**
+ * Called after all data is loaded to convert the externally visible Maps to Unmodifiable.
+ */
+ void freezeMaps() {
+ symbolToIsoCode = Collections.unmodifiableMap(symbolToIsoCode);
+ nameToIsoCode = Collections.unmodifiableMap(nameToIsoCode);
+ currencyUnitPatterns = Collections.unmodifiableMap(currencyUnitPatterns);
+ }
+ }
public ICUCurrencyDisplayInfo(ICUResourceBundle rb, boolean fallback) {
this.fallback = fallback;
this.rb = rb;
- this.currencies = rb.findTopLevel("Currencies");
- this.plurals = rb.findTopLevel("CurrencyPlurals");
- }
+ rawDataCache = new SoftReference<RawCurrencyData>(null);
+ }
@Override
public ULocale getULocale() {
@Override
public String getName(String isoCode) {
- return getName(isoCode, false);
+ return getName(isoCode, RawCurrencyData.DISPLAY_NAME);
}
@Override
public String getSymbol(String isoCode) {
- return getName(isoCode, true);
+ return getName(isoCode, RawCurrencyData.SYMBOL);
}
- private String getName(String isoCode, boolean symbolName) {
- if (currencies != null) {
- ICUResourceBundle result = currencies.findWithFallback(isoCode);
- if (result != null) {
- if (!fallback && !rb.isRoot() && result.isRoot()) {
- return null;
- }
- return result.getString(symbolName ? 0 : 1);
- }
- }
+ @Override
+ public String getNarrowSymbol(String isoCode) {
+ // TODO: Should this fall back to the regular symbol instead of the ISO code?
+ return getName(isoCode, RawCurrencyData.NARROW_SYMBOL);
+ }
- return fallback ? isoCode : null;
+ private String getName(String isoCode, int index) {
+ String[] currencyStrings = getRawCurrencyData().isoCodeToCurrencyStrings.get(isoCode);
+ String result = null;
+ if (currencyStrings != null) {
+ result = currencyStrings[index];
+ }
+ // If fallback is true, don't return null; return the ISO code
+ if (result == null && fallback) {
+ result = isoCode;
+ }
+ return result;
}
@Override
public String getPluralName(String isoCode, String pluralKey ) {
+ StandardPlural plural = StandardPlural.orNullFromString(pluralKey);
+ String[] currencyStrings = getRawCurrencyData().isoCodeToCurrencyStrings.get(isoCode);
+ String result = null;
+ if (currencyStrings != null && plural != null) {
+ result = currencyStrings[RawCurrencyData.PLURALS_OFFSET + plural.ordinal()];
+ }
// See http://unicode.org/reports/tr35/#Currencies, especially the fallback rule.
- if (plurals != null) {
- ICUResourceBundle pluralsBundle = plurals.findWithFallback(isoCode);
- if (pluralsBundle != null) {
- String pluralName = pluralsBundle.findStringWithFallback(pluralKey);
- if (pluralName == null) {
- if (!fallback) {
- return null;
- }
- pluralName = pluralsBundle.findStringWithFallback("other");
- if (pluralName == null) {
- return getName(isoCode);
- }
- }
- return pluralName;
- }
+ if (result == null && currencyStrings != null && fallback) {
+ // First fall back to the "other" plural variant
+ // Note: If plural is already "other", this fallback is benign
+ result = currencyStrings[RawCurrencyData.PLURALS_OFFSET + StandardPlural.OTHER.ordinal()];
}
-
- return fallback ? getName(isoCode) : null;
+ if (result == null && currencyStrings != null && fallback) {
+ // If that fails, fall back to the display name
+ result = currencyStrings[0];
+ }
+ if (result == null && fallback) {
+ // If all else fails, return the ISO code
+ result = isoCode;
+ }
+ return result;
}
@Override
public Map<String, String> symbolMap() {
- Map<String, String> map = _symbolMapRef == null ? null : _symbolMapRef.get();
- if (map == null) {
- map = _createSymbolMap();
- // atomic and idempotent
- _symbolMapRef = new SoftReference<Map<String, String>>(map);
- }
- return map;
+ return getRawCurrencyData().symbolToIsoCode;
}
@Override
public Map<String, String> nameMap() {
- Map<String, String> map = _nameMapRef == null ? null : _nameMapRef.get();
- if (map == null) {
- map = _createNameMap();
- // atomic and idempotent
- _nameMapRef = new SoftReference<Map<String, String>>(map);
- }
- return map;
+ return getRawCurrencyData().nameToIsoCode;
}
@Override
public Map<String, String> getUnitPatterns() {
- Map<String, String> result = new HashMap<String, String>();
-
- ULocale locale = rb.getULocale();
- for (;locale != null; locale = locale.getFallback()) {
- ICUResourceBundle r = (ICUResourceBundle) UResourceBundle.getBundleInstance(
- ICUData.ICU_CURR_BASE_NAME, locale);
- if (r == null) {
- continue;
- }
- ICUResourceBundle cr = r.findWithFallback("CurrencyUnitPatterns");
- if (cr == null) {
- continue;
- }
- for (int index = 0, size = cr.getSize(); index < size; ++index) {
- ICUResourceBundle b = (ICUResourceBundle) cr.get(index);
- String key = b.getKey();
- if (result.containsKey(key)) {
- continue;
- }
- result.put(key, b.getString());
- }
- }
-
// Default result is the empty map. Callers who require a pattern will have to
// supply a default.
- return Collections.unmodifiableMap(result);
+ return getRawCurrencyData().currencyUnitPatterns;
}
@Override
public CurrencyFormatInfo getFormatInfo(String isoCode) {
- ICUResourceBundle crb = currencies.findWithFallback(isoCode);
- if (crb != null && crb.getSize() > 2) {
- crb = crb.at(2);
- if (crb != null) {
- String pattern = crb.getString(0);
- String separator = crb.getString(1);
- String groupingSeparator = crb.getString(2);
- return new CurrencyFormatInfo(pattern, separator, groupingSeparator);
- }
+ String[] currencyStrings = getRawCurrencyData().isoCodeToCurrencyStrings.get(isoCode);
+ if (currencyStrings == null || currencyStrings[RawCurrencyData.FORMAT_PATTERN] == null) {
+ return null;
}
- return null;
+ String pattern = currencyStrings[RawCurrencyData.FORMAT_PATTERN];
+ String decimalSeparator = currencyStrings[RawCurrencyData.DECIMAL_SEPARATOR];
+ String groupingSeparator = currencyStrings[RawCurrencyData.GROUPING_SEPARATOR];
+ return new CurrencyFormatInfo(pattern, decimalSeparator, groupingSeparator);
}
@Override
public CurrencySpacingInfo getSpacingInfo() {
- SpacingInfoSink sink = new SpacingInfoSink();
- rb.getAllItemsWithFallback("currencySpacing", sink);
- return sink.getSpacingInfo(fallback);
+ CurrencySpacingInfo result = getRawCurrencyData().spacingInfo;
+ if (result != null && (!result.hasBeforeCurrency || !result.hasAfterCurrency) && fallback) {
+ result = CurrencySpacingInfo.DEFAULT;
+ }
+ if (result == null && fallback) {
+ result = CurrencySpacingInfo.DEFAULT;
+ }
+ return result;
}
- private final class SpacingInfoSink extends UResource.Sink {
- CurrencySpacingInfo spacingInfo = new CurrencySpacingInfo();
- boolean hasBeforeCurrency = false;
- boolean hasAfterCurrency = false;
+ /**
+ * If the soft cache is populated, returns the data stored there.
+ * Otherwise, computes the data, stores it in the cache, and returns it.
+ * Never returns null.
+ */
+ private RawCurrencyData getRawCurrencyData() {
+ RawCurrencyData data = rawDataCache.get();
+ if (data == null) {
+ data = new RawCurrencyData();
+ RawCurrencyDataSink sink = new RawCurrencyDataSink(data, !fallback);
+ rb.getAllItemsWithFallback("", sink);
+ data.freezeMaps();
+ rawDataCache = new SoftReference<RawCurrencyData>(data);
+ }
+ return data;
+ }
+
+ private static final class RawCurrencyDataSink extends UResource.Sink {
+ private final RawCurrencyData data;
+ private final boolean noRoot;
+
+ RawCurrencyDataSink(RawCurrencyData data, boolean noRoot) {
+ this.data = data;
+ this.noRoot = noRoot;
+ }
+
+ /**
+ * The entrypoint method delegates to helper methods for each of the types of tables
+ * found in the currency data.
+ */
+ @Override
+ public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
+ if (noRoot && noFallback) {
+ // Don't consume the root bundle
+ return;
+ }
+
+ UResource.Table table = value.getTable();
+ for (int i = 0; table.getKeyAndValue(i, key, value); i++) {
+ if (key.contentEquals("Currencies")) {
+ consumeCurrencies(key, value, noFallback);
+ } else if (key.contentEquals("Currencies%narrow")) {
+ consumeCurrenciesNarrow(key, value, noFallback);
+ } else if (key.contentEquals("Currencies%variant")) {
+ consumeCurrenciesVariant(key, value, noFallback);
+ } else if (key.contentEquals("CurrencyPlurals")) {
+ consumeCurrencyPlurals(key, value, noFallback);
+ } else if (key.contentEquals("currencySpacing")) {
+ consumeCurrencySpacing(key, value, noFallback);
+ } else if (key.contentEquals("CurrencyUnitPatterns")) {
+ consumeCurrencyUnitPatterns(key, value, noFallback);
+ }
+ }
+ }
+
+ /*
+ * Currencies{
+ * ...
+ * USD{
+ * "US$", => symbol
+ * "US Dollar", => display name
+ * }
+ * ...
+ * ESP{
+ * "₧", => symbol
+ * "pesseta espanyola", => display name
+ * {
+ * "¤ #,##0.00", => currency-specific pattern
+ * ",", => currency-specific grouping separator
+ * ".", => currency-specific decimal separator
+ * }
+ * }
+ * ...
+ * }
+ */
+ private void consumeCurrencies(UResource.Key key, UResource.Value value, boolean noFallback) {
+
+ UResource.Table table = value.getTable();
+ for (int i = 0; table.getKeyAndValue(i, key, value); i++) {
+ String isoCode = key.toString();
+ String[] currencyStrings = data.getOrCreateCurrencyStrings(isoCode);
+ if (value.getType() != UResourceBundle.ARRAY) {
+ throw new ICUException("Unexpected data type in Currencies table for " + isoCode);
+ }
+ UResource.Array array = value.getArray();
+
+ // First element is the symbol.
+ array.getValue(0, value);
+ String symbol = value.getString();
+ if (currencyStrings[RawCurrencyData.SYMBOL] == null) {
+ currencyStrings[RawCurrencyData.SYMBOL] = symbol;
+ }
+
+ // Second element is the display name.
+ array.getValue(1, value);
+ String name = value.getString();
+ if (currencyStrings[RawCurrencyData.DISPLAY_NAME] == null) {
+ currencyStrings[RawCurrencyData.DISPLAY_NAME] = name;
+ }
+
+ // If present, the third element is the currency format info.
+ // TODO: Write unit test to ensure that this data is being used by number formatting.
+ if (array.getSize() > 2 && currencyStrings[RawCurrencyData.FORMAT_PATTERN] == null) {
+ array.getValue(2, value);
+ UResource.Array formatArray = value.getArray();
+ formatArray.getValue(0, value);
+ currencyStrings[RawCurrencyData.FORMAT_PATTERN] = value.getString();
+ assert currencyStrings[RawCurrencyData.DECIMAL_SEPARATOR] == null;
+ formatArray.getValue(1, value);
+ currencyStrings[RawCurrencyData.DECIMAL_SEPARATOR] = value.getString();
+ assert currencyStrings[RawCurrencyData.GROUPING_SEPARATOR] == null;
+ formatArray.getValue(2, value);
+ currencyStrings[RawCurrencyData.GROUPING_SEPARATOR] = value.getString();
+ }
+
+ // Add the name and symbols to the other two maps (used for parsing).
+ data.nameToIsoCode.put(name, isoCode);
+ data.symbolToIsoCode.put(isoCode, isoCode); // Add the ISO code itself as a symbol
+ data.symbolToIsoCode.put(symbol, isoCode);
+ }
+ }
+
+ /*
+ * Currencies%narrow{
+ * AOA{"Kz"}
+ * ARS{"$"}
+ * ...
+ * }
+ */
+ private void consumeCurrenciesNarrow(UResource.Key key, UResource.Value value, boolean noFallback) {
+ UResource.Table table = value.getTable();
+ for (int i = 0; table.getKeyAndValue(i, key, value); i++) {
+ String isoCode = key.toString();
+ String[] currencyStrings = data.getOrCreateCurrencyStrings(isoCode);
+ if (currencyStrings[RawCurrencyData.NARROW_SYMBOL] == null) {
+ currencyStrings[RawCurrencyData.NARROW_SYMBOL] = value.getString();
+ }
+
+ // Note: This data is used for formatting but not parsing.
+ }
+ }
+
+ /*
+ * Currencies%variant{
+ * TRY{"TL"}
+ * }
+ */
+ private void consumeCurrenciesVariant(UResource.Key key, UResource.Value value, boolean noFallback) {
+ UResource.Table table = value.getTable();
+ for (int i = 0; table.getKeyAndValue(i, key, value); i++) {
+ String isoCode = key.toString();
+
+ // Note: This data is used for parsing but not formatting.
+ data.symbolToIsoCode.put(value.getString(), isoCode);
+ }
+ }
+
+ /*
+ * CurrencyPlurals{
+ * BYB{
+ * one{"Belarusian new rouble (1994–1999)"}
+ * other{"Belarusian new roubles (1994–1999)"}
+ * }
+ * ...
+ * }
+ */
+ private void consumeCurrencyPlurals(UResource.Key key, UResource.Value value, boolean noFallback) {
+ UResource.Table table = value.getTable();
+ for (int i = 0; table.getKeyAndValue(i, key, value); i++) {
+ String isoCode = key.toString();
+ String[] currencyStrings = data.getOrCreateCurrencyStrings(isoCode);
+ UResource.Table pluralsTable = value.getTable();
+ for (int j=0; pluralsTable.getKeyAndValue(j, key, value); j++) {
+ StandardPlural plural = StandardPlural.orNullFromString(key.toString());
+ if (plural == null) {
+ throw new ICUException("Could not make StandardPlural from keyword " + key);
+ }
+ String valueString = value.getString();
+ if (currencyStrings[RawCurrencyData.PLURALS_OFFSET + plural.ordinal()] == null) {
+ currencyStrings[RawCurrencyData.PLURALS_OFFSET + plural.ordinal()] = valueString;
+ }
+
+ // Add the name to the name-to-currency map (used for parsing)
+ data.nameToIsoCode.put(valueString, isoCode);
+ }
+ }
+ }
/*
* currencySpacing{
* }
* }
*/
- @Override
- public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
+ private void consumeCurrencySpacing(UResource.Key key, UResource.Value value, boolean noFallback) {
UResource.Table spacingTypesTable = value.getTable();
for (int i = 0; spacingTypesTable.getKeyAndValue(i, key, value); ++i) {
CurrencySpacingInfo.SpacingType type;
if (key.contentEquals("beforeCurrency")) {
type = CurrencySpacingInfo.SpacingType.BEFORE;
- hasBeforeCurrency = true;
+ data.spacingInfo.hasBeforeCurrency = true;
} else if (key.contentEquals("afterCurrency")) {
type = CurrencySpacingInfo.SpacingType.AFTER;
- hasAfterCurrency = true;
+ data.spacingInfo.hasAfterCurrency = true;
} else {
continue;
}
continue;
}
- spacingInfo.setSymbolIfNull(type, pattern, value.getString());
- }
- }
- }
-
- CurrencySpacingInfo getSpacingInfo(boolean fallback) {
- if (hasBeforeCurrency && hasAfterCurrency) {
- return spacingInfo;
- } else if (fallback) {
- return CurrencySpacingInfo.DEFAULT;
- } else {
- return null;
- }
- }
- }
-
- private Map<String, String> _createSymbolMap() {
- Map<String, String> result = new HashMap<String, String>();
-
- for (ULocale locale = rb.getULocale(); locale != null; locale = locale.getFallback()) {
- ICUResourceBundle bundle = (ICUResourceBundle)
- UResourceBundle.getBundleInstance(ICUData.ICU_CURR_BASE_NAME, locale);
- ICUResourceBundle curr = bundle.findTopLevel("Currencies");
- if (curr == null) {
- continue;
- }
- for (int i = 0; i < curr.getSize(); ++i) {
- ICUResourceBundle item = curr.at(i);
- String isoCode = item.getKey();
- if (!result.containsKey(isoCode)) {
- // put the code itself
- result.put(isoCode, isoCode);
- // 0 == symbol element
- String symbol = item.getString(0);
- result.put(symbol, isoCode);
+ data.spacingInfo.setSymbolIfNull(type, pattern, value.getString());
}
}
}
- return Collections.unmodifiableMap(result);
- }
-
- private Map<String, String> _createNameMap() {
- // ignore case variants
- Map<String, String> result = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
-
- Set<String> visited = new HashSet<String>();
- Map<String, Set<String>> visitedPlurals = new HashMap<String, Set<String>>();
- for (ULocale locale = rb.getULocale(); locale != null; locale = locale.getFallback()) {
- ICUResourceBundle bundle = (ICUResourceBundle)
- UResourceBundle.getBundleInstance(ICUData.ICU_CURR_BASE_NAME, locale);
- ICUResourceBundle curr = bundle.findTopLevel("Currencies");
- if (curr != null) {
- for (int i = 0; i < curr.getSize(); ++i) {
- ICUResourceBundle item = curr.at(i);
- String isoCode = item.getKey();
- if (!visited.contains(isoCode)) {
- visited.add(isoCode);
- // 1 == name element
- String name = item.getString(1);
- result.put(name, isoCode);
- }
- }
- }
-
- ICUResourceBundle plurals = bundle.findTopLevel("CurrencyPlurals");
- if (plurals != null) {
- for (int i = 0; i < plurals.getSize(); ++i) {
- ICUResourceBundle item = plurals.at(i);
- String isoCode = item.getKey();
- Set<String> pluralSet = visitedPlurals.get(isoCode);
- if (pluralSet == null) {
- pluralSet = new HashSet<String>();
- visitedPlurals.put(isoCode, pluralSet);
- }
- for (int j = 0; j < item.getSize(); ++j) {
- ICUResourceBundle plural = item.at(j);
- String pluralType = plural.getKey();
- if (!pluralSet.contains(pluralType)) {
- String pluralName = plural.getString();
- result.put(pluralName, isoCode);
- pluralSet.add(pluralType);
- }
- }
+ /*
+ * CurrencyUnitPatterns{
+ * other{"{0} {1}"}
+ * ...
+ * }
+ */
+ private void consumeCurrencyUnitPatterns(UResource.Key key, UResource.Value value, boolean noFallback) {
+ UResource.Table table = value.getTable();
+ for (int i = 0; table.getKeyAndValue(i, key, value); i++) {
+ String pluralKeyword = key.toString();
+ if (data.currencyUnitPatterns.get(pluralKeyword) == null) {
+ data.currencyUnitPatterns.put(pluralKeyword, value.getString());
}
}
}
-
- return Collections.unmodifiableMap(result);
}
}
}
import java.math.BigDecimal;
import java.math.RoundingMode;
+import java.text.ParseException;
import java.text.ParsePosition;
import org.junit.Test;
import com.ibm.icu.dev.test.TestUtil;
import com.ibm.icu.impl.number.DecimalFormatProperties;
import com.ibm.icu.impl.number.Padder.PadPosition;
+import com.ibm.icu.impl.number.Parse;
import com.ibm.icu.impl.number.Parse.ParseMode;
import com.ibm.icu.impl.number.PatternStringParser;
import com.ibm.icu.impl.number.PatternStringUtils;
import com.ibm.icu.number.LocalizedNumberFormatter;
import com.ibm.icu.number.NumberFormatter;
+import com.ibm.icu.text.DecimalFormat;
+import com.ibm.icu.text.DecimalFormat.PropertySetter;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.text.DecimalFormat_ICU58;
import com.ibm.icu.util.CurrencyAmount;
}
}
+ /**
+ * Formatting, but no other features.
+ */
private DataDrivenNumberFormatTestUtility.CodeUnderTest ICU60 =
new DataDrivenNumberFormatTestUtility.CodeUnderTest() {
}
};
+ /**
+ * All features except formatting.
+ */
+ private DataDrivenNumberFormatTestUtility.CodeUnderTest ICU60_Other =
+ new DataDrivenNumberFormatTestUtility.CodeUnderTest() {
+
+ @Override
+ public Character Id() {
+ return 'S';
+ }
+
+ /**
+ * Runs a single toPattern test. On success, returns null. On failure, returns the error. This implementation
+ * just returns null. Subclasses should override.
+ *
+ * @param tuple
+ * contains the parameters of the format test.
+ */
+ @Override
+ public String toPattern(DataDrivenNumberFormatTestData tuple) {
+ String pattern = (tuple.pattern == null) ? "0" : tuple.pattern;
+ final DecimalFormatProperties properties;
+ DecimalFormat df;
+ try {
+ properties = PatternStringParser.parseToProperties(
+ pattern,
+ tuple.currency != null ? PatternStringParser.IGNORE_ROUNDING_ALWAYS
+ : PatternStringParser.IGNORE_ROUNDING_NEVER);
+ propertiesFromTuple(tuple, properties);
+ // TODO: Use PatternString.propertiesToString() directly. (How to deal with CurrencyUsage?)
+ df = new DecimalFormat();
+ df.setProperties(new PropertySetter() {
+ @Override
+ public void set(DecimalFormatProperties props) {
+ props.copyFrom(properties);
+ }
+ });
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ return e.getLocalizedMessage();
+ }
+
+ if (tuple.toPattern != null) {
+ String expected = tuple.toPattern;
+ String actual = df.toPattern();
+ if (!expected.equals(actual)) {
+ return "Expected toPattern='" + expected + "'; got '" + actual + "'";
+ }
+ }
+ if (tuple.toLocalizedPattern != null) {
+ String expected = tuple.toLocalizedPattern;
+ String actual = PatternStringUtils.propertiesToPatternString(properties);
+ if (!expected.equals(actual)) {
+ return "Expected toLocalizedPattern='" + expected + "'; got '" + actual + "'";
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Runs a single parse test. On success, returns null. On failure, returns the error. This implementation just
+ * returns null. Subclasses should override.
+ *
+ * @param tuple
+ * contains the parameters of the format test.
+ */
+ @Override
+ public String parse(DataDrivenNumberFormatTestData tuple) {
+ String pattern = (tuple.pattern == null) ? "0" : tuple.pattern;
+ DecimalFormatProperties properties;
+ ParsePosition ppos = new ParsePosition(0);
+ Number actual;
+ try {
+ properties = PatternStringParser.parseToProperties(
+ pattern,
+ tuple.currency != null ? PatternStringParser.IGNORE_ROUNDING_ALWAYS
+ : PatternStringParser.IGNORE_ROUNDING_NEVER);
+ propertiesFromTuple(tuple, properties);
+ actual = Parse.parse(tuple.parse, ppos, properties, DecimalFormatSymbols.getInstance(tuple.locale));
+ } catch (IllegalArgumentException e) {
+ return "parse exception: " + e.getMessage();
+ }
+ if (actual == null && ppos.getIndex() != 0) {
+ throw new AssertionError("Error: value is null but parse position is not zero");
+ }
+ if (ppos.getIndex() == 0) {
+ return "Parse failed; got " + actual + ", but expected " + tuple.output;
+ }
+ if (tuple.output.equals("NaN")) {
+ if (!Double.isNaN(actual.doubleValue())) {
+ return "Expected NaN, but got: " + actual;
+ }
+ return null;
+ } else if (tuple.output.equals("Inf")) {
+ if (!Double.isInfinite(actual.doubleValue()) || Double.compare(actual.doubleValue(), 0.0) < 0) {
+ return "Expected Inf, but got: " + actual;
+ }
+ return null;
+ } else if (tuple.output.equals("-Inf")) {
+ if (!Double.isInfinite(actual.doubleValue()) || Double.compare(actual.doubleValue(), 0.0) > 0) {
+ return "Expected -Inf, but got: " + actual;
+ }
+ return null;
+ } else if (tuple.output.equals("fail")) {
+ return null;
+ } else if (new BigDecimal(tuple.output).compareTo(new BigDecimal(actual.toString())) != 0) {
+ return "Expected: " + tuple.output + ", got: " + actual;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Runs a single parse currency test. On success, returns null. On failure, returns the error. This
+ * implementation just returns null. Subclasses should override.
+ *
+ * @param tuple
+ * contains the parameters of the format test.
+ */
+ @Override
+ public String parseCurrency(DataDrivenNumberFormatTestData tuple) {
+ String pattern = (tuple.pattern == null) ? "0" : tuple.pattern;
+ DecimalFormatProperties properties;
+ ParsePosition ppos = new ParsePosition(0);
+ CurrencyAmount actual;
+ try {
+ properties = PatternStringParser.parseToProperties(
+ pattern,
+ tuple.currency != null ? PatternStringParser.IGNORE_ROUNDING_ALWAYS
+ : PatternStringParser.IGNORE_ROUNDING_NEVER);
+ propertiesFromTuple(tuple, properties);
+ actual = Parse
+ .parseCurrency(tuple.parse, ppos, properties, DecimalFormatSymbols.getInstance(tuple.locale));
+ } catch (ParseException e) {
+ e.printStackTrace();
+ return "parse exception: " + e.getMessage();
+ }
+ if (ppos.getIndex() == 0 || actual.getCurrency().getCurrencyCode().equals("XXX")) {
+ return "Parse failed; got " + actual + ", but expected " + tuple.output;
+ }
+ BigDecimal expectedNumber = new BigDecimal(tuple.output);
+ if (expectedNumber.compareTo(new BigDecimal(actual.getNumber().toString())) != 0) {
+ return "Wrong number: Expected: " + expectedNumber + ", got: " + actual;
+ }
+ String expectedCurrency = tuple.outputCurrency;
+ if (!expectedCurrency.equals(actual.getCurrency().toString())) {
+ return "Wrong currency: Expected: " + expectedCurrency + ", got: " + actual;
+ }
+ return null;
+ }
+
+ /**
+ * Runs a single select test. On success, returns null. On failure, returns the error. This implementation just
+ * returns null. Subclasses should override.
+ *
+ * @param tuple
+ * contains the parameters of the format test.
+ */
+ @Override
+ public String select(DataDrivenNumberFormatTestData tuple) {
+ return null;
+ }
+ };
+
@Test
public void TestDataDrivenICU58() {
// Android can't access DecimalFormat_ICU58 for testing (ticket #13283).
}
@Test
- public void TestDataDrivenICU60() {
+ public void TestDataDrivenICULatest_Format() {
DataDrivenNumberFormatTestUtility.runFormatSuiteIncludingKnownFailures(
"numberformattestspecification.txt", ICU60);
}
+
+ @Test
+ public void TestDataDrivenICULatest_Other() {
+ DataDrivenNumberFormatTestUtility.runFormatSuiteIncludingKnownFailures(
+ "numberformattestspecification.txt", ICU60_Other);
+ }
}