From 30471d74345d406ce78e2ecbe5cda0d8bfdba3bf Mon Sep 17 00:00:00 2001 From: Justin Lu Date: Thu, 23 Jul 2026 16:14:36 +0000 Subject: [PATCH] 8388507: DecimalFormat Strict Parsing NaN and Grouping Size Zero Fixes Reviewed-by: naoto --- .../classes/java/text/DecimalFormat.java | 52 +++++++++++++------ .../Format/NumberFormat/LenientParseTest.java | 10 +++- .../Format/NumberFormat/StrictParseTest.java | 18 +++++-- 3 files changed, 59 insertions(+), 21 deletions(-) diff --git a/src/java.base/share/classes/java/text/DecimalFormat.java b/src/java.base/share/classes/java/text/DecimalFormat.java index c803a97ad86..7d8297f42bc 100644 --- a/src/java.base/share/classes/java/text/DecimalFormat.java +++ b/src/java.base/share/classes/java/text/DecimalFormat.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2026, 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 @@ -250,7 +250,7 @@ import sun.util.locale.provider.ResourceBundleBasedAdapter; * {@link DecimalFormatSymbols#getExponentSeparator()} * Number * Separates mantissa and exponent in scientific notation. This value - * is case sensistive. Need not be quoted in prefix or suffix. + * is case sensitive. Need not be quoted in prefix or suffix. * * {@code ;} * {@link DecimalFormatSymbols#getPatternSeparator()} @@ -2059,8 +2059,7 @@ public class DecimalFormat extends NumberFormat { // Output grouping separator if necessary. Don't output a // grouping separator if i==0 though; that's at the end of // the integer part. - if (isGroupingUsed() && i>0 && (groupingSize != 0) && - (i % groupingSize == 0)) { + if (isGroupingEnabled() && i > 0 && i % groupingSize == 0) { int gStart = result.length(); result.append(grouping); delegate.formatted(Field.GROUPING_SEPARATOR, @@ -2242,8 +2241,14 @@ public class DecimalFormat extends NumberFormat { public Number parse(String text, ParsePosition pos) { // special case NaN if (text.regionMatches(pos.index, symbols.getNaN(), 0, symbols.getNaN().length())) { - pos.index = pos.index + symbols.getNaN().length(); - return Double.valueOf(Double.NaN); + var nanEnd = pos.index + symbols.getNaN().length(); + // When strict, parsing NaN must be exact + if (parseStrict && nanEnd != text.length()) { + pos.errorIndex = nanEnd; + return null; + } + pos.index = nanEnd; + return Double.NaN; } boolean[] status = new boolean[STATUS_LENGTH]; @@ -2568,7 +2573,8 @@ public class DecimalFormat extends NumberFormat { } // Enforce the grouping size on the first group - if (parseStrict && isGroupingUsed() && position == startPos + groupingSize + if (parseStrict && isGroupingEnabled() + && position == startPos + groupingSize && prevSeparatorIndex == -groupingSize && !sawDecimal && digit >= 0 && digit <= 9) { return new NumericPosition(position, intIndex); @@ -2612,7 +2618,8 @@ public class DecimalFormat extends NumberFormat { return new NumericPosition(-1, intIndex); } // Check grouping size on decimal separator - if (parseStrict && isGroupingViolation(position, prevSeparatorIndex)) { + if (parseStrict && isGroupingEnabled() + && isGroupingViolation(position, prevSeparatorIndex)) { return new NumericPosition( groupingViolationIndex(position, prevSeparatorIndex), intIndex); } @@ -2624,7 +2631,8 @@ public class DecimalFormat extends NumberFormat { intIndex = position; digits.decimalAt = digitCount; // Not digits.count! sawDecimal = true; - } else if (!isExponent && ch == grouping && isGroupingUsed()) { + } else if (!isExponent && ch == grouping && + (parseStrict ? isGroupingEnabled() : isGroupingUsed())) { if (parseStrict) { // text should not start with grouping when strict if (position == startPos) { @@ -2679,10 +2687,11 @@ public class DecimalFormat extends NumberFormat { // (When strict), within the loop we enforce grouping when encountering // decimal/grouping symbols. Once outside loop, we need to check // the final grouping, ex: "1,234". Only check the final grouping - // if we have not seen a decimal separator, to prevent a non needed check, - // for ex: "1,234.", "1,234.12" + // if we have not seen a decimal separator, to prevent a grouping check in the + // fraction portion for ex: "1,234.", "1,234.12" if (parseStrict) { - if (!sawDecimal && isGroupingViolation(position, prevSeparatorIndex)) { + if (!sawDecimal && isGroupingEnabled() + && isGroupingViolation(position, prevSeparatorIndex)) { // -1, since position is incremented by one too many when loop is finished // "1,234%" and "1,234" both end with pos = 5, since '%' breaks // the loop before incrementing position. In both cases, check @@ -2743,12 +2752,23 @@ public class DecimalFormat extends NumberFormat { return decimalAt; } + /* + * DecimalFormat defines both setGroupingUsed(boolean) and setGroupingSize(int). + * These operate independently, and setting a grouping size of 0 does not mean that + * isGroupingUsed() returns false. As a result, to effectively check whether grouping is used + * for strict parsing, both values need to be verified. Lenient parsing, which preserves the + * legacy parsing behavior, does not require this exhaustive check because grouping size positioning + * is not checked. + */ + private boolean isGroupingEnabled() { + return isGroupingUsed() && groupingSize > 0; + } + // Checks to make sure grouping size is not violated. Used when strict. private boolean isGroupingViolation(int pos, int prevGroupingPos) { assert parseStrict : "Grouping violations should only occur when strict"; - return isGroupingUsed() && // Only violates if using grouping - // Checks if a previous grouping symbol was seen. - prevGroupingPos != -groupingSize && + // Checks if a previous grouping symbol was seen. + return prevGroupingPos != -groupingSize && // The check itself, - 1 to account for grouping/decimal symbol pos - 1 != prevGroupingPos + groupingSize; } @@ -3586,7 +3606,7 @@ public class DecimalFormat extends NumberFormat { int digitCount = useExponentialNotation ? getMaximumIntegerDigits() : Math.max(groupingSize, getMinimumIntegerDigits()) + 1; for (int i = digitCount; i > 0; --i) { - if (i != digitCount && isGroupingUsed() && groupingSize != 0 && + if (i != digitCount && isGroupingEnabled() && i % groupingSize == 0) { result.append(groupingSymbol); } diff --git a/test/jdk/java/text/Format/NumberFormat/LenientParseTest.java b/test/jdk/java/text/Format/NumberFormat/LenientParseTest.java index c85fe0f6cbb..69fb2a15221 100644 --- a/test/jdk/java/text/Format/NumberFormat/LenientParseTest.java +++ b/test/jdk/java/text/Format/NumberFormat/LenientParseTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2026, 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 @@ -23,7 +23,7 @@ /* * @test - * @bug 8327640 8331485 8333456 8335668 + * @bug 8327640 8331485 8333456 8335668 8388507 * @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 @@ -162,6 +162,12 @@ public class LenientParseTest { assertEquals(1.23E45, successParse(fmt, "1.23E45FOO3222", 7)); } + @Test // Non-localized, only run once + @EnabledIfSystemProperty(named = "user.language", matches = "en") + public void nanNumberFormatTest() { + assertEquals(Double.NaN, successParse(new DecimalFormat(), "NaNFoo", 3)); + } + // ---- CurrencyFormat tests ---- // All input Strings should pass and return expected value. @ParameterizedTest diff --git a/test/jdk/java/text/Format/NumberFormat/StrictParseTest.java b/test/jdk/java/text/Format/NumberFormat/StrictParseTest.java index 3e90ccb39ce..1b69c53407b 100644 --- a/test/jdk/java/text/Format/NumberFormat/StrictParseTest.java +++ b/test/jdk/java/text/Format/NumberFormat/StrictParseTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2026, 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 @@ -23,7 +23,7 @@ /* * @test - * @bug 8327640 8331485 8333755 8335668 + * @bug 8327640 8331485 8333755 8335668 8388507 * @summary Test suite for NumberFormat parsing with strict leniency * @run junit/othervm -Duser.language=en -Duser.country=US StrictParseTest * @run junit/othervm -Duser.language=ja -Duser.country=JP StrictParseTest @@ -114,8 +114,20 @@ public class StrictParseTest { successParse(nonLocalizedDFmt, "a12345,67890b"); successParse(nonLocalizedDFmt, "a1234,67890b"); failParse(nonLocalizedDFmt, "a123456,7890b", 6); - } + // Special case: NaN + failParse(nonLocalizedDFmt, "NaNFoo", 3); + successParse(nonLocalizedDFmt, "NaN"); + // Grouping size == 0 cases + var fmt = new DecimalFormat(); + fmt.setStrict(true); + fmt.setGroupingSize(0); + fmt.setGroupingUsed(true); + var pp = new ParsePosition(0); + assertNull(fmt.parse("555,000.0", pp)); + assertEquals(3, pp.getErrorIndex()); + assertDoesNotThrow(() -> fmt.parse("555.0")); + } // 8333755: Check that parsing with integer only against a suffix value works @Test // Non-localized, run once