ICU-13113 Changing decimal format exceptions to always contain the string "Malformed pattern" for better backwards and forwards compatibility.

X-SVN-Rev: 40027
This commit is contained in:
Shane Carr 2017-04-10 23:35:22 +00:00
parent 990a7b0c62
commit 39184c68c2
6 changed files with 101 additions and 46 deletions

View File

@ -19,18 +19,19 @@ public class PatternString {
* @param pattern The pattern string, like "#,##0.00"
* @param ignoreRounding Whether to leave out rounding information (minFrac, maxFrac, and rounding
* increment) when parsing the pattern. This may be desirable if a custom rounding mode, such
* as CurrencyUsage, is to be used instead.
* as CurrencyUsage, is to be used instead. One of {@link #IGNORE_ROUNDING_ALWAYS}, {@link
* #IGNORE_ROUNDING_IF_CURRENCY}, or {@link #IGNORE_ROUNDING_NEVER}.
* @return A property bag object.
* @throws IllegalArgumentException If there is a syntax error in the pattern string.
*/
public static Properties parseToProperties(String pattern, boolean ignoreRounding) {
public static Properties parseToProperties(String pattern, int ignoreRounding) {
Properties properties = new Properties();
LdmlDecimalPatternParser.parse(pattern, properties, ignoreRounding);
return properties;
}
public static Properties parseToProperties(String pattern) {
return parseToProperties(pattern, false);
return parseToProperties(pattern, PatternString.IGNORE_ROUNDING_NEVER);
}
/**
@ -43,16 +44,17 @@ public class PatternString {
* @param properties The property bag object to overwrite.
* @param ignoreRounding Whether to leave out rounding information (minFrac, maxFrac, and rounding
* increment) when parsing the pattern. This may be desirable if a custom rounding mode, such
* as CurrencyUsage, is to be used instead.
* as CurrencyUsage, is to be used instead. One of {@link #IGNORE_ROUNDING_ALWAYS}, {@link
* #IGNORE_ROUNDING_IF_CURRENCY}, or {@link #IGNORE_ROUNDING_NEVER}.
* @throws IllegalArgumentException If there was a syntax error in the pattern string.
*/
public static void parseToExistingProperties(
String pattern, Properties properties, boolean ignoreRounding) {
String pattern, Properties properties, int ignoreRounding) {
LdmlDecimalPatternParser.parse(pattern, properties, ignoreRounding);
}
public static void parseToExistingProperties(String pattern, Properties properties) {
parseToExistingProperties(pattern, properties, false);
parseToExistingProperties(pattern, properties, PatternString.IGNORE_ROUNDING_NEVER);
}
/**
@ -384,6 +386,10 @@ public class PatternString {
return result.toString();
}
public static final int IGNORE_ROUNDING_NEVER = 0;
public static final int IGNORE_ROUNDING_IF_CURRENCY = 1;
public static final int IGNORE_ROUNDING_ALWAYS = 2;
/** Implements a recursive descent parser for decimal format patterns. */
static class LdmlDecimalPatternParser {
@ -396,10 +402,20 @@ public class PatternString {
SubpatternParseResult negative = null;
/** Finalizes the temporary data stored in the PatternParseResult to the Builder. */
void saveToProperties(Properties properties, boolean ignoreRounding) {
void saveToProperties(Properties properties, int _ignoreRounding) {
// Translate from PatternState to Properties.
// Note that most data from "negative" is ignored per the specification of DecimalFormat.
boolean ignoreRounding;
if (_ignoreRounding == IGNORE_ROUNDING_NEVER) {
ignoreRounding = false;
} else if (_ignoreRounding == IGNORE_ROUNDING_IF_CURRENCY) {
ignoreRounding = positive.hasCurrencySign;
} else {
assert _ignoreRounding == IGNORE_ROUNDING_ALWAYS;
ignoreRounding = true;
}
// Grouping settings
if (positive.groupingSizes[1] != -1) {
properties.setGroupingSize(positive.groupingSizes[0]);
@ -556,6 +572,7 @@ public class PatternString {
int exponentDigits = 0;
boolean hasPercentSign = false;
boolean hasPerMilleSign = false;
boolean hasCurrencySign = false;
StringBuilder padding = new StringBuilder();
StringBuilder prefix = new StringBuilder();
@ -588,23 +605,17 @@ public class PatternString {
IllegalArgumentException toParseException(String message) {
StringBuilder sb = new StringBuilder();
sb.append("Unexpected character in decimal format pattern: '");
sb.append("Malformed pattern for ICU DecimalFormat: \"");
sb.append(pattern);
sb.append("': ");
sb.append("\": ");
sb.append(message);
sb.append(": ");
if (peek() == -1) {
sb.append("EOL");
} else {
sb.append("'");
sb.append(Character.toChars(peek()));
sb.append("'");
}
sb.append(" at position ");
sb.append(offset);
return new IllegalArgumentException(sb.toString());
}
}
static void parse(String pattern, Properties properties, boolean ignoreRounding) {
static void parse(String pattern, Properties properties, int ignoreRounding) {
if (pattern == null || pattern.length() == 0) {
// Backwards compatibility requires that we reset to the default values.
// TODO: Only overwrite the properties that "saveToProperties" normally touches?
@ -629,7 +640,7 @@ public class PatternString {
consumeSubpattern(state, result.negative);
}
if (state.peek() != -1) {
throw state.toParseException("pattern");
throw state.toParseException("Found unquoted special character");
}
}
@ -689,7 +700,7 @@ public class PatternString {
break;
case '¤':
// no need to record that we saw it
result.hasCurrencySign = true;
break;
}
consumeLiteral(state, destination);
@ -698,12 +709,12 @@ public class PatternString {
private static void consumeLiteral(ParserState state, StringBuilder destination) {
if (state.peek() == -1) {
throw state.toParseException("expected unquoted literal but found end of string");
throw state.toParseException("Expected unquoted literal but found EOL");
} else if (state.peek() == '\'') {
destination.appendCodePoint(state.next()); // consume the starting quote
while (state.peek() != '\'') {
if (state.peek() == -1) {
throw state.toParseException("expected quoted literal but found end of string");
throw state.toParseException("Expected quoted literal but found EOL");
} else {
destination.appendCodePoint(state.next()); // consume a quoted character
}
@ -751,7 +762,7 @@ public class PatternString {
case '@':
seenSignificantDigitMarker = true;
if (seenDigit) throw state.toParseException("Can't mix @ and 0 in pattern");
if (seenDigit) throw state.toParseException("Cannot mix 0 and @");
result.paddingWidth += 1;
result.groupingSizes[0] += 1;
result.totalIntegerDigits += 1;
@ -772,8 +783,7 @@ public class PatternString {
case '8':
case '9':
seenDigit = true;
if (seenSignificantDigitMarker)
throw state.toParseException("Can't mix @ and 0 in pattern");
if (seenSignificantDigitMarker) throw state.toParseException("Cannot mix @ and 0");
// TODO: Crash here if we've seen the significant digit marker? See NumberFormatTestCases.txt
result.paddingWidth += 1;
result.groupingSizes[0] += 1;

View File

@ -219,7 +219,8 @@ public class CurrencyFormat {
} else {
// CurrencyPluralInfo is available. Use it to generate affixes for long name support.
String pluralPattern = info.getCurrencyPluralPattern(plural.getKeyword());
PatternString.parseToExistingProperties(pluralPattern, temp, true);
PatternString.parseToExistingProperties(
pluralPattern, temp, PatternString.IGNORE_ROUNDING_ALWAYS);
result = pnag.getModifiers(symbols, sym, iso, longName, temp);
}
mod.put(plural, result.positive, result.negative);

View File

@ -12,6 +12,7 @@ package com.ibm.icu.text;
import java.text.ParsePosition;
import java.util.Locale;
import com.ibm.icu.impl.number.PatternString;
import com.ibm.icu.impl.number.Properties;
import com.ibm.icu.util.CurrencyAmount;
import com.ibm.icu.util.ULocale;
@ -106,7 +107,7 @@ public class CompactDecimalFormat extends DecimalFormat {
properties = new Properties();
properties.setCompactStyle(style);
exportedProperties = new Properties();
setPropertiesFromPattern(pattern, true);
setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_NEVER);
if (style == CompactStyle.SHORT) {
// TODO: This was setGroupingUsed(false) in ICU 58. Is it okay that I changed it for ICU 59?
properties.setMinimumGroupingDigits(2);

View File

@ -13,7 +13,6 @@ import java.text.FieldPosition;
import java.text.ParseException;
import java.text.ParsePosition;
import com.ibm.icu.impl.number.AffixPatternUtils;
import com.ibm.icu.impl.number.Endpoint;
import com.ibm.icu.impl.number.Format.SingularFormat;
import com.ibm.icu.impl.number.FormatQuantity4;
@ -302,8 +301,7 @@ public class DecimalFormat extends NumberFormat {
properties = new Properties();
exportedProperties = new Properties();
// Regression: ignore pattern rounding information if the pattern has currency symbols.
boolean ignorePatternRounding = AffixPatternUtils.hasCurrencySymbols(pattern);
setPropertiesFromPattern(pattern, ignorePatternRounding);
setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_IF_CURRENCY);
refreshFormatter();
}
@ -332,8 +330,7 @@ public class DecimalFormat extends NumberFormat {
properties = new Properties();
exportedProperties = new Properties();
// Regression: ignore pattern rounding information if the pattern has currency symbols.
boolean ignorePatternRounding = AffixPatternUtils.hasCurrencySymbols(pattern);
setPropertiesFromPattern(pattern, ignorePatternRounding);
setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_IF_CURRENCY);
refreshFormatter();
}
@ -362,8 +359,7 @@ public class DecimalFormat extends NumberFormat {
properties = new Properties();
exportedProperties = new Properties();
// Regression: ignore pattern rounding information if the pattern has currency symbols.
boolean ignorePatternRounding = AffixPatternUtils.hasCurrencySymbols(pattern);
setPropertiesFromPattern(pattern, ignorePatternRounding);
setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_IF_CURRENCY);
refreshFormatter();
}
@ -405,11 +401,10 @@ public class DecimalFormat extends NumberFormat {
|| choice == ACCOUNTINGCURRENCYSTYLE
|| choice == CASHCURRENCYSTYLE
|| choice == STANDARDCURRENCYSTYLE
|| choice == PLURALCURRENCYSTYLE
|| AffixPatternUtils.hasCurrencySymbols(pattern)) {
setPropertiesFromPattern(pattern, true);
|| choice == PLURALCURRENCYSTYLE) {
setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_ALWAYS);
} else {
setPropertiesFromPattern(pattern, false);
setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_IF_CURRENCY);
}
refreshFormatter();
}
@ -450,7 +445,7 @@ public class DecimalFormat extends NumberFormat {
* @stable ICU 2.0
*/
public synchronized void applyPattern(String pattern) {
setPropertiesFromPattern(pattern, false);
setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_NEVER);
// Backwards compatibility: clear out user-specified prefix and suffix,
// as well as CurrencyPluralInfo.
properties.setPositivePrefix(null);
@ -2447,11 +2442,14 @@ public class DecimalFormat extends NumberFormat {
* Updates the property bag with settings from the given pattern.
*
* @param pattern The pattern string to parse.
* @param ignoreRounding Whether to read rounding information from the string. Set to false if
* CurrencyUsage is to be used instead.
* @param ignoreRounding Whether to leave out rounding information (minFrac, maxFrac, and rounding
* increment) when parsing the pattern. This may be desirable if a custom rounding mode, such
* as CurrencyUsage, is to be used instead. One of {@link
* PatternString#IGNORE_ROUNDING_ALWAYS}, {@link PatternString#IGNORE_ROUNDING_IF_CURRENCY},
* or {@link PatternString#IGNORE_ROUNDING_NEVER}.
* @see PatternString#parseToExistingProperties
*/
void setPropertiesFromPattern(String pattern, boolean ignoreRounding) {
void setPropertiesFromPattern(String pattern, int ignoreRounding) {
PatternString.parseToExistingProperties(pattern, properties, ignoreRounding);
}

View File

@ -448,7 +448,12 @@ public class NumberFormatDataDrivenTest {
public String format(DataDrivenNumberFormatTestData tuple) {
String pattern = (tuple.pattern == null) ? "0" : tuple.pattern;
ULocale locale = (tuple.locale == null) ? ULocale.ENGLISH : tuple.locale;
Properties properties = PatternString.parseToProperties(pattern, tuple.currency != null);
Properties properties =
PatternString.parseToProperties(
pattern,
tuple.currency != null
? PatternString.IGNORE_ROUNDING_ALWAYS
: PatternString.IGNORE_ROUNDING_IF_CURRENCY);
propertiesFromTuple(tuple, properties);
Format fmt = Endpoint.fromBTA(properties, locale);
FormatQuantity q1, q2, q3;
@ -513,7 +518,12 @@ public class NumberFormatDataDrivenTest {
final Properties properties;
DecimalFormat df;
try {
properties = PatternString.parseToProperties(pattern, tuple.currency != null);
properties =
PatternString.parseToProperties(
pattern,
tuple.currency != null
? PatternString.IGNORE_ROUNDING_ALWAYS
: PatternString.IGNORE_ROUNDING_IF_CURRENCY);
propertiesFromTuple(tuple, properties);
// TODO: Use PatternString.propertiesToString() directly. (How to deal with CurrencyUsage?)
df = new DecimalFormat();
@ -559,7 +569,12 @@ public class NumberFormatDataDrivenTest {
ParsePosition ppos = new ParsePosition(0);
Number actual;
try {
properties = PatternString.parseToProperties(pattern, tuple.currency != null);
properties =
PatternString.parseToProperties(
pattern,
tuple.currency != null
? PatternString.IGNORE_ROUNDING_ALWAYS
: PatternString.IGNORE_ROUNDING_IF_CURRENCY);
propertiesFromTuple(tuple, properties);
actual =
Parse.parse(
@ -613,7 +628,12 @@ public class NumberFormatDataDrivenTest {
ParsePosition ppos = new ParsePosition(0);
CurrencyAmount actual;
try {
properties = PatternString.parseToProperties(pattern, tuple.currency != null);
properties =
PatternString.parseToProperties(
pattern,
tuple.currency != null
? PatternString.IGNORE_ROUNDING_ALWAYS
: PatternString.IGNORE_ROUNDING_IF_CURRENCY);
propertiesFromTuple(tuple, properties);
actual =
Parse.parseCurrency(

View File

@ -4992,6 +4992,31 @@ public class NumberFormatTest extends TestFmwk {
expect2(numfmt, num, "‎٪ ‎−۱٬۲۳۴");
}
@Test
public void Test13113() {
String[][] cases = {
{"'", "quoted literal"},
{"ab#c'd", "quoted literal"},
{"ab#c*", "unquoted literal"},
{"0#", "# cannot follow 0"},
{".#0", "0 cannot follow #"},
{"@0", "Cannot mix @ and 0"},
{"0@", "Cannot mix 0 and @"},
{"#x#", "unquoted special character"}
};
for (String[] cas : cases) {
try {
new DecimalFormat(cas[0]);
fail("Should have thrown on malformed pattern");
} catch (IllegalArgumentException ex) {
assertTrue("Exception should contain \"Malformed pattern\": " + ex.getMessage(),
ex.getMessage().contains("Malformed pattern"));
assertTrue("Exception should contain \"" + cas[1] + "\"" + ex.getMessage(),
ex.getMessage().contains(cas[1]));
}
}
}
@Test
public void Test13118() {
DecimalFormat df = new DecimalFormat("@@@");