ICU-20623 Add java compability parse mode into DecimalFormat

This internal parse mode is introduced for Android libcore.
This commit is contained in:
Victor Chang 2018-08-13 16:49:10 +01:00 committed by Shane F. Carr
parent b4b2378931
commit 7942b58b81
7 changed files with 152 additions and 9 deletions

View file

@ -24,6 +24,7 @@ import com.ibm.icu.util.UResourceBundle;
*/
public class StaticUnicodeSets {
public static enum Key {
EMPTY,
// Ignorables
DEFAULT_IGNORABLES,
STRICT_IGNORABLES,
@ -231,6 +232,7 @@ public class StaticUnicodeSets {
}
static {
unicodeSets.put(Key.EMPTY, new UnicodeSet("[]").freeze());
// These sets were decided after discussion with icu-design@. See tickets #13084 and #13309.
// Zs+TAB is "horizontal whitespace" according to UTS #18 (blank property).
unicodeSets.put(Key.DEFAULT_IGNORABLES,

View file

@ -61,6 +61,13 @@ public class DecimalFormatProperties implements Cloneable, Serializable {
* </ul>
*/
STRICT,
/**
* Internal parse mode for increased compatibility with java.text.DecimalFormat.
* Used by Android libcore. To enable this feature, java.text.DecimalFormat holds an instance of
* ICU4J's DecimalFormat and enable it by calling setParseStrictMode(ParseMode.COMPATIBILITY).
*/
JAVA_COMPATIBILITY,
}
// The setters in this class should NOT have any side-effects or perform any validation. It is
@ -1396,8 +1403,8 @@ public class DecimalFormatProperties implements Cloneable, Serializable {
// Extra int for possible future use
oos.writeInt(0);
ArrayList<Field> fieldsToSerialize = new ArrayList<Field>();
ArrayList<Object> valuesToSerialize = new ArrayList<Object>();
ArrayList<Field> fieldsToSerialize = new ArrayList<>();
ArrayList<Object> valuesToSerialize = new ArrayList<>();
Field[] fields = DecimalFormatProperties.class.getDeclaredFields();
for (Field field : fields) {
if (Modifier.isStatic(field.getModifiers())) {

View file

@ -18,8 +18,13 @@ public class IgnorablesMatcher extends SymbolMatcher implements NumberParseMatch
private static final IgnorablesMatcher STRICT = new IgnorablesMatcher(
StaticUnicodeSets.get(StaticUnicodeSets.Key.STRICT_IGNORABLES));
private static final IgnorablesMatcher JAVA_COMPATIBILITY = new IgnorablesMatcher(
StaticUnicodeSets.get(StaticUnicodeSets.Key.EMPTY));
public static IgnorablesMatcher getInstance(int parseFlags) {
if (0 != (parseFlags & ParsingUtils.PARSE_FLAG_STRICT_IGNORABLES)) {
if (0 != (parseFlags & ParsingUtils.PARSE_FLAG_JAVA_COMPATIBILITY_IGNORABLES)) {
return JAVA_COMPATIBILITY;
} else if (0 != (parseFlags & ParsingUtils.PARSE_FLAG_STRICT_IGNORABLES)) {
return STRICT;
} else {
return DEFAULT;

View file

@ -145,7 +145,10 @@ public class NumberParserImpl {
affixProvider = new CurrencyPluralInfoAffixProvider(properties.getCurrencyPluralInfo(), properties);
}
Currency currency = CustomSymbolCurrency.resolve(properties.getCurrency(), locale, symbols);
boolean isStrict = properties.getParseMode() == ParseMode.STRICT;
ParseMode parseMode = properties.getParseMode();
if (parseMode == null) {
parseMode = ParseMode.LENIENT;
}
Grouper grouper = Grouper.forProperties(properties);
int parseFlags = 0;
if (!properties.getParseCaseSensitive()) {
@ -160,7 +163,12 @@ public class NumberParserImpl {
if (properties.getSignAlwaysShown()) {
parseFlags |= ParsingUtils.PARSE_FLAG_PLUS_SIGN_ALLOWED;
}
if (isStrict) {
if (parseMode == ParseMode.JAVA_COMPATIBILITY) {
parseFlags |= ParsingUtils.PARSE_FLAG_STRICT_SEPARATORS;
parseFlags |= ParsingUtils.PARSE_FLAG_USE_FULL_AFFIXES;
parseFlags |= ParsingUtils.PARSE_FLAG_EXACT_AFFIX;
parseFlags |= ParsingUtils.PARSE_FLAG_JAVA_COMPATIBILITY_IGNORABLES;
} else if (parseMode == ParseMode.STRICT) {
parseFlags |= ParsingUtils.PARSE_FLAG_STRICT_GROUPING_SIZE;
parseFlags |= ParsingUtils.PARSE_FLAG_STRICT_SEPARATORS;
parseFlags |= ParsingUtils.PARSE_FLAG_USE_FULL_AFFIXES;
@ -210,10 +218,10 @@ public class NumberParserImpl {
// ICU-TC meeting, April 11, 2018: accept percent/permille only if it is in the pattern,
// and to maintain regressive behavior, divide by 100 even if no percent sign is present.
if (!isStrict && affixProvider.containsSymbolType(AffixUtils.TYPE_PERCENT)) {
if (parseMode == ParseMode.LENIENT && affixProvider.containsSymbolType(AffixUtils.TYPE_PERCENT)) {
parser.addMatcher(PercentMatcher.getInstance(symbols));
}
if (!isStrict && affixProvider.containsSymbolType(AffixUtils.TYPE_PERMILLE)) {
if (parseMode == ParseMode.LENIENT && affixProvider.containsSymbolType(AffixUtils.TYPE_PERMILLE)) {
parser.addMatcher(PermilleMatcher.getInstance(symbols));
}
@ -221,7 +229,7 @@ public class NumberParserImpl {
/// OTHER STANDARD MATCHERS ///
///////////////////////////////
if (!isStrict) {
if (parseMode == ParseMode.LENIENT) {
parser.addMatcher(PlusSignMatcher.getInstance(symbols, false));
parser.addMatcher(MinusSignMatcher.getInstance(symbols, false));
}
@ -243,7 +251,7 @@ public class NumberParserImpl {
//////////////////
parser.addMatcher(new RequireNumberValidator());
if (isStrict) {
if (parseMode != ParseMode.LENIENT) {
parser.addMatcher(new RequireAffixValidator());
}
if (parseCurrency) {

View file

@ -26,6 +26,7 @@ public class ParsingUtils {
public static final int PARSE_FLAG_NO_FOREIGN_CURRENCIES = 0x2000;
public static final int PARSE_FLAG_ALLOW_INFINITE_RECURSION = 0x4000;
public static final int PARSE_FLAG_STRICT_IGNORABLES = 0x8000;
public static final int PARSE_FLAG_JAVA_COMPATIBILITY_IGNORABLES = 0x10000;
public static void putLeadCodePoints(UnicodeSet input, UnicodeSet output) {
for (EntryRange range : input.ranges()) {

View file

@ -2194,6 +2194,15 @@ public class DecimalFormat extends NumberFormat {
refreshFormatter();
}
/**
* Android libcore uses this internal method to set {@link ParseMode#JAVA_COMPATIBILITY}.
* @internal
*/
public synchronized void setParseStrictMode(ParseMode parseMode) {
properties.setParseMode(parseMode);
refreshFormatter();
}
/**
* {@inheritDoc}
*

View file

@ -0,0 +1,111 @@
// © 2019 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.dev.test.format;
import static org.junit.Assert.assertEquals;
import java.text.ParsePosition;
import java.util.Locale;
import java.util.Objects;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import com.ibm.icu.dev.test.TestUtil;
import com.ibm.icu.impl.number.DecimalFormatProperties.ParseMode;
import com.ibm.icu.text.DecimalFormat;
import com.ibm.icu.text.DecimalFormatSymbols;
import com.ibm.icu.util.ULocale;
/**
* Test for {@link DecimalFormat} in {@link ParseMode#JAVA_COMPATIBILITY} mode.
*/
@RunWith(JUnit4.class)
public class NumberFormatJavaCompatilityTest {
@Test
public void testIgnoreables() {
// Test bidi characters
assertParseError("0", "\u200e1");
assertParsed("0", "1\u200e", 1);
assertParseError("0%", "\u200e1%");
}
@Test
public void testParseGroupingSeparator() {
// Test that grouping separator is optional when the group separator is specified
assertParsed("#,##0", "9,999", 9999);
assertParsed("#,##0", "9999", 9999);
assertParsed("#,###0", "9,9999", 99999);
// Test that grouping size doesn't affect parsing at all
assertParsed("#,##0", "9,9999", 99999);
assertParsed("#,###0", "99,999", 99999);
assertParsed("###0", "9999", 9999);
assertParsed("###0", "99999", 99999);
// Test that grouping separator must not be present when the group separator is NOT specified
// Only the 1st character in front of separator , should be consumed.
assertParsed("###0", "9,9999", 9);
assertParsed("###0", "9,999", 9);
}
@Test
public void testParseScienificNotation() {
assertParsed("0.###E0", "1E-3", 0.001);
assertParsed("0.###E0", "1E0", 1);
assertParsed("0.###E0", "1E3", 1000);
assertParsed("0.###E0", "1.111E3", 1111);
assertParsed("0.###E0", "1.1E3", 1100);
// "0.###E0" is engineering notation, i.e. the exponent should be a multiple of 3
// for formatting. But it shouldn't affect parsing.
assertParsed("0.###E0", "1E1", 10);
// Test that exponent is not required for parsing
assertParsed("0.###E0", "1.1", 1.1);
assertParsed("0.###E0", "1100", 1100);
// Test that the max of fraction, integer or signficant digits don't affect parsing
// Note that the max of signficant digits is 4 = min integer digits (1)
// + max fraction digits (3)
assertParsed("0.###E0", "1111.4E3", 1111400);
assertParsed("0.###E0", "1111.9999E3", 1111999.9);
}
private void assertParseError(String pattern, String input) {
assertParsed(pattern, input, null);
}
private void assertParsed(String pattern, String input, Number expected) {
assertParsedICU4J(pattern, input, expected);
// Skip the OpenJDK test if the runtime is not OpenJDK
if (TestUtil.getJavaRuntimeName() != TestUtil.JavaRuntimeName.OpenJDK) {
return;
}
assertParsedOpenJDK(pattern, input, expected);
}
private void assertParsedICU4J(String pattern, String input, Number expected) {
DecimalFormat df = new DecimalFormat(pattern, new DecimalFormatSymbols(ULocale.US));
df.setParseStrictMode(ParseMode.JAVA_COMPATIBILITY);
ParsePosition ppos = new ParsePosition(0);
Number actual = df.parse(input, ppos);
assertEquals(String.format("pattern: %s input: %s", pattern, input),
Objects.toString(expected), Objects.toString(actual));
}
private void assertParsedOpenJDK(String pattern, String input, Number expected) {
java.text.DecimalFormat df = new java.text.DecimalFormat(pattern,
new java.text.DecimalFormatSymbols(Locale.US));
ParsePosition ppos = new ParsePosition(0);
Number actual = df.parse(input, ppos);
assertEquals(String.format("pattern: %s input: %s", pattern, input),
Objects.toString(expected), Objects.toString(actual));
}
}