From a0de8d89c54c60dbb6ce8616b7ff3760c7a8b111 Mon Sep 17 00:00:00 2001 From: Shane Carr Date: Wed, 13 Dec 2017 10:04:56 +0000 Subject: [PATCH] ICU-13513 Starting to tie in with existing code. Working through the data-driven test file first. X-SVN-Rev: 40726 --- .../CurrencyPluralInfoAffixProvider.java | 56 ++++++ .../PropertiesAffixPatternProvider.java | 128 ++++++++++++ .../icu/impl/number/parse/DecimalMatcher.java | 64 ++++-- .../impl/number/parse/NumberParserImpl.java | 88 +++++++-- .../icu/impl/number/parse/ParsedNumber.java | 39 +++- .../impl/number/parse/ScientificMatcher.java | 28 ++- .../icu/impl/number/parse/StrictMatcher.java | 23 +++ .../ibm/icu/number/NumberPropertyMapper.java | 182 +----------------- .../data/numberformattestspecification.txt | 28 +-- .../format/NumberFormatDataDrivenTest.java | 106 ++++++++++ .../icu/dev/test/number/NumberParserTest.java | 28 ++- 11 files changed, 526 insertions(+), 244 deletions(-) create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencyPluralInfoAffixProvider.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/PropertiesAffixPatternProvider.java create mode 100644 icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/StrictMatcher.java diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencyPluralInfoAffixProvider.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencyPluralInfoAffixProvider.java new file mode 100644 index 00000000000..5f8d65353fc --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencyPluralInfoAffixProvider.java @@ -0,0 +1,56 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +import com.ibm.icu.impl.StandardPlural; +import com.ibm.icu.impl.number.PatternStringParser.ParsedPatternInfo; +import com.ibm.icu.text.CurrencyPluralInfo; + +public class CurrencyPluralInfoAffixProvider implements AffixPatternProvider { + private final AffixPatternProvider[] affixesByPlural; + + public CurrencyPluralInfoAffixProvider(CurrencyPluralInfo cpi) { + affixesByPlural = new ParsedPatternInfo[StandardPlural.COUNT]; + for (StandardPlural plural : StandardPlural.VALUES) { + affixesByPlural[plural.ordinal()] = PatternStringParser + .parseToPatternInfo(cpi.getCurrencyPluralPattern(plural.getKeyword())); + } + } + + @Override + public char charAt(int flags, int i) { + int pluralOrdinal = (flags & Flags.PLURAL_MASK); + return affixesByPlural[pluralOrdinal].charAt(flags, i); + } + + @Override + public int length(int flags) { + int pluralOrdinal = (flags & Flags.PLURAL_MASK); + return affixesByPlural[pluralOrdinal].length(flags); + } + + @Override + public boolean positiveHasPlusSign() { + return affixesByPlural[StandardPlural.OTHER.ordinal()].positiveHasPlusSign(); + } + + @Override + public boolean hasNegativeSubpattern() { + return affixesByPlural[StandardPlural.OTHER.ordinal()].hasNegativeSubpattern(); + } + + @Override + public boolean negativeHasMinusSign() { + return affixesByPlural[StandardPlural.OTHER.ordinal()].negativeHasMinusSign(); + } + + @Override + public boolean hasCurrencySign() { + return affixesByPlural[StandardPlural.OTHER.ordinal()].hasCurrencySign(); + } + + @Override + public boolean containsSymbolType(int type) { + return affixesByPlural[StandardPlural.OTHER.ordinal()].containsSymbolType(type); + } +} \ No newline at end of file diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PropertiesAffixPatternProvider.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PropertiesAffixPatternProvider.java new file mode 100644 index 00000000000..aada5fda771 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PropertiesAffixPatternProvider.java @@ -0,0 +1,128 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number; + +public class PropertiesAffixPatternProvider implements AffixPatternProvider { + private final String posPrefix; + private final String posSuffix; + private final String negPrefix; + private final String negSuffix; + + public PropertiesAffixPatternProvider(DecimalFormatProperties properties) { + // There are two ways to set affixes in DecimalFormat: via the pattern string (applyPattern), and via the + // explicit setters (setPositivePrefix and friends). The way to resolve the settings is as follows: + // + // 1) If the explicit setting is present for the field, use it. + // 2) Otherwise, follows UTS 35 rules based on the pattern string. + // + // Importantly, the explicit setters affect only the one field they override. If you set the positive + // prefix, that should not affect the negative prefix. Since it is impossible for the user of this class + // to know whether the origin for a string was the override or the pattern, we have to say that we always + // have a negative subpattern and perform all resolution logic here. + + // Convenience: Extract the properties into local variables. + // Variables are named with three chars: [p/n][p/s][o/p] + // [p/n] => p for positive, n for negative + // [p/s] => p for prefix, s for suffix + // [o/p] => o for escaped custom override string, p for pattern string + String ppo = AffixUtils.escape(properties.getPositivePrefix()); + String pso = AffixUtils.escape(properties.getPositiveSuffix()); + String npo = AffixUtils.escape(properties.getNegativePrefix()); + String nso = AffixUtils.escape(properties.getNegativeSuffix()); + String ppp = properties.getPositivePrefixPattern(); + String psp = properties.getPositiveSuffixPattern(); + String npp = properties.getNegativePrefixPattern(); + String nsp = properties.getNegativeSuffixPattern(); + + if (ppo != null) { + posPrefix = ppo; + } else if (ppp != null) { + posPrefix = ppp; + } else { + // UTS 35: Default positive prefix is empty string. + posPrefix = ""; + } + + if (pso != null) { + posSuffix = pso; + } else if (psp != null) { + posSuffix = psp; + } else { + // UTS 35: Default positive suffix is empty string. + posSuffix = ""; + } + + if (npo != null) { + negPrefix = npo; + } else if (npp != null) { + negPrefix = npp; + } else { + // UTS 35: Default negative prefix is "-" with positive prefix. + // Important: We prepend the "-" to the pattern, not the override! + negPrefix = ppp == null ? "-" : "-" + ppp; + } + + if (nso != null) { + negSuffix = nso; + } else if (nsp != null) { + negSuffix = nsp; + } else { + // UTS 35: Default negative prefix is the positive prefix. + negSuffix = psp == null ? "" : psp; + } + } + + @Override + public char charAt(int flags, int i) { + return getStringForFlags(flags).charAt(i); + } + + @Override + public int length(int flags) { + return getStringForFlags(flags).length(); + } + + private String getStringForFlags(int flags) { + boolean prefix = (flags & Flags.PREFIX) != 0; + boolean negative = (flags & Flags.NEGATIVE_SUBPATTERN) != 0; + if (prefix && negative) { + return negPrefix; + } else if (prefix) { + return posPrefix; + } else if (negative) { + return negSuffix; + } else { + return posSuffix; + } + } + + @Override + public boolean positiveHasPlusSign() { + return AffixUtils.containsType(posPrefix, AffixUtils.TYPE_PLUS_SIGN) + || AffixUtils.containsType(posSuffix, AffixUtils.TYPE_PLUS_SIGN); + } + + @Override + public boolean hasNegativeSubpattern() { + // See comments in the constructor for more information on why this is always true. + return true; + } + + @Override + public boolean negativeHasMinusSign() { + return AffixUtils.containsType(negPrefix, AffixUtils.TYPE_MINUS_SIGN) + || AffixUtils.containsType(negSuffix, AffixUtils.TYPE_MINUS_SIGN); + } + + @Override + public boolean hasCurrencySign() { + return AffixUtils.hasCurrencySymbols(posPrefix) || AffixUtils.hasCurrencySymbols(posSuffix) + || AffixUtils.hasCurrencySymbols(negPrefix) || AffixUtils.hasCurrencySymbols(negSuffix); + } + + @Override + public boolean containsSymbolType(int type) { + return AffixUtils.containsType(posPrefix, type) || AffixUtils.containsType(posSuffix, type) + || AffixUtils.containsType(negPrefix, type) || AffixUtils.containsType(negSuffix, type); + } +} \ No newline at end of file diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/DecimalMatcher.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/DecimalMatcher.java index c6a63a992be..8cbe29397ac 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/DecimalMatcher.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/DecimalMatcher.java @@ -16,22 +16,37 @@ import com.ibm.icu.text.UnicodeSet; */ public class DecimalMatcher implements NumberParseMatcher { - /** - * @return - */ + // TODO: Re-generate these sets from the database. They probably haven't been updated in a while. + private static final UnicodeSet UNISET_PERIOD_LIKE = new UnicodeSet("[.\\u2024\\u3002\\uFE12\\uFE52\\uFF0E\\uFF61]") + .freeze(); + private static final UnicodeSet UNISET_STRICT_PERIOD_LIKE = new UnicodeSet("[.\\u2024\\uFE52\\uFF0E\\uFF61]") + .freeze(); + private static final UnicodeSet UNISET_COMMA_LIKE = new UnicodeSet( + "[,\\u060C\\u066B\\u3001\\uFE10\\uFE11\\uFE50\\uFE51\\uFF0C\\uFF64]").freeze(); + private static final UnicodeSet UNISET_STRICT_COMMA_LIKE = new UnicodeSet("[,\\u066B\\uFE10\\uFE50\\uFF0C]") + .freeze(); + private static final UnicodeSet UNISET_OTHER_GROUPING_SEPARATORS = new UnicodeSet( + "[\\ '\\u00A0\\u066C\\u2000-\\u200A\\u2018\\u2019\\u202F\\u205F\\u3000\\uFF07]").freeze(); + public static DecimalMatcher getInstance(DecimalFormatSymbols symbols) { - // TODO(sffc): Auto-generated method stub - return new DecimalMatcher(symbols.getDigitStrings(), - new UnicodeSet("[,]").freeze(), - new UnicodeSet("[.]").freeze(), - false); + String groupingSeparator = symbols.getGroupingSeparatorString(); + UnicodeSet groupingSet = UNISET_COMMA_LIKE.contains(groupingSeparator) ? UNISET_COMMA_LIKE.cloneAsThawed().addAll(UNISET_OTHER_GROUPING_SEPARATORS).freeze() + : UNISET_PERIOD_LIKE.contains(groupingSeparator) ? UNISET_PERIOD_LIKE.cloneAsThawed().addAll(UNISET_OTHER_GROUPING_SEPARATORS).freeze() + : UNISET_OTHER_GROUPING_SEPARATORS.contains(groupingSeparator) + ? UNISET_OTHER_GROUPING_SEPARATORS + : new UnicodeSet().addAll(groupingSeparator).freeze(); + + String decimalSeparator = symbols.getDecimalSeparatorString(); + UnicodeSet decimalSet = UNISET_COMMA_LIKE.contains(decimalSeparator) ? UNISET_COMMA_LIKE + : UNISET_PERIOD_LIKE.contains(decimalSeparator) ? UNISET_PERIOD_LIKE + : new UnicodeSet().addAll(decimalSeparator).freeze(); + + return new DecimalMatcher(symbols.getDigitStrings(), groupingSet, decimalSet, false); } public static DecimalMatcher getExponentInstance(DecimalFormatSymbols symbols) { - return new DecimalMatcher(symbols.getDigitStrings(), - new UnicodeSet("[,]").freeze(), - new UnicodeSet("[.]").freeze(), - true); + return new DecimalMatcher(symbols + .getDigitStrings(), new UnicodeSet("[,]").freeze(), new UnicodeSet("[.]").freeze(), true); } private final String[] digitStrings; @@ -39,6 +54,7 @@ public class DecimalMatcher implements NumberParseMatcher { private final UnicodeSet decimalUniSet; private final UnicodeSet separatorSet; public boolean requireGroupingMatch = false; + public boolean groupingEnabled = true; private final int grouping1 = 3; private final int grouping2 = 3; private final boolean isScientific; @@ -51,7 +67,11 @@ public class DecimalMatcher implements NumberParseMatcher { this.digitStrings = digitStrings; this.groupingUniSet = groupingUniSet; this.decimalUniSet = decimalUniSet; - separatorSet = groupingUniSet.cloneAsThawed().addAll(decimalUniSet).freeze(); + if (groupingEnabled) { + separatorSet = groupingUniSet.cloneAsThawed().addAll(decimalUniSet).freeze(); + } else { + separatorSet = decimalUniSet; + } this.isScientific = isScientific; } @@ -65,6 +85,7 @@ public class DecimalMatcher implements NumberParseMatcher { int currGroup = 0; int separator = -1; int lastSeparatorOffset = segment.getOffset(); + int scientificAdjustment = 0; boolean hasPartialPrefix = false; boolean seenBothSeparators = false; while (segment.length() > 0) { @@ -97,7 +118,7 @@ public class DecimalMatcher implements NumberParseMatcher { // If found, save it in the DecimalQuantity or scientific adjustment. if (digit >= 0) { if (isScientific) { - result.scientificAdjustment = digit + result.scientificAdjustment * 10; + scientificAdjustment = digit + scientificAdjustment * 10; } else { if (result.quantity == null) { result.quantity = new DecimalQuantity_DualStorageBCD(); @@ -117,13 +138,13 @@ public class DecimalMatcher implements NumberParseMatcher { if (requireGroupingMatch && currGroup == 0) { break; } - } else if (separator == cp && groupingUniSet.contains(cp)) { + } else if (groupingEnabled && separator == cp && groupingUniSet.contains(cp)) { // Second or later grouping separator. if (requireGroupingMatch && currGroup != grouping2) { break; } - } else if (separator != cp && decimalUniSet.contains(cp)) { - // Decimal separator. + } else if (groupingEnabled && separator != cp && decimalUniSet.contains(cp)) { + // Decimal separator after a grouping separator. if (requireGroupingMatch && currGroup != grouping1) { break; } @@ -141,10 +162,13 @@ public class DecimalMatcher implements NumberParseMatcher { break; } - if (seenBothSeparators || (separator != -1 && decimalUniSet.contains(separator))) { + if (isScientific) { + result.quantity.adjustMagnitude(scientificAdjustment); + } else if (seenBothSeparators || (separator != -1 && decimalUniSet.contains(separator))) { result.quantity.adjustMagnitude(-currGroup); - } else if (requireGroupingMatch && separator != -1 && groupingUniSet.contains(separator) - && currGroup != grouping1) { + } else if (separator != -1 + && ((requireGroupingMatch && groupingUniSet.contains(separator) && currGroup != grouping1) + || !groupingEnabled)) { result.quantity.adjustMagnitude(-currGroup); result.quantity.roundToMagnitude(0, RoundingUtils.mathContextUnlimited(RoundingMode.FLOOR)); segment.setOffset(lastSeparatorOffset); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/NumberParserImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/NumberParserImpl.java index b01f800c551..c15db2f808f 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/NumberParserImpl.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/NumberParserImpl.java @@ -2,14 +2,19 @@ // License & terms of use: http://www.unicode.org/copyright.html#License package com.ibm.icu.impl.number.parse; +import java.text.ParsePosition; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import com.ibm.icu.impl.number.AffixPatternProvider; import com.ibm.icu.impl.number.AffixUtils; +import com.ibm.icu.impl.number.CustomSymbolCurrency; +import com.ibm.icu.impl.number.DecimalFormatProperties; import com.ibm.icu.impl.number.MutablePatternModifier; +import com.ibm.icu.impl.number.Parse.ParseMode; import com.ibm.icu.impl.number.PatternStringParser; +import com.ibm.icu.impl.number.PropertiesAffixPatternProvider; import com.ibm.icu.number.NumberFormatter.SignDisplay; import com.ibm.icu.number.NumberFormatter.UnitWidth; import com.ibm.icu.text.DecimalFormatSymbols; @@ -23,7 +28,9 @@ import com.ibm.icu.util.ULocale; * */ public class NumberParserImpl { - public static NumberParserImpl createParserFromPattern(String pattern) { + public static NumberParserImpl createParserFromPattern(String pattern, boolean strictGrouping) { + // Temporary frontend for testing. + NumberParserImpl parser = new NumberParserImpl(); ULocale locale = ULocale.ENGLISH; DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale); @@ -32,10 +39,7 @@ public class NumberParserImpl { AffixPatternProvider provider = PatternStringParser.parseToPatternInfo(pattern); mod.setPatternInfo(provider); mod.setPatternAttributes(SignDisplay.AUTO, false); - mod.setSymbols(symbols, - Currency.getInstance("USD"), - UnitWidth.FULL_NAME, - null); + mod.setSymbols(symbols, Currency.getInstance("USD"), UnitWidth.FULL_NAME, null); int flags = 0; if (provider.containsSymbolType(AffixUtils.TYPE_PERCENT)) { flags |= ParsedNumber.FLAG_PERCENT; @@ -45,18 +49,77 @@ public class NumberParserImpl { } AffixMatcher.generateFromPatternModifier(mod, flags, parser); - parser.addMatcher(DecimalMatcher.getInstance(symbols)); + DecimalMatcher decimalMatcher = DecimalMatcher.getInstance(symbols); + decimalMatcher.requireGroupingMatch = strictGrouping; + parser.addMatcher(decimalMatcher); parser.addMatcher(WhitespaceMatcher.getInstance()); parser.addMatcher(new MinusSignMatcher()); parser.addMatcher(new ScientificMatcher(symbols)); parser.addMatcher(new CurrencyMatcher(locale)); - parser.setComparator(new Comparator() { - @Override - public int compare(ParsedNumber o1, ParsedNumber o2) { - return o1.charsConsumed - o2.charsConsumed; - } - }); + parser.freeze(); + return parser; + } + + public static Number parseStatic(String input, + ParsePosition ppos, + DecimalFormatProperties properties, + DecimalFormatSymbols symbols) { + NumberParserImpl parser = createParserFromProperties(properties, symbols); + ParsedNumber result = new ParsedNumber(); + parser.parse(input, true, result); + ppos.setIndex(result.charsConsumed); + return result.getDouble(); + } + + public static NumberParserImpl createParserFromProperties( + DecimalFormatProperties properties, + DecimalFormatSymbols symbols) { + NumberParserImpl parser = new NumberParserImpl(); + ULocale locale = symbols.getULocale(); + Currency currency = CustomSymbolCurrency.resolve(properties.getCurrency(), locale, symbols); + + ////////////////////// + /// AFFIX MATCHERS /// + ////////////////////// + + // Set up a pattern modifier with mostly defaults to generate AffixMatchers. + MutablePatternModifier mod = new MutablePatternModifier(false); + AffixPatternProvider provider = new PropertiesAffixPatternProvider(properties); + mod.setPatternInfo(provider); + mod.setPatternAttributes(SignDisplay.AUTO, false); + mod.setSymbols(symbols, currency, UnitWidth.SHORT, null); + + // Figure out which flags correspond to this pattern modifier. Note: negatives are taken care of in the + // generateFromPatternModifier function. + int flags = 0; + if (provider.containsSymbolType(AffixUtils.TYPE_PERCENT)) { + flags |= ParsedNumber.FLAG_PERCENT; + } + if (provider.containsSymbolType(AffixUtils.TYPE_PERMILLE)) { + flags |= ParsedNumber.FLAG_PERMILLE; + } + + AffixMatcher.generateFromPatternModifier(mod, flags, parser); + + /////////////////////////////// + /// OTHER STANDARD MATCHERS /// + /////////////////////////////// + + DecimalMatcher decimalMatcher = DecimalMatcher.getInstance(symbols); + decimalMatcher.groupingEnabled = properties.getGroupingSize() > 0; + decimalMatcher.requireGroupingMatch = properties.getParseMode() == ParseMode.STRICT; + parser.addMatcher(decimalMatcher); + parser.addMatcher(WhitespaceMatcher.getInstance()); + parser.addMatcher(new MinusSignMatcher()); + parser.addMatcher(new ScientificMatcher(symbols)); + + //////////////////////// + /// CURRENCY MATCHER /// + //////////////////////// + + parser.addMatcher(new CurrencyMatcher(locale)); + parser.freeze(); return parser; } @@ -67,6 +130,7 @@ public class NumberParserImpl { public NumberParserImpl() { matchers = new ArrayList(); + comparator = ParsedNumber.COMPARATOR; // default value frozen = false; } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/ParsedNumber.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/ParsedNumber.java index 3ab53f1aadf..ae847a28fd7 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/ParsedNumber.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/ParsedNumber.java @@ -2,7 +2,8 @@ // License & terms of use: http://www.unicode.org/copyright.html#License package com.ibm.icu.impl.number.parse; -import com.ibm.icu.impl.number.DecimalQuantity; +import java.util.Comparator; + import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD; /** @@ -12,17 +13,46 @@ import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD; public class ParsedNumber { public DecimalQuantity_DualStorageBCD quantity = null; + + /** + * The number of chars accepted during parsing. This is NOT necessarily the same as the StringSegment offset; "weak" + * chars, like whitespace, change the offset, but the charsConsumed is not touched until a "strong" char is + * encountered. + */ public int charsConsumed = 0; + + /** + * Boolean flags (see constants below). + */ public int flags = 0; + + /** + * The prefix string that got consumed. + */ public String prefix = null; + + /** + * The suffix string that got consumed. + */ public String suffix = null; - public int scientificAdjustment = 0; + + /** + * The currency that got consumed. + */ public String currencyCode = null; public static final int FLAG_NEGATIVE = 0x0001; public static final int FLAG_PERCENT = 0x0002; public static final int FLAG_PERMILLE = 0x0004; + /** A Comparator that favors ParsedNumbers with the most chars consumed. */ + public static final Comparator COMPARATOR = new Comparator() { + @Override + public int compare(ParsedNumber o1, ParsedNumber o2) { + return o1.charsConsumed - o2.charsConsumed; + } + }; + /** * @param other */ @@ -32,7 +62,6 @@ public class ParsedNumber { flags = other.flags; prefix = other.prefix; suffix = other.suffix; - scientificAdjustment = other.scientificAdjustment; currencyCode = other.currencyCode; } @@ -41,9 +70,7 @@ public class ParsedNumber { } public double getDouble() { - DecimalQuantity copy = quantity.createCopy(); - copy.adjustMagnitude(scientificAdjustment); - double d = copy.toDouble(); + double d = quantity.toDouble(); if (0 != (flags & FLAG_NEGATIVE)) { d = -d; } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/ScientificMatcher.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/ScientificMatcher.java index 1fae1595be2..2b4549289df 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/ScientificMatcher.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/ScientificMatcher.java @@ -11,10 +11,12 @@ import com.ibm.icu.text.DecimalFormatSymbols; public class ScientificMatcher implements NumberParseMatcher { private final String exponentSeparatorString; + private final String minusSignString; private final DecimalMatcher exponentMatcher; public ScientificMatcher(DecimalFormatSymbols symbols) { exponentSeparatorString = symbols.getExponentSeparator(); + minusSignString = symbols.getMinusSignString(); exponentMatcher = DecimalMatcher.getExponentInstance(symbols); } @@ -26,19 +28,33 @@ public class ScientificMatcher implements NumberParseMatcher { } // First match the scientific separator, and then match another number after it. - int overlap = segment.getCommonPrefixLength(exponentSeparatorString); - if (overlap == exponentSeparatorString.length()) { - // Full exponent separator match; try to match digits. - segment.adjustOffset(overlap); + int overlap1 = segment.getCommonPrefixLength(exponentSeparatorString); + if (overlap1 == exponentSeparatorString.length()) { + // Full exponent separator match; allow a sign, and then try to match digits. + segment.adjustOffset(overlap1); + int overlap2 = segment.getCommonPrefixLength(minusSignString); + boolean sign = false; + if (overlap2 == minusSignString.length()) { + sign = true; + segment.adjustOffset(overlap2); + } else if (overlap2 == segment.length()) { + // Partial sign match + return true; + } + int digitsOffset = segment.getOffset(); + int oldMagnitude = result.quantity.getMagnitude(); boolean digitsReturnValue = exponentMatcher.match(segment, result); + if (result.quantity.getMagnitude() != oldMagnitude && sign) { + result.quantity.adjustMagnitude(2*(oldMagnitude - result.quantity.getMagnitude())); + } if (segment.getOffset() == digitsOffset) { // No digits were matched; un-match the exponent separator. - segment.adjustOffset(-overlap); + segment.adjustOffset(-overlap1); } return digitsReturnValue; - } else if (overlap == segment.length()) { + } else if (overlap1 == segment.length()) { // Partial exponent separator match return true; } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/StrictMatcher.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/StrictMatcher.java new file mode 100644 index 00000000000..0f9b3905852 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/StrictMatcher.java @@ -0,0 +1,23 @@ +// © 2017 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.parse; + +/** + * @author sffc + * + */ +public class StrictMatcher implements NumberParseMatcher { + + @Override + public boolean match(StringSegment segment, ParsedNumber result) { + return false; + } + + @Override + public void postProcess(ParsedNumber result) { + if (result.prefix == null && result.suffix == null) { + // Do something? + } + } + +} diff --git a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberPropertyMapper.java b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberPropertyMapper.java index f1cdafe1dec..c07d88b7705 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/number/NumberPropertyMapper.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/number/NumberPropertyMapper.java @@ -5,16 +5,15 @@ package com.ibm.icu.number; import java.math.BigDecimal; import java.math.MathContext; -import com.ibm.icu.impl.StandardPlural; import com.ibm.icu.impl.number.AffixPatternProvider; -import com.ibm.icu.impl.number.AffixUtils; +import com.ibm.icu.impl.number.CurrencyPluralInfoAffixProvider; import com.ibm.icu.impl.number.CustomSymbolCurrency; import com.ibm.icu.impl.number.DecimalFormatProperties; import com.ibm.icu.impl.number.MacroProps; import com.ibm.icu.impl.number.MultiplierImpl; import com.ibm.icu.impl.number.Padder; import com.ibm.icu.impl.number.PatternStringParser; -import com.ibm.icu.impl.number.PatternStringParser.ParsedPatternInfo; +import com.ibm.icu.impl.number.PropertiesAffixPatternProvider; import com.ibm.icu.impl.number.RoundingUtils; import com.ibm.icu.number.NumberFormatter.DecimalSeparatorDisplay; import com.ibm.icu.number.NumberFormatter.SignDisplay; @@ -22,7 +21,6 @@ import com.ibm.icu.number.Rounder.FractionRounderImpl; import com.ibm.icu.number.Rounder.IncrementRounderImpl; import com.ibm.icu.number.Rounder.SignificantRounderImpl; import com.ibm.icu.text.CompactDecimalFormat.CompactStyle; -import com.ibm.icu.text.CurrencyPluralInfo; import com.ibm.icu.text.DecimalFormatSymbols; import com.ibm.icu.util.Currency; import com.ibm.icu.util.Currency.CurrencyUsage; @@ -60,7 +58,7 @@ final class NumberPropertyMapper { * @param symbols * The symbols associated with the property bag. * @param exportedProperties - * A property bag in which to store validated properties. + * A property bag in which to store validated properties. Used by some DecimalFormat getters. * @return A new MacroProps containing all of the information in the Properties. */ public static MacroProps oldToNew(DecimalFormatProperties properties, DecimalFormatSymbols symbols, @@ -334,178 +332,4 @@ final class NumberPropertyMapper { return macros; } - - private static class PropertiesAffixPatternProvider implements AffixPatternProvider { - private final String posPrefix; - private final String posSuffix; - private final String negPrefix; - private final String negSuffix; - - public PropertiesAffixPatternProvider(DecimalFormatProperties properties) { - // There are two ways to set affixes in DecimalFormat: via the pattern string (applyPattern), and via the - // explicit setters (setPositivePrefix and friends). The way to resolve the settings is as follows: - // - // 1) If the explicit setting is present for the field, use it. - // 2) Otherwise, follows UTS 35 rules based on the pattern string. - // - // Importantly, the explicit setters affect only the one field they override. If you set the positive - // prefix, that should not affect the negative prefix. Since it is impossible for the user of this class - // to know whether the origin for a string was the override or the pattern, we have to say that we always - // have a negative subpattern and perform all resolution logic here. - - // Convenience: Extract the properties into local variables. - // Variables are named with three chars: [p/n][p/s][o/p] - // [p/n] => p for positive, n for negative - // [p/s] => p for prefix, s for suffix - // [o/p] => o for escaped custom override string, p for pattern string - String ppo = AffixUtils.escape(properties.getPositivePrefix()); - String pso = AffixUtils.escape(properties.getPositiveSuffix()); - String npo = AffixUtils.escape(properties.getNegativePrefix()); - String nso = AffixUtils.escape(properties.getNegativeSuffix()); - String ppp = properties.getPositivePrefixPattern(); - String psp = properties.getPositiveSuffixPattern(); - String npp = properties.getNegativePrefixPattern(); - String nsp = properties.getNegativeSuffixPattern(); - - if (ppo != null) { - posPrefix = ppo; - } else if (ppp != null) { - posPrefix = ppp; - } else { - // UTS 35: Default positive prefix is empty string. - posPrefix = ""; - } - - if (pso != null) { - posSuffix = pso; - } else if (psp != null) { - posSuffix = psp; - } else { - // UTS 35: Default positive suffix is empty string. - posSuffix = ""; - } - - if (npo != null) { - negPrefix = npo; - } else if (npp != null) { - negPrefix = npp; - } else { - // UTS 35: Default negative prefix is "-" with positive prefix. - // Important: We prepend the "-" to the pattern, not the override! - negPrefix = ppp == null ? "-" : "-" + ppp; - } - - if (nso != null) { - negSuffix = nso; - } else if (nsp != null) { - negSuffix = nsp; - } else { - // UTS 35: Default negative prefix is the positive prefix. - negSuffix = psp == null ? "" : psp; - } - } - - @Override - public char charAt(int flags, int i) { - return getStringForFlags(flags).charAt(i); - } - - @Override - public int length(int flags) { - return getStringForFlags(flags).length(); - } - - private String getStringForFlags(int flags) { - boolean prefix = (flags & Flags.PREFIX) != 0; - boolean negative = (flags & Flags.NEGATIVE_SUBPATTERN) != 0; - if (prefix && negative) { - return negPrefix; - } else if (prefix) { - return posPrefix; - } else if (negative) { - return negSuffix; - } else { - return posSuffix; - } - } - - @Override - public boolean positiveHasPlusSign() { - return AffixUtils.containsType(posPrefix, AffixUtils.TYPE_PLUS_SIGN) - || AffixUtils.containsType(posSuffix, AffixUtils.TYPE_PLUS_SIGN); - } - - @Override - public boolean hasNegativeSubpattern() { - // See comments in the constructor for more information on why this is always true. - return true; - } - - @Override - public boolean negativeHasMinusSign() { - return AffixUtils.containsType(negPrefix, AffixUtils.TYPE_MINUS_SIGN) - || AffixUtils.containsType(negSuffix, AffixUtils.TYPE_MINUS_SIGN); - } - - @Override - public boolean hasCurrencySign() { - return AffixUtils.hasCurrencySymbols(posPrefix) || AffixUtils.hasCurrencySymbols(posSuffix) - || AffixUtils.hasCurrencySymbols(negPrefix) || AffixUtils.hasCurrencySymbols(negSuffix); - } - - @Override - public boolean containsSymbolType(int type) { - return AffixUtils.containsType(posPrefix, type) || AffixUtils.containsType(posSuffix, type) - || AffixUtils.containsType(negPrefix, type) || AffixUtils.containsType(negSuffix, type); - } - } - - private static class CurrencyPluralInfoAffixProvider implements AffixPatternProvider { - private final AffixPatternProvider[] affixesByPlural; - - public CurrencyPluralInfoAffixProvider(CurrencyPluralInfo cpi) { - affixesByPlural = new ParsedPatternInfo[StandardPlural.COUNT]; - for (StandardPlural plural : StandardPlural.VALUES) { - affixesByPlural[plural.ordinal()] = PatternStringParser - .parseToPatternInfo(cpi.getCurrencyPluralPattern(plural.getKeyword())); - } - } - - @Override - public char charAt(int flags, int i) { - int pluralOrdinal = (flags & Flags.PLURAL_MASK); - return affixesByPlural[pluralOrdinal].charAt(flags, i); - } - - @Override - public int length(int flags) { - int pluralOrdinal = (flags & Flags.PLURAL_MASK); - return affixesByPlural[pluralOrdinal].length(flags); - } - - @Override - public boolean positiveHasPlusSign() { - return affixesByPlural[StandardPlural.OTHER.ordinal()].positiveHasPlusSign(); - } - - @Override - public boolean hasNegativeSubpattern() { - return affixesByPlural[StandardPlural.OTHER.ordinal()].hasNegativeSubpattern(); - } - - @Override - public boolean negativeHasMinusSign() { - return affixesByPlural[StandardPlural.OTHER.ordinal()].negativeHasMinusSign(); - } - - @Override - public boolean hasCurrencySign() { - return affixesByPlural[StandardPlural.OTHER.ordinal()].hasCurrencySign(); - } - - @Override - public boolean containsSymbolType(int type) { - return affixesByPlural[StandardPlural.OTHER.ordinal()].containsSymbolType(type); - } - } } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/data/numberformattestspecification.txt b/icu4j/main/tests/core/src/com/ibm/icu/dev/data/numberformattestspecification.txt index 113473a2a57..96868920c68 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/data/numberformattestspecification.txt +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/data/numberformattestspecification.txt @@ -441,11 +441,11 @@ en_US 1 123,456 123456 en_US 0 123,456 123 en_US 1 123.456 123.456 en_US 0 123.456 123.456 -fr_FR 1 123,456 123.456 -fr_FR 0 123,456 123.456 +it_IT 1 123,456 123.456 +it_IT 0 123,456 123.456 // JDK returns 123 here; not sure why. -fr_FR 1 123.456 123456 K -fr_FR 0 123.456 123 +it_IT 1 123.456 123456 K +it_IT 0 123.456 123 test no grouping in pattern with parsing set pattern 0 @@ -736,7 +736,7 @@ parse output breaks (5,347.25) -5347.25 // J requires prefix and suffix for lenient parsing, but C doesn't 5,347.25 5347.25 JK -(5,347.25 -5347.25 J +(5,347.25 -5347.25 JP // S is successful at parsing this as -5347.25 in lenient mode -5,347.25 -5347.25 CJK +3.52E4 35200 @@ -750,7 +750,8 @@ parse output breaks (34,,25 E-1) -342.5 CJK // Spaces are not allowed after exponent symbol // C parses up to the E but J bails -(34 25E -1) -3425 JK +// P does not make the number negative +(34 25E -1) -3425 JKP +3.52EE4 3.52 +1,234,567.8901 1234567.8901 +1,23,4567.8901 1234567.8901 @@ -775,18 +776,19 @@ parse output breaks ( 19 45 ) -1945 JK (,,19,45) -1945 // C parses to the space, but J bails -(,,19 45) -19 J +// P makes the number positive +(,,19 45) -19 JP // J bails b/c comma different separator than space. C doesn't treat leading spaces // as a separator. -( 19,45) -1945 JK +( 19,45) -1945 JKP // J bails. Doesn't allow trailing separators when there is prefix and suffix. (,,19,45,) -1945 J // J bails on next 4 because J doesn't allow letters inside prefix and suffix. // C will parse up to the letter. -(,,19,45,d1) -1945 J -(,,19,45d1) -1945 J -( 19 45 d1) -1945 JK -( 19 45d1) -1945 JK +(,,19,45,d1) -1945 JP +(,,19,45d1) -1945 JP +( 19 45 d1) -1945 JKP +( 19 45d1) -1945 JKP // J does allow trailing separator before a decimal point (19,45,.25) -1945.25 // 2nd decimal points are ignored @@ -829,7 +831,7 @@ parse output breaks (65347.25) -65347.25 (65,347.25) -65347.25 // JDK does allow separators in the wrong place and parses as -5347.25 -(53,47.25) fail K +(53,47.25) fail KP // strict requires prefix or suffix, except in C 65,347.25 fail +3.52E4 35200 diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatDataDrivenTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatDataDrivenTest.java index 6cd17dfe19e..2f5b23a21a6 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatDataDrivenTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatDataDrivenTest.java @@ -7,6 +7,7 @@ import java.math.RoundingMode; import java.text.ParseException; import java.text.ParsePosition; +import org.junit.Ignore; import org.junit.Test; import com.ibm.icu.dev.test.TestUtil; @@ -16,6 +17,7 @@ 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.impl.number.parse.NumberParserImpl; import com.ibm.icu.number.LocalizedNumberFormatter; import com.ibm.icu.number.NumberFormatter; import com.ibm.icu.text.DecimalFormat; @@ -573,6 +575,100 @@ public class NumberFormatDataDrivenTest { } }; + /** + * Parsing, but no other features. + */ + private DataDrivenNumberFormatTestUtility.CodeUnderTest ICU60_Parsing = + new DataDrivenNumberFormatTestUtility.CodeUnderTest() { + + @Override + public Character Id() { + return 'P'; + } + + @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 = NumberParserImpl.parseStatic(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; + } + } + +// @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 = NumberParserImpl.parseStatic(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; +// } + }; + /** * All features except formatting. */ @@ -738,6 +834,7 @@ public class NumberFormatDataDrivenTest { }; @Test + @Ignore public void TestDataDrivenICU58() { // Android can't access DecimalFormat_ICU58 for testing (ticket #13283). if (TestUtil.getJavaVendor() == TestUtil.JavaVendor.Android) return; @@ -750,6 +847,7 @@ public class NumberFormatDataDrivenTest { // something may or may not work. However the test data assumes a specific // Java runtime version. We should probably disable this test case - #13372 @Test + @Ignore public void TestDataDrivenJDK() { // Android implements java.text.DecimalFormat with ICU4J (ticket #13322). // Oracle/OpenJDK 9's behavior is not exactly same with Oracle/OpenJDK 8. @@ -764,12 +862,20 @@ public class NumberFormatDataDrivenTest { } @Test + @Ignore public void TestDataDrivenICULatest_Format() { DataDrivenNumberFormatTestUtility.runFormatSuiteIncludingKnownFailures( "numberformattestspecification.txt", ICU60); } @Test + public void TestDataDrivenICULatest_Parsing() { + DataDrivenNumberFormatTestUtility.runFormatSuiteIncludingKnownFailures( + "numberformattestspecification.txt", ICU60_Parsing); + } + + @Test + @Ignore public void TestDataDrivenICULatest_Other() { DataDrivenNumberFormatTestUtility.runFormatSuiteIncludingKnownFailures( "numberformattestspecification.txt", ICU60_Other); diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberParserTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberParserTest.java index 0c9fdf3a7dd..fcc58aedae4 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberParserTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberParserTest.java @@ -22,6 +22,7 @@ public class NumberParserTest { // a) Flags: // --- Bit 0x01 => Test greedy implementation // --- Bit 0x02 => Test slow implementation + // --- Bit 0x04 => Test strict grouping separators // b) Input string // c) Pattern // d) Expected chars consumed @@ -34,13 +35,13 @@ public class NumberParserTest { { 3, "𝟱𝟭𝟰𝟮𝟯x", "0", 10, 51423. }, { 3, " 𝟱𝟭𝟰𝟮𝟯", "0", 11, 51423. }, { 3, "𝟱𝟭𝟰𝟮𝟯 ", "0", 10, 51423. }, - { 3, "𝟱𝟭,𝟰𝟮𝟯", "0", 11, 51423. }, - { 3, "𝟳𝟴,𝟵𝟱𝟭,𝟰𝟮𝟯", "0", 18, 78951423. }, - { 3, "𝟳𝟴,𝟵𝟱𝟭.𝟰𝟮𝟯", "0", 18, 78951.423 }, - { 3, "𝟳𝟴,𝟬𝟬𝟬", "0", 11, 78000. }, - { 3, "𝟳𝟴,𝟬𝟬𝟬.𝟬𝟬𝟬", "0", 18, 78000. }, - { 3, "𝟳𝟴,𝟬𝟬𝟬.𝟬𝟮𝟯", "0", 18, 78000.023 }, - { 3, "𝟳𝟴.𝟬𝟬𝟬.𝟬𝟮𝟯", "0", 11, 78. }, + { 7, "𝟱𝟭,𝟰𝟮𝟯", "0", 11, 51423. }, + { 7, "𝟳𝟴,𝟵𝟱𝟭,𝟰𝟮𝟯", "0", 18, 78951423. }, + { 7, "𝟳𝟴,𝟵𝟱𝟭.𝟰𝟮𝟯", "0", 18, 78951.423 }, + { 7, "𝟳𝟴,𝟬𝟬𝟬", "0", 11, 78000. }, + { 7, "𝟳𝟴,𝟬𝟬𝟬.𝟬𝟬𝟬", "0", 18, 78000. }, + { 7, "𝟳𝟴,𝟬𝟬𝟬.𝟬𝟮𝟯", "0", 18, 78000.023 }, + { 7, "𝟳𝟴.𝟬𝟬𝟬.𝟬𝟮𝟯", "0", 11, 78. }, { 3, "-𝟱𝟭𝟰𝟮𝟯", "0", 11, -51423. }, { 3, "-𝟱𝟭𝟰𝟮𝟯-", "0", 11, -51423. }, { 3, "a51423US dollars", "a0¤¤¤", 16, 51423. }, @@ -51,6 +52,7 @@ public class NumberParserTest { { 1, "a40b", "a0'0b'", 3, 40. }, // greedy code path thinks "40" is the number { 2, "a40b", "a0'0b'", 4, 4. }, // slow code path find the suffix "0b" { 3, "𝟱.𝟭𝟰𝟮E𝟯", "0", 12, 5142. }, + { 3, "𝟱.𝟭𝟰𝟮E-𝟯", "0", 13, 0.005142 }, { 3, "5,142.50 Canadian dollars", "0", 25, 5142.5 }, { 3, "0", "0", 1, 0.0 } }; @@ -60,7 +62,7 @@ public class NumberParserTest { String pattern = (String) cas[2]; int expectedCharsConsumed = (Integer) cas[3]; double resultDouble = (Double) cas[4]; - NumberParserImpl parser = NumberParserImpl.createParserFromPattern(pattern); + NumberParserImpl parser = NumberParserImpl.createParserFromPattern(pattern, false); String message = "Input <" + input + "> Parser " + parser; if (0 != (flags & 0x01)) { @@ -80,6 +82,16 @@ public class NumberParserTest { assertEquals(message, resultDouble, resultObject.getDouble(), 0.0); assertEquals(message, expectedCharsConsumed, resultObject.charsConsumed); } + + if (0 != (flags & 0x04)) { + // Test with strict separators + parser = NumberParserImpl.createParserFromPattern(pattern, true); + ParsedNumber resultObject = new ParsedNumber(); + parser.parse(input, true, resultObject); + assertNotNull(message, resultObject.quantity); + assertEquals(message, resultDouble, resultObject.getDouble(), 0.0); + assertEquals(message, expectedCharsConsumed, resultObject.charsConsumed); + } } } } -- 2.40.0