From 3595aa074edfb03c64f47a45d7d582d5ac8b30b8 Mon Sep 17 00:00:00 2001 From: Roland Mesde Date: Mon, 17 Nov 2025 10:18:03 -0800 Subject: [PATCH 1/2] Backport ffb0867e2c07b41cb7124e11fe6cf63d9471f0d2 --- .../classes/java/text/DecimalFormat.java | 83 +++- .../DecimalFormat/LargeExponentsTest.java | 193 ++++++++ .../Format/NumberFormat/LenientParseTest.java | 430 ++++++++++++++++++ 3 files changed, 692 insertions(+), 14 deletions(-) create mode 100644 test/jdk/java/text/Format/DecimalFormat/LargeExponentsTest.java create mode 100644 test/jdk/java/text/Format/NumberFormat/LenientParseTest.java diff --git a/src/java.base/share/classes/java/text/DecimalFormat.java b/src/java.base/share/classes/java/text/DecimalFormat.java index 3e0f1e3246f..73604ef7e25 100644 --- a/src/java.base/share/classes/java/text/DecimalFormat.java +++ b/src/java.base/share/classes/java/text/DecimalFormat.java @@ -2433,9 +2433,12 @@ int subparseNumber(String text, int position, symbols.getGroupingSeparator(); String exponentString = symbols.getExponentSeparator(); boolean sawDecimal = false; - boolean sawExponent = false; boolean sawDigit = false; - int exponent = 0; // Set to the exponent value, if any + // Storing as long allows us to maintain accuracy of exponent + // when the exponent value as well as the decimalAt nears + // Integer.MAX/MIN value. However, the final expressed value is an int + long exponent = 0; + boolean[] expStat = new boolean[STATUS_LENGTH]; // We have to track digitCount ourselves, because digits.count will // pin when the maximum allowable digits is reached. @@ -2505,21 +2508,27 @@ int subparseNumber(String text, int position, // require that they be followed by a digit. Otherwise // we backup and reprocess them. backup = position; - } else if (checkExponent && !isExponent && text.regionMatches(position, exponentString, 0, exponentString.length()) - && !sawExponent) { + } else if (checkExponent && !isExponent + && text.regionMatches(position, exponentString, 0, exponentString.length())) { // Process the exponent by recursively calling this method. ParsePosition pos = new ParsePosition(position + exponentString.length()); - boolean[] stat = new boolean[STATUS_LENGTH]; DigitList exponentDigits = new DigitList(); - if (subparse(text, pos, "", symbols.getMinusSignText(), exponentDigits, true, stat) && - exponentDigits.fitsIntoLong(stat[STATUS_POSITIVE], true)) { - position = pos.index; // Advance past the exponent - exponent = (int)exponentDigits.getLong(); - if (!stat[STATUS_POSITIVE]) { - exponent = -exponent; + if (subparse(text, pos, "", symbols.getMinusSignText(), exponentDigits, true, expStat)) { + // We parse the exponent with isExponent == true, thus fitsIntoLong() + // only returns false here if the exponent DigitList value exceeds + // Long.MAX_VALUE. We do not need to worry about false being + // returned for faulty values as they are ignored by DigitList. + if (exponentDigits.fitsIntoLong(expStat[STATUS_POSITIVE], true)) { + exponent = exponentDigits.getLong(); + if (!expStat[STATUS_POSITIVE]) { + exponent = -exponent; + } + } else { + exponent = expStat[STATUS_POSITIVE] ? + Long.MAX_VALUE : Long.MIN_VALUE; } - sawExponent = true; + position = pos.index; // Advance past the exponent } break; // Whether we fail or succeed, we exit this loop } else { @@ -2537,7 +2546,9 @@ int subparseNumber(String text, int position, } // Adjust for exponent, if any - digits.decimalAt += exponent; + if (exponent != 0) { + digits.decimalAt = shiftDecimalAt(digits.decimalAt, exponent); + } // If none of the text string was recognized. For example, parse // "x" with pattern "#0.00" (return index and error index both 0) @@ -2548,7 +2559,52 @@ int subparseNumber(String text, int position, } } return position; + } + // Calculate the final decimal position based off the exponent value + // and the existing decimalAt position. If overflow/underflow, the value + // should be set as either Integer.MAX/MIN + private int shiftDecimalAt(int decimalAt, long exponent) { + try { + exponent = Math.addExact(decimalAt, exponent); + } catch (ArithmeticException ex) { + // If we under/overflow a Long do not bother with the decimalAt + // As it can only shift up to Integer.MAX/MIN which has no affect + if (exponent > 0 && decimalAt > 0) { + return Integer.MAX_VALUE; + } else { + return Integer.MIN_VALUE; + } + } + try { + decimalAt = Math.toIntExact(exponent); + } catch (ArithmeticException ex) { + decimalAt = exponent > 0 ? Integer.MAX_VALUE : Integer.MIN_VALUE; + } + return decimalAt; + } + + // Checks to make sure grouping size is not violated. Used when strict. + private boolean isGroupingViolation(int pos, int prevGroupingPos) { + return isGroupingUsed() && // Only violates if using grouping + // Checks if a previous grouping symbol was seen. + prevGroupingPos != -groupingSize && + // The check itself, - 1 to account for grouping/decimal symbol + pos - 1 != prevGroupingPos + groupingSize; + } + + // Calculates the index that violated the grouping size + // Violation can be over or under the grouping size + // under - Current group has a grouping size of less than the expected + // over - Current group has a grouping size of more than the expected + private int groupingViolationIndex(int pos, int prevGroupingPos) { + // Both examples assume grouping size of 3 and 0 indexed + // under ex: "1,23,4". (4) OR "1,,2". (2) When under, violating char is grouping symbol + // over ex: "1,2345,6. (5) When over, violating char is the excess digit + // This method is only evaluated when a grouping symbol is found, thus + // we can take the minimum of either the current pos, or where we expect + // the current group to have ended + return Math.min(pos, prevGroupingPos + groupingSize + 1); } /** @@ -2566,7 +2622,6 @@ public DecimalFormatSymbols getDecimalFormatSymbols() { } } - /** * Sets the decimal format symbols, which is generally not changed * by the programmer or user. diff --git a/test/jdk/java/text/Format/DecimalFormat/LargeExponentsTest.java b/test/jdk/java/text/Format/DecimalFormat/LargeExponentsTest.java new file mode 100644 index 00000000000..c49bad2edef --- /dev/null +++ b/test/jdk/java/text/Format/DecimalFormat/LargeExponentsTest.java @@ -0,0 +1,193 @@ +/* + * 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 8331485 + * @summary Ensure correctness when parsing large (+/-) exponent values that + * exceed Integer.MAX_VALUE and Long.MAX_VALUE. + * @run junit/othervm --add-opens java.base/java.text=ALL-UNNAMED LargeExponentsTest + */ + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.ParsePosition; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +// We prevent odd results when parsing large exponent values by ensuring +// that we properly handle overflow in the implementation of DigitList +public class LargeExponentsTest { + + // Exponent symbol is 'E' + private static final NumberFormat FMT = NumberFormat.getInstance(Locale.US); + + // Check that the parsed value and parse position index are both equal to the expected values. + // We are mainly checking that an exponent > Integer.MAX_VALUE no longer + // parses to 0 and that an exponent > Long.MAX_VALUE no longer parses to the mantissa. + @ParameterizedTest + @MethodSource({"largeExponentValues", "smallExponentValues", "bugReportValues", "edgeCases"}) + public void overflowTest(String parseString, Double expectedValue) throws ParseException { + checkParse(parseString, expectedValue); + checkParseWithPP(parseString, expectedValue); + } + + // A separate white-box test to avoid the memory consumption of testing cases + // when the String is near Integer.MAX_LENGTH + @ParameterizedTest + @MethodSource + public void largeDecimalAtExponentTest(int expected, int decimalAt, long expVal) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + DecimalFormat df = new DecimalFormat(); + Method m = df.getClass().getDeclaredMethod( + "shiftDecimalAt", int.class, long.class); + m.setAccessible(true); + assertEquals(expected, m.invoke(df, decimalAt, expVal)); + } + + // Cases where we can test behavior when the String is near Integer.MAX_LENGTH + private static Stream largeDecimalAtExponentTest() { + return Stream.of( + // Equivalent to testing Arguments.of("0."+"0".repeat(Integer.MAX_VALUE-20)+"1"+"E2147483650", 1.0E22) + // This is an absurdly long decimal string with length close to Integer.MAX_VALUE + // where the decimal position correctly negates the exponent value, even if it exceeds Integer.MAX_VALUE + Arguments.of(23, -(Integer.MAX_VALUE-20), 3L+Integer.MAX_VALUE), + Arguments.of(-23, Integer.MAX_VALUE-20, -(3L+Integer.MAX_VALUE)), + Arguments.of(Integer.MIN_VALUE, -(Integer.MAX_VALUE-20), -(3L+Integer.MAX_VALUE)), + Arguments.of(Integer.MAX_VALUE, Integer.MAX_VALUE-20, 3L+Integer.MAX_VALUE), + Arguments.of(Integer.MAX_VALUE, -(Integer.MAX_VALUE-20), Long.MAX_VALUE), + Arguments.of(Integer.MIN_VALUE, Integer.MAX_VALUE-20, Long.MIN_VALUE) + ); + } + + // Checks the parse(String, ParsePosition) method + public void checkParse(String parseString, Double expectedValue) { + ParsePosition pp = new ParsePosition(0); + Number actualValue = FMT.parse(parseString, pp); + assertEquals(expectedValue, (double)actualValue); + assertEquals(parseString.length(), pp.getIndex()); + } + + // Checks the parse(String) method + public void checkParseWithPP(String parseString, Double expectedValue) + throws ParseException { + Number actualValue = FMT.parse(parseString); + assertEquals(expectedValue, (double)actualValue); + } + + // Generate large enough exponents that should all be parsed as infinity + // when positive. This includes exponents that exceed Long.MAX_VALUE + private static List largeExponentValues() { + return createExponentValues(false); + } + + // Same as previous provider but for negative exponent values, so expecting + // a parsed value of 0. + private static List smallExponentValues() { + return createExponentValues(true); + } + + // Programmatically generate some large parse values that are expected + // to be parsed as infinity or 0 + private static List createExponentValues(boolean negative) { + List args = new ArrayList<>(); + // Start with a base value that should be parsed as infinity + String baseValue = "12234.123E1100"; + // Continuously add to the String until we trigger the overflow condition + for (int i = 0; i < 100; i++) { + StringBuilder bldr = new StringBuilder(); + // Add to exponent + bldr.append(baseValue).append("1".repeat(i)); + // Add to mantissa + bldr.insert(0, "1".repeat(i)); + args.add(Arguments.of( + // Prepend "-" to exponent if negative + negative ? bldr.insert(bldr.indexOf("E")+1, "-").toString() : bldr.toString(), + // Expect 0 if negative, else infinity + negative ? 0.0 : Double.POSITIVE_INFINITY)); + } + return args; + } + + // The provided values are all from the JBS issue + // These contain exponents that exceed Integer.MAX_VALUE, but not Long.MAX_VALUE + private static Stream bugReportValues() { + return Stream.of( + Arguments.of("0.123E1", 1.23), + Arguments.of("0.123E309", 1.23E308), + Arguments.of("0.123E310", Double.POSITIVE_INFINITY), + Arguments.of("0.123E2147483647", Double.POSITIVE_INFINITY), + Arguments.of("0.123E2147483648", Double.POSITIVE_INFINITY), + Arguments.of("0.0123E2147483648", Double.POSITIVE_INFINITY), + Arguments.of("0.0123E2147483649", Double.POSITIVE_INFINITY), + Arguments.of("1.23E2147483646", Double.POSITIVE_INFINITY), + Arguments.of("1.23E2147483647", Double.POSITIVE_INFINITY), + Arguments.of("0.123E4294967296", Double.POSITIVE_INFINITY), + Arguments.of("0.123E-322", 9.9E-324), + Arguments.of("0.123E-323", 0.0), + Arguments.of("0.123E-2147483647", 0.0), + Arguments.of("0.123E-2147483648", 0.0), + Arguments.of("0.123E-2147483649", 0.0) + ); + } + + // Some other edge case values to ensure parse correctness + private static Stream edgeCases() { + return Stream.of( + // Exponent itself does not cause underflow, but decimalAt adjustment + // based off mantissa should. decimalAt(-1) + exponent(Integer.MIN_VALUE) = underflow + Arguments.of("0.0123E-2147483648", 0.0), + // 0 exponent + Arguments.of("1.23E0", 1.23), + // Leading zeroes + Arguments.of("1.23E0000123", 1.23E123), + // Leading zeroes - Past Long.MAX_VALUE length + Arguments.of("1.23E00000000000000000000000000000000000000000123", 1.23E123), + // Trailing zeroes + Arguments.of("1.23E100", 1.23E100), + // Long.MAX_VALUE length + Arguments.of("1.23E1234567891234567800", Double.POSITIVE_INFINITY), + // Long.MAX_VALUE with trailing zeroes + Arguments.of("1.23E9223372036854775807000", Double.POSITIVE_INFINITY), + // Long.MIN_VALUE + Arguments.of("1.23E-9223372036854775808", 0.0), + // Exponent value smaller than Long.MIN_VALUE + Arguments.of("1.23E-9223372036854775809", 0.0), + // Exponent value equal to Long.MAX_VALUE + Arguments.of("1.23E9223372036854775807", Double.POSITIVE_INFINITY), + // Exponent value larger than Long.MAX_VALUE + Arguments.of("1.23E9223372036854775808", Double.POSITIVE_INFINITY) + ); + } +} 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..2f07521ff23 --- /dev/null +++ b/test/jdk/java/text/Format/NumberFormat/LenientParseTest.java @@ -0,0 +1,430 @@ +/* + * 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 + * @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())); + } + + // ---- 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)) + ); + } + + // 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 d8d611afc93e3fd9c7bdcd8205c8fdf1d7d84035 Mon Sep 17 00:00:00 2001 From: Roland Mesde Date: Tue, 18 Nov 2025 14:58:24 -0800 Subject: [PATCH 2/2] Removed LenientParseTest.java that is part of JDK-8327640 (Large and Required CSR) --- .../Format/NumberFormat/LenientParseTest.java | 430 ------------------ 1 file changed, 430 deletions(-) delete mode 100644 test/jdk/java/text/Format/NumberFormat/LenientParseTest.java 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 2f07521ff23..00000000000 --- a/test/jdk/java/text/Format/NumberFormat/LenientParseTest.java +++ /dev/null @@ -1,430 +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 - * @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())); - } - - // ---- 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)) - ); - } - - // 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(); - } -}