From 5a316af9f109152c7bcd0c2f657c603e98f86bf8 Mon Sep 17 00:00:00 2001 From: Roland Mesde Date: Thu, 13 Nov 2025 10:48:14 -0800 Subject: [PATCH 1/3] Backport 6238bc8da2abe7a1f0cdd98c0af01e9ba1869ec3 --- .../java/text/CompactNumberFormat.java | 2 +- .../Format/NumberFormat/LenientParseTest.java | 454 ++++++++++++++++++ 2 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 test/jdk/java/text/Format/NumberFormat/LenientParseTest.java diff --git a/src/java.base/share/classes/java/text/CompactNumberFormat.java b/src/java.base/share/classes/java/text/CompactNumberFormat.java index 5b95b945799..237c81f2222 100644 --- a/src/java.base/share/classes/java/text/CompactNumberFormat.java +++ b/src/java.base/share/classes/java/text/CompactNumberFormat.java @@ -1681,7 +1681,7 @@ public Number parse(String text, ParsePosition pos) { // If parse integer only is true and the parsing is broken at // decimal point, then pass/ignore all digits and move pointer // at the start of suffix, to process the suffix part - if (isParseIntegerOnly() + if (isParseIntegerOnly() && position < text.length() && text.charAt(position) == symbols.getDecimalSeparator()) { position++; // Pass decimal character for (; position < text.length(); ++position) { diff --git a/test/jdk/java/text/Format/NumberFormat/LenientParseTest.java b/test/jdk/java/text/Format/NumberFormat/LenientParseTest.java new file mode 100644 index 00000000000..41f8961ff32 --- /dev/null +++ b/test/jdk/java/text/Format/NumberFormat/LenientParseTest.java @@ -0,0 +1,454 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8327640 8331485 8333456 + * @summary Test suite for NumberFormat parsing when lenient. + * @run junit/othervm -Duser.language=en -Duser.country=US LenientParseTest + * @run junit/othervm -Duser.language=ja -Duser.country=JP LenientParseTest + * @run junit/othervm -Duser.language=zh -Duser.country=CN LenientParseTest + * @run junit/othervm -Duser.language=tr -Duser.country=TR LenientParseTest + * @run junit/othervm -Duser.language=de -Duser.country=DE LenientParseTest + * @run junit/othervm -Duser.language=fr -Duser.country=FR LenientParseTest + * @run junit/othervm -Duser.language=ar -Duser.country=AR LenientParseTest + */ + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.text.CompactNumberFormat; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.ParsePosition; +import java.util.Locale; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +// Tests lenient parsing, this is done by testing the NumberFormat factory instances +// against a number of locales with different formatting conventions. The locales +// used all use a grouping size of 3. When lenient, parsing only fails +// if the prefix and/or suffix are not found, or the first character after the +// prefix is un-parseable. The tested locales all use groupingSize of 3. +public class LenientParseTest { + + // Used to retrieve the locale's expected symbols + private static final DecimalFormatSymbols dfs = + new DecimalFormatSymbols(Locale.getDefault()); + private static final DecimalFormat dFmt = (DecimalFormat) + NumberFormat.getNumberInstance(Locale.getDefault()); + private static final DecimalFormat cFmt = + (DecimalFormat) NumberFormat.getCurrencyInstance(Locale.getDefault()); + private static final DecimalFormat pFmt = + (DecimalFormat) NumberFormat.getPercentInstance(Locale.getDefault()); + private static final CompactNumberFormat cmpctFmt = + (CompactNumberFormat) NumberFormat.getCompactNumberInstance(Locale.getDefault(), + NumberFormat.Style.SHORT); + + // All NumberFormats should parse leniently (which is the default) + static { + // To effectively test compactNumberFormat, these should be set accordingly + cmpctFmt.setParseIntegerOnly(false); + cmpctFmt.setGroupingUsed(true); + } + + // ---- NumberFormat tests ---- + // Test prefix/suffix behavior with a predefined DecimalFormat + // Non-localized, only run once + @ParameterizedTest + @MethodSource("badParseStrings") + @EnabledIfSystemProperty(named = "user.language", matches = "en") + public void numFmtFailParseTest(String toParse, int expectedErrorIndex) { + // Format with grouping size = 3, prefix = a, suffix = b + DecimalFormat nonLocalizedDFmt = new DecimalFormat("a#,#00.00b"); + failParse(nonLocalizedDFmt, toParse, expectedErrorIndex); + } + + // All input Strings should parse fully and return the expected value. + // Expected index should be the length of the parse string, since it parses fully + @ParameterizedTest + @MethodSource("validFullParseStrings") + public void numFmtSuccessFullParseTest(String toParse, double expectedValue) { + assertEquals(expectedValue, successParse(dFmt, toParse, toParse.length())); + } + + // All input Strings should parse partially and return expected value + // with the expected final index + @ParameterizedTest + @MethodSource("validPartialParseStrings") + public void numFmtSuccessPartialParseTest(String toParse, double expectedValue, + int expectedIndex) { + assertEquals(expectedValue, successParse(dFmt, toParse, expectedIndex)); + } + + // Parse partially due to no grouping + @ParameterizedTest + @MethodSource("noGroupingParseStrings") + public void numFmtStrictGroupingNotUsed(String toParse, double expectedValue, int expectedIndex) { + dFmt.setGroupingUsed(false); + assertEquals(expectedValue, successParse(dFmt, toParse, expectedIndex)); + dFmt.setGroupingUsed(true); + } + + // Parse partially due to integer only + @ParameterizedTest + @MethodSource("integerOnlyParseStrings") + public void numFmtStrictIntegerOnlyUsed(String toParse, int expectedValue, int expectedIndex) { + dFmt.setParseIntegerOnly(true); + assertEquals(expectedValue, successParse(dFmt, toParse, expectedIndex)); + dFmt.setParseIntegerOnly(false); + } + + @Test // Non-localized, only run once + @EnabledIfSystemProperty(named = "user.language", matches = "en") + public void badExponentParseNumberFormatTest() { + // Some fmt, with an "E" exponent string + DecimalFormat fmt = (DecimalFormat) NumberFormat.getNumberInstance(Locale.US); + // Upon non-numeric in exponent, parse will still successfully complete + // but index should end on the last valid char in exponent + assertEquals(1.23E45, successParse(fmt, "1.23E45.123", 7)); + assertEquals(1.23E45, successParse(fmt, "1.23E45.", 7)); + assertEquals(1.23E45, successParse(fmt, "1.23E45FOO3222", 7)); + } + + // ---- CurrencyFormat tests ---- + // All input Strings should pass and return expected value. + @ParameterizedTest + @MethodSource("currencyValidFullParseStrings") + public void currFmtSuccessParseTest(String toParse, double expectedValue) { + assertEquals(expectedValue, successParse(cFmt, toParse, toParse.length())); + } + + // Strings may parse partially or fail. This is because the mapped + // data may cause the error to occur before the suffix can be found, (if the locale + // uses a suffix). + @ParameterizedTest + @MethodSource("currencyValidPartialParseStrings") + public void currFmtParseTest(String toParse, double expectedValue, + int expectedIndex) { + if (cFmt.getPositiveSuffix().length() > 0) { + // Since the error will occur before suffix is found, exception is thrown. + failParse(cFmt, toParse, expectedIndex); + } else { + // Empty suffix, thus even if the error occurs, we have already found the + // prefix, and simply parse partially + assertEquals(expectedValue, successParse(cFmt, toParse, expectedIndex)); + } + } + + // ---- PercentFormat tests ---- + // All input Strings should pass and return expected value. + @ParameterizedTest + @MethodSource("percentValidFullParseStrings") + public void percentFmtSuccessParseTest(String toParse, double expectedValue) { + assertEquals(expectedValue, successParse(pFmt, toParse, toParse.length())); + } + + // Strings may parse partially or fail. This is because the mapped + // data may cause the error to occur before the suffix can be found, (if the locale + // uses a suffix). + @ParameterizedTest + @MethodSource("percentValidPartialParseStrings") + public void percentFmtParseTest(String toParse, double expectedValue, + int expectedIndex) { + if (pFmt.getPositiveSuffix().length() > 0) { + // Since the error will occur before suffix is found, exception is thrown. + failParse(pFmt, toParse, expectedIndex); + } else { + // Empty suffix, thus even if the error occurs, we have already found the + // prefix, and simply parse partially + assertEquals(expectedValue, successParse(pFmt, toParse, expectedIndex)); + } + } + + // ---- CompactNumberFormat tests ---- + // Can match to both the decimalFormat patterns and the compact patterns + // Unlike the other tests, this test is only ran against the US Locale and + // tests against data built with the thousands format (K). + @ParameterizedTest + @MethodSource("compactValidPartialParseStrings") + @EnabledIfSystemProperty(named = "user.language", matches = "en") + public void compactFmtFailParseTest(String toParse, double expectedValue, int expectedErrorIndex) { + assertEquals(expectedValue, successParse(cmpctFmt, toParse, expectedErrorIndex)); + } + + + @ParameterizedTest + @MethodSource("compactValidFullParseStrings") + @EnabledIfSystemProperty(named = "user.language", matches = "en") + public void compactFmtSuccessParseTest(String toParse, double expectedValue) { + assertEquals(expectedValue, successParse(cmpctFmt, toParse, toParse.length())); + } + + // 8333456: Parse values with no compact suffix -> which allows parsing to iterate + // position to the same value as string length which throws + // StringIndexOutOfBoundsException upon charAt invocation + @ParameterizedTest + @MethodSource("compactValidNoSuffixParseStrings") + @EnabledIfSystemProperty(named = "user.language", matches = "en") + public void compactFmtSuccessParseIntOnlyTest(String toParse, double expectedValue) { + cmpctFmt.setParseIntegerOnly(true); + assertEquals(expectedValue, successParse(cmpctFmt, toParse, toParse.length())); + cmpctFmt.setParseIntegerOnly(false); + } + + // ---- Helper test methods ---- + + // Method is used when a String should parse successfully. This does not indicate + // that the entire String was used, however. The index and errorIndex values + // should be as expected. + private double successParse(NumberFormat fmt, String toParse, int expectedIndex) { + Number parsedValue = assertDoesNotThrow(() -> fmt.parse(toParse)); + ParsePosition pp = new ParsePosition(0); + assertDoesNotThrow(() -> fmt.parse(toParse, pp)); + assertEquals(-1, pp.getErrorIndex(), + "ParsePosition ErrorIndex is not in correct location"); + assertEquals(expectedIndex, pp.getIndex(), + "ParsePosition Index is not in correct location"); + return parsedValue.doubleValue(); + } + + // Method is used when a String should fail parsing. Indicated by either a thrown + // ParseException, or null is returned depending on which parse method is invoked. + // errorIndex should be as expected. + private void failParse(NumberFormat fmt, String toParse, int expectedErrorIndex) { + ParsePosition pp = new ParsePosition(0); + assertThrows(ParseException.class, () -> fmt.parse(toParse)); + assertNull(fmt.parse(toParse, pp)); + assertEquals(expectedErrorIndex, pp.getErrorIndex()); + } + + // ---- Data Providers ---- + + // Strings that should fail when parsed leniently. + // Given as Arguments + // Non-localized data. For reference, the pattern of nonLocalizedDFmt is + // "a#,#00.00b" + private static Stream badParseStrings() { + return Stream.of( + // No prefix + Arguments.of("1,1b", 0), + // No suffix + Arguments.of("a1,11", 5), + // Digit does not follow the last grouping separator + // Current behavior fails on the grouping separator + Arguments.of("a1,11,z", 5), + // No suffix after grouping + Arguments.of("a1,11,", 5), + // No prefix and suffix + Arguments.of("1,11", 0), + // First character after prefix is un-parseable + // Behavior is to expect error index at 0, not 1 + Arguments.of("ac1,11", 0)); + } + + // These data providers use US locale grouping and decimal separators + // for readability, however, the data is tested against multiple locales + // and is converted appropriately at runtime. + + // Strings that should parse successfully, and consume the entire String + // Form of Arguments(parseString, expectedParsedNumber) + private static Stream validFullParseStrings() { + return Stream.of( + // Many subsequent grouping symbols + Arguments.of("1,,,1", 11d), + Arguments.of("11,,,11,,,11", 111111d), + // Bad grouping size (with decimal) + Arguments.of("1,1.", 11d), + Arguments.of("11,111,11.", 1111111d), + // Improper grouping size (with decimal and digits after) + Arguments.of("1,1.1", 11.1d), + Arguments.of("1,11.1", 111.1d), + Arguments.of("1,1111.1", 11111.1d), + Arguments.of("11,111,11.1", 1111111.1d), + // Starts with grouping symbol + Arguments.of(",111,,1,1", 11111d), + Arguments.of(",1", 1d), + Arguments.of(",,1", 1d), + // Leading Zeros (not digits) + Arguments.of("000,1,1", 11d), + Arguments.of("000,111,11,,1", 111111d), + Arguments.of("0,000,1,,1,1", 111d), + Arguments.of("1,234.00", 1234d), + Arguments.of("1,234.0", 1234d), + Arguments.of("1,234.", 1234d), + Arguments.of("1,234.00123", 1234.00123d), + Arguments.of("1,234.012", 1234.012d), + Arguments.of("1,234.224", 1234.224d), + Arguments.of("1", 1d), + Arguments.of("10", 10d), + Arguments.of("100", 100d), + Arguments.of("1000", 1000d), + Arguments.of("1,000", 1000d), + Arguments.of("10,000", 10000d), + Arguments.of("10000", 10000d), + Arguments.of("100,000", 100000d), + Arguments.of("1,000,000", 1000000d), + Arguments.of("10,000,000", 10000000d)) + .map(args -> Arguments.of( + localizeText(String.valueOf(args.get()[0])), args.get()[1])); + } + + // Strings that should parse successfully, but do not use the entire String + // Form of Arguments(parseString, expectedParsedNumber, expectedIndex) + private static Stream validPartialParseStrings() { + return Stream.of( + // End with grouping symbol + Arguments.of("11,", 11d, 2), + Arguments.of("11,,", 11d, 3), + Arguments.of("11,,,", 11d, 4), + // Random chars that aren't the expected symbols + Arguments.of("1,1P111", 11d, 3), + Arguments.of("1.1P111", 1.1d, 3), + Arguments.of("1P,1111", 1d, 1), + Arguments.of("1P.1111", 1d, 1), + Arguments.of("1,1111P", 11111d, 6), + // Grouping occurs after decimal separator) + Arguments.of("1.11,11", 1.11d, 4), + Arguments.of("1.,11,11", 1d, 2)) + .map(args -> Arguments.of( + localizeText(String.valueOf(args.get()[0])), args.get()[1], args.get()[2])); + } + + // Test data input for when parse integer only is true + // Form of Arguments(parseString, expectedParsedNumber, expectedIndex) + private static Stream integerOnlyParseStrings() { + return Stream.of( + Arguments.of("1234.1234", 1234, 4), + Arguments.of("1234.12", 1234, 4), + Arguments.of("1234.1a", 1234, 4), + Arguments.of("1234.", 1234, 4)) + .map(args -> Arguments.of( + localizeText(String.valueOf(args.get()[0])), args.get()[1], args.get()[2])); + } + + // Test data input for when no grouping is true + // Form of Arguments(parseString, expectedParsedNumber, expectedIndex) + private static Stream noGroupingParseStrings() { + return Stream.of( + Arguments.of("12,34", 12d, 2), + Arguments.of("1234,", 1234d, 4), + Arguments.of("123,456.789", 123d, 3)) + .map(args -> Arguments.of( + localizeText(String.valueOf(args.get()[0])), args.get()[1], args.get()[2])); + } + + // Mappers for respective data providers to adjust values accordingly + // Localized percent prefix/suffix is added, with appropriate expected values + // adjusted. Expected parsed number should be divided by 100. + private static Stream percentValidPartialParseStrings() { + return validPartialParseStrings().map(args -> + Arguments.of(pFmt.getPositivePrefix() + args.get()[0] + pFmt.getPositiveSuffix(), + (double) args.get()[1] / 100, (int) args.get()[2] + pFmt.getPositivePrefix().length()) + ); + } + + private static Stream percentValidFullParseStrings() { + return validFullParseStrings().map(args -> Arguments.of( + pFmt.getPositivePrefix() + args.get()[0] + pFmt.getPositiveSuffix(), + (double) args.get()[1] / 100) + ); + } + + // Mappers for respective data providers to adjust values accordingly + // Localized percent prefix/suffix is added, with appropriate expected values + // adjusted. Separators replaced for monetary versions. + private static Stream currencyValidPartialParseStrings() { + return validPartialParseStrings().map(args -> Arguments.of( + cFmt.getPositivePrefix() + String.valueOf(args.get()[0]) + .replace(dfs.getGroupingSeparator(), dfs.getMonetaryGroupingSeparator()) + .replace(dfs.getDecimalSeparator(), dfs.getMonetaryDecimalSeparator()) + + cFmt.getPositiveSuffix(), + args.get()[1], (int) args.get()[2] + cFmt.getPositivePrefix().length()) + ); + } + + private static Stream currencyValidFullParseStrings() { + return validFullParseStrings().map(args -> Arguments.of( + cFmt.getPositivePrefix() + String.valueOf(args.get()[0]) + .replace(dfs.getGroupingSeparator(), dfs.getMonetaryGroupingSeparator()) + .replace(dfs.getDecimalSeparator(), dfs.getMonetaryDecimalSeparator()) + + cFmt.getPositiveSuffix(), + args.get()[1]) + ); + } + + // Compact Pattern Data Provider provides test input for both DecimalFormat patterns + // and the compact patterns. As there is no method to retrieve compact patterns, + // thus test only against US English locale, and use a hard coded K - 1000 + private static Stream compactValidPartialParseStrings() { + return Stream.concat(validPartialParseStrings().map(args -> Arguments.of(args.get()[0], + args.get()[1], args.get()[2])), validPartialParseStrings().map(args -> Arguments.of(args.get()[0] + "K", + args.get()[1], args.get()[2])) + ); + } + + private static Stream compactValidFullParseStrings() { + return Stream.concat(validFullParseStrings().map(args -> Arguments.of(args.get()[0], + args.get()[1])), validFullParseStrings().map(args -> Arguments.of(args.get()[0] + "K", + (double)args.get()[1] * 1000.0)) + ); + } + + // No compact suffixes + private static Stream compactValidNoSuffixParseStrings() { + return Stream.of( + Arguments.of("5", 5), + Arguments.of("50", 50), + Arguments.of("50.", 50), + Arguments.of("5,000", 5000), + Arguments.of("5,000.", 5000), + Arguments.of("5,000.00", 5000) + ); + } + + // Replace the grouping and decimal separators with localized variants + // Used during localization of data + private static String localizeText(String text) { + // As this is a single pass conversion, this is safe for multiple replacement, + // even if a ',' could be a decimal separator for a locale. + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == ',') { + sb.append(dfs.getGroupingSeparator()); + } else if (c == '.') { + sb.append(dfs.getDecimalSeparator()); + } else if (c == '0') { + sb.append(dfs.getZeroDigit()); + } else { + sb.append(c); + } + } + return sb.toString(); + } +} From 0c2007ca7d8afa2f2c06a3715b589addec51b2cd Mon Sep 17 00:00:00 2001 From: Roland Mesde Date: Tue, 18 Nov 2025 14:03:02 -0800 Subject: [PATCH 2/3] Update test --- .../text/Format/NumberFormat/Bug8333456.java | 90 ++++ .../Format/NumberFormat/LenientParseTest.java | 454 ------------------ 2 files changed, 90 insertions(+), 454 deletions(-) create mode 100644 test/jdk/java/text/Format/NumberFormat/Bug8333456.java delete mode 100644 test/jdk/java/text/Format/NumberFormat/LenientParseTest.java diff --git a/test/jdk/java/text/Format/NumberFormat/Bug8333456.java b/test/jdk/java/text/Format/NumberFormat/Bug8333456.java new file mode 100644 index 00000000000..eb55ed4fc2d --- /dev/null +++ b/test/jdk/java/text/Format/NumberFormat/Bug8333456.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8333456 + * @summary Make sure that CompactNumberFormat integer parsing doesn't fail + * when there is no suffix. + * @run junit Bug8333456 + */ +import java.text.CompactNumberFormat; +import java.text.NumberFormat; +import java.text.ParsePosition; +import java.util.Locale; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class Bug8333456 { + + private static final CompactNumberFormat cmpctFmt = + (CompactNumberFormat) NumberFormat.getCompactNumberInstance(Locale.US, + NumberFormat.Style.SHORT); + + // No compact suffixes + private static final Stream compactValidNoSuffixParseStrings() { + return Stream.of( + Arguments.of("5", 5), + Arguments.of("50", 50), + Arguments.of("50.", 50), + Arguments.of("5,000", 5000), + Arguments.of("5,000.", 5000), + Arguments.of("5,000.00", 5000) + ); + } + + // 8333456: Parse values with no compact suffix -> which allows parsing to iterate + // position to the same value as string length which throws + // StringIndexOutOfBoundsException upon charAt invocation + @ParameterizedTest + @MethodSource("compactValidNoSuffixParseStrings") + @EnabledIfSystemProperty(named = "user.language", matches = "en") + public void compactFmtSuccessParseIntOnlyTest(String toParse, double expectedValue) { + cmpctFmt.setParseIntegerOnly(true); + cmpctFmt.setGroupingUsed(true); + assertEquals(expectedValue, successParse(cmpctFmt, toParse, toParse.length())); + cmpctFmt.setParseIntegerOnly(false); + } + + // Method is used when a String should parse successfully. This does not indicate + // that the entire String was used, however. The index and errorIndex values + // should be as expected. + private double successParse(NumberFormat fmt, String toParse, int expectedIndex) { + Number parsedValue = assertDoesNotThrow(() -> fmt.parse(toParse)); + ParsePosition pp = new ParsePosition(0); + assertDoesNotThrow(() -> fmt.parse(toParse, pp)); + assertEquals(-1, pp.getErrorIndex(), + "ParsePosition ErrorIndex is not in correct location"); + assertEquals(expectedIndex, pp.getIndex(), + "ParsePosition Index is not in correct location"); + return parsedValue.doubleValue(); + } +} diff --git a/test/jdk/java/text/Format/NumberFormat/LenientParseTest.java b/test/jdk/java/text/Format/NumberFormat/LenientParseTest.java deleted file mode 100644 index 41f8961ff32..00000000000 --- a/test/jdk/java/text/Format/NumberFormat/LenientParseTest.java +++ /dev/null @@ -1,454 +0,0 @@ -/* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -/* - * @test - * @bug 8327640 8331485 8333456 - * @summary Test suite for NumberFormat parsing when lenient. - * @run junit/othervm -Duser.language=en -Duser.country=US LenientParseTest - * @run junit/othervm -Duser.language=ja -Duser.country=JP LenientParseTest - * @run junit/othervm -Duser.language=zh -Duser.country=CN LenientParseTest - * @run junit/othervm -Duser.language=tr -Duser.country=TR LenientParseTest - * @run junit/othervm -Duser.language=de -Duser.country=DE LenientParseTest - * @run junit/othervm -Duser.language=fr -Duser.country=FR LenientParseTest - * @run junit/othervm -Duser.language=ar -Duser.country=AR LenientParseTest - */ - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfSystemProperty; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import java.text.CompactNumberFormat; -import java.text.DecimalFormat; -import java.text.DecimalFormatSymbols; -import java.text.NumberFormat; -import java.text.ParseException; -import java.text.ParsePosition; -import java.util.Locale; -import java.util.stream.Stream; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -// Tests lenient parsing, this is done by testing the NumberFormat factory instances -// against a number of locales with different formatting conventions. The locales -// used all use a grouping size of 3. When lenient, parsing only fails -// if the prefix and/or suffix are not found, or the first character after the -// prefix is un-parseable. The tested locales all use groupingSize of 3. -public class LenientParseTest { - - // Used to retrieve the locale's expected symbols - private static final DecimalFormatSymbols dfs = - new DecimalFormatSymbols(Locale.getDefault()); - private static final DecimalFormat dFmt = (DecimalFormat) - NumberFormat.getNumberInstance(Locale.getDefault()); - private static final DecimalFormat cFmt = - (DecimalFormat) NumberFormat.getCurrencyInstance(Locale.getDefault()); - private static final DecimalFormat pFmt = - (DecimalFormat) NumberFormat.getPercentInstance(Locale.getDefault()); - private static final CompactNumberFormat cmpctFmt = - (CompactNumberFormat) NumberFormat.getCompactNumberInstance(Locale.getDefault(), - NumberFormat.Style.SHORT); - - // All NumberFormats should parse leniently (which is the default) - static { - // To effectively test compactNumberFormat, these should be set accordingly - cmpctFmt.setParseIntegerOnly(false); - cmpctFmt.setGroupingUsed(true); - } - - // ---- NumberFormat tests ---- - // Test prefix/suffix behavior with a predefined DecimalFormat - // Non-localized, only run once - @ParameterizedTest - @MethodSource("badParseStrings") - @EnabledIfSystemProperty(named = "user.language", matches = "en") - public void numFmtFailParseTest(String toParse, int expectedErrorIndex) { - // Format with grouping size = 3, prefix = a, suffix = b - DecimalFormat nonLocalizedDFmt = new DecimalFormat("a#,#00.00b"); - failParse(nonLocalizedDFmt, toParse, expectedErrorIndex); - } - - // All input Strings should parse fully and return the expected value. - // Expected index should be the length of the parse string, since it parses fully - @ParameterizedTest - @MethodSource("validFullParseStrings") - public void numFmtSuccessFullParseTest(String toParse, double expectedValue) { - assertEquals(expectedValue, successParse(dFmt, toParse, toParse.length())); - } - - // All input Strings should parse partially and return expected value - // with the expected final index - @ParameterizedTest - @MethodSource("validPartialParseStrings") - public void numFmtSuccessPartialParseTest(String toParse, double expectedValue, - int expectedIndex) { - assertEquals(expectedValue, successParse(dFmt, toParse, expectedIndex)); - } - - // Parse partially due to no grouping - @ParameterizedTest - @MethodSource("noGroupingParseStrings") - public void numFmtStrictGroupingNotUsed(String toParse, double expectedValue, int expectedIndex) { - dFmt.setGroupingUsed(false); - assertEquals(expectedValue, successParse(dFmt, toParse, expectedIndex)); - dFmt.setGroupingUsed(true); - } - - // Parse partially due to integer only - @ParameterizedTest - @MethodSource("integerOnlyParseStrings") - public void numFmtStrictIntegerOnlyUsed(String toParse, int expectedValue, int expectedIndex) { - dFmt.setParseIntegerOnly(true); - assertEquals(expectedValue, successParse(dFmt, toParse, expectedIndex)); - dFmt.setParseIntegerOnly(false); - } - - @Test // Non-localized, only run once - @EnabledIfSystemProperty(named = "user.language", matches = "en") - public void badExponentParseNumberFormatTest() { - // Some fmt, with an "E" exponent string - DecimalFormat fmt = (DecimalFormat) NumberFormat.getNumberInstance(Locale.US); - // Upon non-numeric in exponent, parse will still successfully complete - // but index should end on the last valid char in exponent - assertEquals(1.23E45, successParse(fmt, "1.23E45.123", 7)); - assertEquals(1.23E45, successParse(fmt, "1.23E45.", 7)); - assertEquals(1.23E45, successParse(fmt, "1.23E45FOO3222", 7)); - } - - // ---- CurrencyFormat tests ---- - // All input Strings should pass and return expected value. - @ParameterizedTest - @MethodSource("currencyValidFullParseStrings") - public void currFmtSuccessParseTest(String toParse, double expectedValue) { - assertEquals(expectedValue, successParse(cFmt, toParse, toParse.length())); - } - - // Strings may parse partially or fail. This is because the mapped - // data may cause the error to occur before the suffix can be found, (if the locale - // uses a suffix). - @ParameterizedTest - @MethodSource("currencyValidPartialParseStrings") - public void currFmtParseTest(String toParse, double expectedValue, - int expectedIndex) { - if (cFmt.getPositiveSuffix().length() > 0) { - // Since the error will occur before suffix is found, exception is thrown. - failParse(cFmt, toParse, expectedIndex); - } else { - // Empty suffix, thus even if the error occurs, we have already found the - // prefix, and simply parse partially - assertEquals(expectedValue, successParse(cFmt, toParse, expectedIndex)); - } - } - - // ---- PercentFormat tests ---- - // All input Strings should pass and return expected value. - @ParameterizedTest - @MethodSource("percentValidFullParseStrings") - public void percentFmtSuccessParseTest(String toParse, double expectedValue) { - assertEquals(expectedValue, successParse(pFmt, toParse, toParse.length())); - } - - // Strings may parse partially or fail. This is because the mapped - // data may cause the error to occur before the suffix can be found, (if the locale - // uses a suffix). - @ParameterizedTest - @MethodSource("percentValidPartialParseStrings") - public void percentFmtParseTest(String toParse, double expectedValue, - int expectedIndex) { - if (pFmt.getPositiveSuffix().length() > 0) { - // Since the error will occur before suffix is found, exception is thrown. - failParse(pFmt, toParse, expectedIndex); - } else { - // Empty suffix, thus even if the error occurs, we have already found the - // prefix, and simply parse partially - assertEquals(expectedValue, successParse(pFmt, toParse, expectedIndex)); - } - } - - // ---- CompactNumberFormat tests ---- - // Can match to both the decimalFormat patterns and the compact patterns - // Unlike the other tests, this test is only ran against the US Locale and - // tests against data built with the thousands format (K). - @ParameterizedTest - @MethodSource("compactValidPartialParseStrings") - @EnabledIfSystemProperty(named = "user.language", matches = "en") - public void compactFmtFailParseTest(String toParse, double expectedValue, int expectedErrorIndex) { - assertEquals(expectedValue, successParse(cmpctFmt, toParse, expectedErrorIndex)); - } - - - @ParameterizedTest - @MethodSource("compactValidFullParseStrings") - @EnabledIfSystemProperty(named = "user.language", matches = "en") - public void compactFmtSuccessParseTest(String toParse, double expectedValue) { - assertEquals(expectedValue, successParse(cmpctFmt, toParse, toParse.length())); - } - - // 8333456: Parse values with no compact suffix -> which allows parsing to iterate - // position to the same value as string length which throws - // StringIndexOutOfBoundsException upon charAt invocation - @ParameterizedTest - @MethodSource("compactValidNoSuffixParseStrings") - @EnabledIfSystemProperty(named = "user.language", matches = "en") - public void compactFmtSuccessParseIntOnlyTest(String toParse, double expectedValue) { - cmpctFmt.setParseIntegerOnly(true); - assertEquals(expectedValue, successParse(cmpctFmt, toParse, toParse.length())); - cmpctFmt.setParseIntegerOnly(false); - } - - // ---- Helper test methods ---- - - // Method is used when a String should parse successfully. This does not indicate - // that the entire String was used, however. The index and errorIndex values - // should be as expected. - private double successParse(NumberFormat fmt, String toParse, int expectedIndex) { - Number parsedValue = assertDoesNotThrow(() -> fmt.parse(toParse)); - ParsePosition pp = new ParsePosition(0); - assertDoesNotThrow(() -> fmt.parse(toParse, pp)); - assertEquals(-1, pp.getErrorIndex(), - "ParsePosition ErrorIndex is not in correct location"); - assertEquals(expectedIndex, pp.getIndex(), - "ParsePosition Index is not in correct location"); - return parsedValue.doubleValue(); - } - - // Method is used when a String should fail parsing. Indicated by either a thrown - // ParseException, or null is returned depending on which parse method is invoked. - // errorIndex should be as expected. - private void failParse(NumberFormat fmt, String toParse, int expectedErrorIndex) { - ParsePosition pp = new ParsePosition(0); - assertThrows(ParseException.class, () -> fmt.parse(toParse)); - assertNull(fmt.parse(toParse, pp)); - assertEquals(expectedErrorIndex, pp.getErrorIndex()); - } - - // ---- Data Providers ---- - - // Strings that should fail when parsed leniently. - // Given as Arguments - // Non-localized data. For reference, the pattern of nonLocalizedDFmt is - // "a#,#00.00b" - private static Stream badParseStrings() { - return Stream.of( - // No prefix - Arguments.of("1,1b", 0), - // No suffix - Arguments.of("a1,11", 5), - // Digit does not follow the last grouping separator - // Current behavior fails on the grouping separator - Arguments.of("a1,11,z", 5), - // No suffix after grouping - Arguments.of("a1,11,", 5), - // No prefix and suffix - Arguments.of("1,11", 0), - // First character after prefix is un-parseable - // Behavior is to expect error index at 0, not 1 - Arguments.of("ac1,11", 0)); - } - - // These data providers use US locale grouping and decimal separators - // for readability, however, the data is tested against multiple locales - // and is converted appropriately at runtime. - - // Strings that should parse successfully, and consume the entire String - // Form of Arguments(parseString, expectedParsedNumber) - private static Stream validFullParseStrings() { - return Stream.of( - // Many subsequent grouping symbols - Arguments.of("1,,,1", 11d), - Arguments.of("11,,,11,,,11", 111111d), - // Bad grouping size (with decimal) - Arguments.of("1,1.", 11d), - Arguments.of("11,111,11.", 1111111d), - // Improper grouping size (with decimal and digits after) - Arguments.of("1,1.1", 11.1d), - Arguments.of("1,11.1", 111.1d), - Arguments.of("1,1111.1", 11111.1d), - Arguments.of("11,111,11.1", 1111111.1d), - // Starts with grouping symbol - Arguments.of(",111,,1,1", 11111d), - Arguments.of(",1", 1d), - Arguments.of(",,1", 1d), - // Leading Zeros (not digits) - Arguments.of("000,1,1", 11d), - Arguments.of("000,111,11,,1", 111111d), - Arguments.of("0,000,1,,1,1", 111d), - Arguments.of("1,234.00", 1234d), - Arguments.of("1,234.0", 1234d), - Arguments.of("1,234.", 1234d), - Arguments.of("1,234.00123", 1234.00123d), - Arguments.of("1,234.012", 1234.012d), - Arguments.of("1,234.224", 1234.224d), - Arguments.of("1", 1d), - Arguments.of("10", 10d), - Arguments.of("100", 100d), - Arguments.of("1000", 1000d), - Arguments.of("1,000", 1000d), - Arguments.of("10,000", 10000d), - Arguments.of("10000", 10000d), - Arguments.of("100,000", 100000d), - Arguments.of("1,000,000", 1000000d), - Arguments.of("10,000,000", 10000000d)) - .map(args -> Arguments.of( - localizeText(String.valueOf(args.get()[0])), args.get()[1])); - } - - // Strings that should parse successfully, but do not use the entire String - // Form of Arguments(parseString, expectedParsedNumber, expectedIndex) - private static Stream validPartialParseStrings() { - return Stream.of( - // End with grouping symbol - Arguments.of("11,", 11d, 2), - Arguments.of("11,,", 11d, 3), - Arguments.of("11,,,", 11d, 4), - // Random chars that aren't the expected symbols - Arguments.of("1,1P111", 11d, 3), - Arguments.of("1.1P111", 1.1d, 3), - Arguments.of("1P,1111", 1d, 1), - Arguments.of("1P.1111", 1d, 1), - Arguments.of("1,1111P", 11111d, 6), - // Grouping occurs after decimal separator) - Arguments.of("1.11,11", 1.11d, 4), - Arguments.of("1.,11,11", 1d, 2)) - .map(args -> Arguments.of( - localizeText(String.valueOf(args.get()[0])), args.get()[1], args.get()[2])); - } - - // Test data input for when parse integer only is true - // Form of Arguments(parseString, expectedParsedNumber, expectedIndex) - private static Stream integerOnlyParseStrings() { - return Stream.of( - Arguments.of("1234.1234", 1234, 4), - Arguments.of("1234.12", 1234, 4), - Arguments.of("1234.1a", 1234, 4), - Arguments.of("1234.", 1234, 4)) - .map(args -> Arguments.of( - localizeText(String.valueOf(args.get()[0])), args.get()[1], args.get()[2])); - } - - // Test data input for when no grouping is true - // Form of Arguments(parseString, expectedParsedNumber, expectedIndex) - private static Stream noGroupingParseStrings() { - return Stream.of( - Arguments.of("12,34", 12d, 2), - Arguments.of("1234,", 1234d, 4), - Arguments.of("123,456.789", 123d, 3)) - .map(args -> Arguments.of( - localizeText(String.valueOf(args.get()[0])), args.get()[1], args.get()[2])); - } - - // Mappers for respective data providers to adjust values accordingly - // Localized percent prefix/suffix is added, with appropriate expected values - // adjusted. Expected parsed number should be divided by 100. - private static Stream percentValidPartialParseStrings() { - return validPartialParseStrings().map(args -> - Arguments.of(pFmt.getPositivePrefix() + args.get()[0] + pFmt.getPositiveSuffix(), - (double) args.get()[1] / 100, (int) args.get()[2] + pFmt.getPositivePrefix().length()) - ); - } - - private static Stream percentValidFullParseStrings() { - return validFullParseStrings().map(args -> Arguments.of( - pFmt.getPositivePrefix() + args.get()[0] + pFmt.getPositiveSuffix(), - (double) args.get()[1] / 100) - ); - } - - // Mappers for respective data providers to adjust values accordingly - // Localized percent prefix/suffix is added, with appropriate expected values - // adjusted. Separators replaced for monetary versions. - private static Stream currencyValidPartialParseStrings() { - return validPartialParseStrings().map(args -> Arguments.of( - cFmt.getPositivePrefix() + String.valueOf(args.get()[0]) - .replace(dfs.getGroupingSeparator(), dfs.getMonetaryGroupingSeparator()) - .replace(dfs.getDecimalSeparator(), dfs.getMonetaryDecimalSeparator()) - + cFmt.getPositiveSuffix(), - args.get()[1], (int) args.get()[2] + cFmt.getPositivePrefix().length()) - ); - } - - private static Stream currencyValidFullParseStrings() { - return validFullParseStrings().map(args -> Arguments.of( - cFmt.getPositivePrefix() + String.valueOf(args.get()[0]) - .replace(dfs.getGroupingSeparator(), dfs.getMonetaryGroupingSeparator()) - .replace(dfs.getDecimalSeparator(), dfs.getMonetaryDecimalSeparator()) - + cFmt.getPositiveSuffix(), - args.get()[1]) - ); - } - - // Compact Pattern Data Provider provides test input for both DecimalFormat patterns - // and the compact patterns. As there is no method to retrieve compact patterns, - // thus test only against US English locale, and use a hard coded K - 1000 - private static Stream compactValidPartialParseStrings() { - return Stream.concat(validPartialParseStrings().map(args -> Arguments.of(args.get()[0], - args.get()[1], args.get()[2])), validPartialParseStrings().map(args -> Arguments.of(args.get()[0] + "K", - args.get()[1], args.get()[2])) - ); - } - - private static Stream compactValidFullParseStrings() { - return Stream.concat(validFullParseStrings().map(args -> Arguments.of(args.get()[0], - args.get()[1])), validFullParseStrings().map(args -> Arguments.of(args.get()[0] + "K", - (double)args.get()[1] * 1000.0)) - ); - } - - // No compact suffixes - private static Stream compactValidNoSuffixParseStrings() { - return Stream.of( - Arguments.of("5", 5), - Arguments.of("50", 50), - Arguments.of("50.", 50), - Arguments.of("5,000", 5000), - Arguments.of("5,000.", 5000), - Arguments.of("5,000.00", 5000) - ); - } - - // Replace the grouping and decimal separators with localized variants - // Used during localization of data - private static String localizeText(String text) { - // As this is a single pass conversion, this is safe for multiple replacement, - // even if a ',' could be a decimal separator for a locale. - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - if (c == ',') { - sb.append(dfs.getGroupingSeparator()); - } else if (c == '.') { - sb.append(dfs.getDecimalSeparator()); - } else if (c == '0') { - sb.append(dfs.getZeroDigit()); - } else { - sb.append(c); - } - } - return sb.toString(); - } -} From 354993cebe33dde3061b4c08d2b71bf7279ad278 Mon Sep 17 00:00:00 2001 From: Roland Mesde Date: Tue, 18 Nov 2025 14:08:20 -0800 Subject: [PATCH 3/3] Fix formatting --- .../text/Format/NumberFormat/Bug8333456.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/jdk/java/text/Format/NumberFormat/Bug8333456.java b/test/jdk/java/text/Format/NumberFormat/Bug8333456.java index eb55ed4fc2d..1c150162e43 100644 --- a/test/jdk/java/text/Format/NumberFormat/Bug8333456.java +++ b/test/jdk/java/text/Format/NumberFormat/Bug8333456.java @@ -28,6 +28,7 @@ * when there is no suffix. * @run junit Bug8333456 */ + import java.text.CompactNumberFormat; import java.text.NumberFormat; import java.text.ParsePosition; @@ -52,15 +53,15 @@ public class Bug8333456 { // No compact suffixes private static final Stream compactValidNoSuffixParseStrings() { return Stream.of( - Arguments.of("5", 5), - Arguments.of("50", 50), - Arguments.of("50.", 50), - Arguments.of("5,000", 5000), - Arguments.of("5,000.", 5000), - Arguments.of("5,000.00", 5000) + Arguments.of("5", 5), + Arguments.of("50", 50), + Arguments.of("50.", 50), + Arguments.of("5,000", 5000), + Arguments.of("5,000.", 5000), + Arguments.of("5,000.00", 5000) ); } - + // 8333456: Parse values with no compact suffix -> which allows parsing to iterate // position to the same value as string length which throws // StringIndexOutOfBoundsException upon charAt invocation @@ -73,7 +74,7 @@ public void compactFmtSuccessParseIntOnlyTest(String toParse, double expectedVal assertEquals(expectedValue, successParse(cmpctFmt, toParse, toParse.length())); cmpctFmt.setParseIntegerOnly(false); } - + // Method is used when a String should parse successfully. This does not indicate // that the entire String was used, however. The index and errorIndex values // should be as expected.