diff --git a/icu4c/source/i18n/numparse_impl.cpp b/icu4c/source/i18n/numparse_impl.cpp index db1586bca48..acc68394a1c 100644 --- a/icu4c/source/i18n/numparse_impl.cpp +++ b/icu4c/source/i18n/numparse_impl.cpp @@ -150,6 +150,23 @@ NumberParserImpl::createParserFromProperties(const number::impl::DecimalFormatPr parser->addMatcher(parser->fLocalMatchers.currency = {currencySymbols, symbols, status}); } + /////////////// + /// PERCENT /// + /////////////// + + // 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 (affixProvider->containsSymbolType(AffixPatternType::TYPE_PERCENT, status)) { + parser->addMatcher(parser->fLocalMatchers.percent = {symbols}); + // causes number to be always scaled by 100: + parser->addMatcher(parser->fLocalValidators.percentFlags = {ResultFlags::FLAG_PERCENT}); + } + if (affixProvider->containsSymbolType(AffixPatternType::TYPE_PERMILLE, status)) { + parser->addMatcher(parser->fLocalMatchers.permille = {symbols}); + // causes number to be always scaled by 1000: + parser->addMatcher(parser->fLocalValidators.permilleFlags = {ResultFlags::FLAG_PERMILLE}); + } + /////////////////////////////// /// OTHER STANDARD MATCHERS /// /////////////////////////////// @@ -157,8 +174,6 @@ NumberParserImpl::createParserFromProperties(const number::impl::DecimalFormatPr if (!isStrict) { parser->addMatcher(parser->fLocalMatchers.plusSign = {symbols, false}); parser->addMatcher(parser->fLocalMatchers.minusSign = {symbols, false}); - parser->addMatcher(parser->fLocalMatchers.percent = {symbols}); - parser->addMatcher(parser->fLocalMatchers.permille = {symbols}); } parser->addMatcher(parser->fLocalMatchers.nan = {symbols}); parser->addMatcher(parser->fLocalMatchers.infinity = {symbols}); diff --git a/icu4c/source/i18n/numparse_impl.h b/icu4c/source/i18n/numparse_impl.h index 748b9415ecb..4677ae14d88 100644 --- a/icu4c/source/i18n/numparse_impl.h +++ b/icu4c/source/i18n/numparse_impl.h @@ -80,6 +80,8 @@ class NumberParserImpl : public MutableMatcherCollection { RequireExponentValidator exponent; RequireNumberValidator number; MultiplierParseHandler multiplier; + FlagHandler percentFlags; + FlagHandler permilleFlags; } fLocalValidators; explicit NumberParserImpl(parse_flags_t parseFlags); diff --git a/icu4c/source/i18n/numparse_validators.cpp b/icu4c/source/i18n/numparse_validators.cpp index 724b0cf0313..a36a15f5120 100644 --- a/icu4c/source/i18n/numparse_validators.cpp +++ b/icu4c/source/i18n/numparse_validators.cpp @@ -80,4 +80,16 @@ UnicodeString RequireNumberValidator::toString() const { } +FlagHandler::FlagHandler(result_flags_t flags) + : fFlags(flags) {} + +void FlagHandler::postProcess(ParsedNumber& result) const { + result.flags |= fFlags; +} + +UnicodeString FlagHandler::toString() const { + return u""; +} + + #endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/i18n/numparse_validators.h b/icu4c/source/i18n/numparse_validators.h index d3bc63aceb3..c27890ae11d 100644 --- a/icu4c/source/i18n/numparse_validators.h +++ b/icu4c/source/i18n/numparse_validators.h @@ -97,6 +97,24 @@ class MultiplierParseHandler : public ValidationMatcher, public UMemory { }; +/** + * Unconditionally applies a given set of flags to the ParsedNumber in the post-processing step. + */ +class FlagHandler : public ValidationMatcher, public UMemory { + public: + FlagHandler() = default; + + FlagHandler(result_flags_t flags); + + void postProcess(ParsedNumber& result) const U_OVERRIDE; + + UnicodeString toString() const U_OVERRIDE; + + private: + result_flags_t fFlags; +}; + + } // namespace impl } // namespace numparse U_NAMESPACE_END diff --git a/icu4c/source/test/intltest/numfmtst.cpp b/icu4c/source/test/intltest/numfmtst.cpp index 3d26e1a6b6c..0d07750a4ea 100644 --- a/icu4c/source/test/intltest/numfmtst.cpp +++ b/icu4c/source/test/intltest/numfmtst.cpp @@ -655,6 +655,7 @@ void NumberFormatTest::runIndexedTest( int32_t index, UBool exec, const char* &n TESTCASE_AUTO(Test11735_ExceptionIssue); TESTCASE_AUTO(Test11035_FormatCurrencyAmount); TESTCASE_AUTO(Test11318_DoubleConversion); + TESTCASE_AUTO(TestParsePercentRegression); TESTCASE_AUTO_END; } @@ -9029,4 +9030,35 @@ void NumberFormatTest::Test11318_DoubleConversion() { assertEquals("Should render all digits", u"999,999,999,999,999.9", appendTo); } +void NumberFormatTest::TestParsePercentRegression() { + IcuTestErrorCode status(*this, "TestParsePercentRegression"); + LocalPointer df1((DecimalFormat*) NumberFormat::createInstance("en", status)); + LocalPointer df2((DecimalFormat*) NumberFormat::createPercentInstance("en", status)); + df1->setLenient(TRUE); + df2->setLenient(TRUE); + + { + ParsePosition ppos; + Formattable result; + df1->parse("50%", result, ppos); + assertEquals("df1 should accept a number but not the percent sign", 2, ppos.getIndex()); + assertEquals("df1 should return the number as 50", 50.0, result.getDouble(status)); + } + { + ParsePosition ppos; + Formattable result; + df2->parse("50%", result, ppos); + assertEquals("df2 should accept the percent sign", 3, ppos.getIndex()); + assertEquals("df2 should return the number as 0.5", 0.5, result.getDouble(status)); + } + { + ParsePosition ppos; + Formattable result; + df2->parse("50", result, ppos); + assertEquals("df2 should return the number as 0.5 even though the percent sign is missing", + 0.5, + result.getDouble(status)); + } +} + #endif /* #if !UCONFIG_NO_FORMATTING */ diff --git a/icu4c/source/test/intltest/numfmtst.h b/icu4c/source/test/intltest/numfmtst.h index d71b8c4880f..e17b825d03f 100644 --- a/icu4c/source/test/intltest/numfmtst.h +++ b/icu4c/source/test/intltest/numfmtst.h @@ -222,6 +222,7 @@ class NumberFormatTest: public CalendarTimeZoneTest { void Test11735_ExceptionIssue(); void Test11035_FormatCurrencyAmount(); void Test11318_DoubleConversion(); + void TestParsePercentRegression(); private: UBool testFormattableAsUFormattable(const char *file, int line, Formattable &f); diff --git a/icu4c/source/test/testdata/numberformattestspecification.txt b/icu4c/source/test/testdata/numberformattestspecification.txt index 629073890c3..7b375222ca8 100644 --- a/icu4c/source/test/testdata/numberformattestspecification.txt +++ b/icu4c/source/test/testdata/numberformattestspecification.txt @@ -1543,8 +1543,8 @@ begin parse output breaks 55% 0.55 // J and K get null -// P requires the symbol to be present and gets 55 -55 0.55 CJKP +// C and P scale by 100 even if the percent sign is not present +55 0.55 JK test trailing grouping separators in pattern // This test is for #13115 diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/FlagHandler.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/FlagHandler.java new file mode 100644 index 00000000000..37d39113ad7 --- /dev/null +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/parse/FlagHandler.java @@ -0,0 +1,28 @@ +// © 2018 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html#License +package com.ibm.icu.impl.number.parse; + +/** + * Unconditionally applies a given set of flags to the ParsedNumber in the post-processing step. + */ +public class FlagHandler extends ValidationMatcher { + + public static final FlagHandler PERCENT = new FlagHandler(ParsedNumber.FLAG_PERCENT); + public static final FlagHandler PERMILLE = new FlagHandler(ParsedNumber.FLAG_PERMILLE); + + private final int flags; + + private FlagHandler(int flags) { + this.flags = flags; + } + + @Override + public void postProcess(ParsedNumber result) { + result.flags |= flags; + } + + @Override + public String toString() { + return ""; + } +} 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 bff15dce37e..3d41c37ce78 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 @@ -9,6 +9,7 @@ import java.util.List; import com.ibm.icu.impl.StringSegment; 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; @@ -195,6 +196,23 @@ public class NumberParserImpl { parser.addMatcher(CombinedCurrencyMatcher.getInstance(currency, symbols)); } + /////////////// + /// PERCENT /// + /////////////// + + // 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 (affixProvider.containsSymbolType(AffixUtils.TYPE_PERCENT)) { + parser.addMatcher(PercentMatcher.getInstance(symbols)); + // causes number to be always scaled by 100: + parser.addMatcher(FlagHandler.PERCENT); + } + if (affixProvider.containsSymbolType(AffixUtils.TYPE_PERMILLE)) { + parser.addMatcher(PermilleMatcher.getInstance(symbols)); + // causes number to be always scaled by 1000: + parser.addMatcher(FlagHandler.PERMILLE); + } + /////////////////////////////// /// OTHER STANDARD MATCHERS /// /////////////////////////////// @@ -202,8 +220,6 @@ public class NumberParserImpl { if (!isStrict) { parser.addMatcher(PlusSignMatcher.getInstance(symbols, false)); parser.addMatcher(MinusSignMatcher.getInstance(symbols, false)); - parser.addMatcher(PercentMatcher.getInstance(symbols)); - parser.addMatcher(PermilleMatcher.getInstance(symbols)); } parser.addMatcher(NanMatcher.getInstance(symbols, parseFlags)); parser.addMatcher(InfinityMatcher.getInstance(symbols)); 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 35731ff8b7b..d1e71388d3e 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 @@ -1545,8 +1545,8 @@ begin parse output breaks 55% 0.55 // J and K get null -// P requires the symbol to be present and gets 55 -55 0.55 JKP +// C and P scale by 100 even if the percent sign is not present +55 0.55 JK test trailing grouping separators in pattern // This test is for #13115 diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTest.java index dbbdc13210f..b442d3b3029 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTest.java @@ -5637,7 +5637,7 @@ public class NumberFormatTest extends TestFmwk { assertEquals("Should consume the trailing bidi since it is in the symbol", 5, ppos.getIndex()); ppos.setIndex(0); result = df.parse("-42a\u200E ", ppos); - assertEquals("Should not parse as percent", new Long(-42), result); + assertEquals("Should parse as percent", -0.42, result.doubleValue()); assertEquals("Should not consume the trailing bidi or whitespace", 4, ppos.getIndex()); // A few more cases based on the docstring: @@ -6075,4 +6075,32 @@ public class NumberFormatTest extends TestFmwk { DecimalFormat df = new DecimalFormat("-0", DecimalFormatSymbols.getInstance(ULocale.ENGLISH)); expect2(df, -5, "--5"); } + + @Test + public void testParsePercentRegression() { + DecimalFormat df1 = (DecimalFormat) NumberFormat.getInstance(ULocale.ENGLISH); + DecimalFormat df2 = (DecimalFormat) NumberFormat.getPercentInstance(ULocale.ENGLISH); + df1.setParseStrict(false); + df2.setParseStrict(false); + + { + ParsePosition ppos = new ParsePosition(0); + Number result = df1.parse("50%", ppos); + assertEquals("df1 should accept a number but not the percent sign", 2, ppos.getIndex()); + assertEquals("df1 should return the number as 50", 50.0, result.doubleValue()); + } + { + ParsePosition ppos = new ParsePosition(0); + Number result = df2.parse("50%", ppos); + assertEquals("df2 should accept the percent sign", 3, ppos.getIndex()); + assertEquals("df2 should return the number as 0.5", 0.5, result.doubleValue()); + } + { + ParsePosition ppos = new ParsePosition(0); + Number result = df2.parse("50", ppos); + assertEquals("df2 should return the number as 0.5 even though the percent sign is missing", + 0.5, + result.doubleValue()); + } + } }