From 23670fd41895ccc38931f836d218ff7392a6065a Mon Sep 17 00:00:00 2001 From: Naoto Sato Date: Tue, 26 Aug 2025 21:49:57 +0000 Subject: [PATCH] 8363972: Lenient parsing of minus sign pattern in DecimalFormat/CompactNumberFormat Reviewed-by: jlu, rriggs --- .../build/tools/cldrconverter/Bundle.java | 1 + .../tools/cldrconverter/LDMLParseHandler.java | 28 ++ .../java/text/CompactNumberFormat.java | 24 +- .../classes/java/text/DecimalFormat.java | 71 ++++- .../java/text/DecimalFormatSymbols.java | 54 +++- .../share/classes/java/text/NumberFormat.java | 6 +- .../TestCompactNumber.java | 9 +- .../NumberFormat/LenientMinusSignTest.java | 251 ++++++++++++++++++ 8 files changed, 406 insertions(+), 38 deletions(-) create mode 100644 test/jdk/java/text/Format/NumberFormat/LenientMinusSignTest.java diff --git a/make/jdk/src/classes/build/tools/cldrconverter/Bundle.java b/make/jdk/src/classes/build/tools/cldrconverter/Bundle.java index cbef22d91c0..c4aaa28193a 100644 --- a/make/jdk/src/classes/build/tools/cldrconverter/Bundle.java +++ b/make/jdk/src/classes/build/tools/cldrconverter/Bundle.java @@ -79,6 +79,7 @@ class Bundle { "NumberElements/nan", "NumberElements/currencyDecimal", "NumberElements/currencyGroup", + "NumberElements/lenientMinusSigns", }; private static final String[] TIME_PATTERN_KEYS = { diff --git a/make/jdk/src/classes/build/tools/cldrconverter/LDMLParseHandler.java b/make/jdk/src/classes/build/tools/cldrconverter/LDMLParseHandler.java index 6d5dde0d181..98c0605f8b7 100644 --- a/make/jdk/src/classes/build/tools/cldrconverter/LDMLParseHandler.java +++ b/make/jdk/src/classes/build/tools/cldrconverter/LDMLParseHandler.java @@ -844,6 +844,26 @@ class LDMLParseHandler extends AbstractLDMLHandler { }); break; + // Lenient parsing + case "parseLenients": + if ("lenient".equals(attributes.getValue("level"))) { + pushKeyContainer(qName, attributes, attributes.getValue("scope")); + } else { + pushIgnoredContainer(qName); + } + break; + + case "parseLenient": + // Use only the lenient minus sign for now + if (currentContainer instanceof KeyContainer kc + && kc.getKey().equals("number") + && attributes.getValue("sample").equals("-")) { + pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/lenientMinusSigns"); + } else { + pushIgnoredContainer(qName); + } + break; + default: // treat anything else as a container pushContainer(qName, attributes); @@ -1150,6 +1170,14 @@ class LDMLParseHandler extends AbstractLDMLHandler { currentStyle = ""; putIfEntry(); break; + case "parseLenient": + if (currentContainer instanceof StringEntry se) { + // Convert to a simple concatenation of lenient minuses + // e.g. "[\--﹣ ‐‑ ‒ – −⁻₋ ➖]" -> "--﹣‐‑‒–−⁻₋➖" for the root locale + put(se.getKey(), se.getValue().replaceAll("[\\[\\]\\\\ ]", "")); + } + break; + default: putIfEntry(); } diff --git a/src/java.base/share/classes/java/text/CompactNumberFormat.java b/src/java.base/share/classes/java/text/CompactNumberFormat.java index fae11cbdba1..7163b2dd63b 100644 --- a/src/java.base/share/classes/java/text/CompactNumberFormat.java +++ b/src/java.base/share/classes/java/text/CompactNumberFormat.java @@ -147,7 +147,7 @@ import java.util.stream.Collectors; * a compact pattern. This special pattern can appear explicitly for any specific * range, or considered as a default pattern for an empty string. * - *

Negative Subpatterns

+ *

Negative Subpatterns

* A compact pattern contains a positive and negative subpattern * separated by a subpattern boundary character {@code ';'}, * for example, {@code "0K;-0K"}. Each subpattern has a prefix, @@ -159,7 +159,10 @@ import java.util.stream.Collectors; * the negative prefix and suffix. The number of minimum integer digits, * and other characteristics are all the same as the positive pattern. * That means that {@code "0K;-00K"} produces precisely the same behavior - * as {@code "0K;-0K"}. + * as {@code "0K;-0K"}. In {@link NumberFormat##leniency lenient parsing} + * mode, loose matching of the minus sign pattern is enabled, following the + * LDML’s + * loose matching specification. * *

Escaping Special Characters

* Many characters in a compact pattern are taken literally, they are matched @@ -1585,6 +1588,9 @@ public final class CompactNumberFormat extends NumberFormat { * and are not digits that occur within the numerical portion * *

+ * When lenient, the minus sign in the {@link ##negative_subpatterns + * negative subpatterns} is loosely matched against lenient minus sign characters. + *

* The subclass returned depends on the value of * {@link #isParseBigDecimal}. *

    @@ -1693,14 +1699,12 @@ public final class CompactNumberFormat extends NumberFormat { // Given text does not match the non empty valid compact prefixes // check with the default prefixes if (!gotPositive && !gotNegative) { - if (text.regionMatches(pos.index, defaultPosPrefix, 0, - defaultPosPrefix.length())) { + if (decimalFormat.matchAffix(text, position, defaultPosPrefix)) { // Matches the default positive prefix matchedPosPrefix = defaultPosPrefix; gotPositive = true; } - if (text.regionMatches(pos.index, defaultNegPrefix, 0, - defaultNegPrefix.length())) { + if (decimalFormat.matchAffix(text, position, defaultNegPrefix)) { // Matches the default negative prefix matchedNegPrefix = defaultNegPrefix; gotNegative = true; @@ -1924,7 +1928,7 @@ public final class CompactNumberFormat extends NumberFormat { if (!affix.isEmpty() && !affix.equals(defaultAffix)) { // Look ahead only for the longer match than the previous match if (matchedAffix.length() < affix.length()) { - return text.regionMatches(position, affix, 0, affix.length()); + return decimalFormat.matchAffix(text, position, affix); } } return false; @@ -2026,8 +2030,7 @@ public final class CompactNumberFormat extends NumberFormat { if (!gotPos && !gotNeg) { String positiveSuffix = defaultDecimalFormat.getPositiveSuffix(); String negativeSuffix = defaultDecimalFormat.getNegativeSuffix(); - boolean containsPosSuffix = text.regionMatches(position, - positiveSuffix, 0, positiveSuffix.length()); + boolean containsPosSuffix = decimalFormat.matchAffix(text, position, positiveSuffix); boolean endsWithPosSuffix = containsPosSuffix && text.length() == position + positiveSuffix.length(); if (parseStrict ? endsWithPosSuffix : containsPosSuffix) { @@ -2035,8 +2038,7 @@ public final class CompactNumberFormat extends NumberFormat { matchedPosSuffix = positiveSuffix; gotPos = true; } - boolean containsNegSuffix = text.regionMatches(position, - negativeSuffix, 0, negativeSuffix.length()); + boolean containsNegSuffix = decimalFormat.matchAffix(text, position, negativeSuffix); boolean endsWithNegSuffix = containsNegSuffix && text.length() == position + negativeSuffix.length(); if (parseStrict ? endsWithNegSuffix : containsNegSuffix) { diff --git a/src/java.base/share/classes/java/text/DecimalFormat.java b/src/java.base/share/classes/java/text/DecimalFormat.java index 50bb1e01336..aa881aecc8a 100644 --- a/src/java.base/share/classes/java/text/DecimalFormat.java +++ b/src/java.base/share/classes/java/text/DecimalFormat.java @@ -296,7 +296,7 @@ import sun.util.locale.provider.ResourceBundleBasedAdapter; * #setMaximumIntegerDigits(int)} can be used to manually adjust the maximum * integer digits. * - *

    Negative Subpatterns

    + *

    Negative Subpatterns

    * A {@code DecimalFormat} pattern contains a positive and negative * subpattern, for example, {@code "#,##0.00;(#,##0.00)"}. Each * subpattern has a prefix, numeric part, and suffix. The negative subpattern @@ -307,7 +307,11 @@ import sun.util.locale.provider.ResourceBundleBasedAdapter; * serves only to specify the negative prefix and suffix; the number of digits, * minimal digits, and other characteristics are all the same as the positive * pattern. That means that {@code "#,##0.0#;(#)"} produces precisely - * the same behavior as {@code "#,##0.0#;(#,##0.0#)"}. + * the same behavior as {@code "#,##0.0#;(#,##0.0#)"}. In + * {@link NumberFormat##leniency lenient parsing} mode, loose matching of the + * minus sign pattern is enabled, following the LDML’s + * + * loose matching specification. * *

    The prefixes, suffixes, and various symbols used for infinity, digits, * grouping separators, decimal separators, etc. may be set to arbitrary @@ -2189,6 +2193,9 @@ public class DecimalFormat extends NumberFormat { * and are not digits that occur within the numerical portion *

*

+ * When lenient, the minus sign in the {@link ##negative_subpatterns + * negative subpatterns} is loosely matched against lenient minus sign characters. + *

* The subclass returned depends on the value of {@link #isParseBigDecimal} * as well as on the string being parsed. *

    @@ -2385,10 +2392,8 @@ public class DecimalFormat extends NumberFormat { boolean gotPositive, gotNegative; // check for positivePrefix; take longest - gotPositive = text.regionMatches(position, positivePrefix, 0, - positivePrefix.length()); - gotNegative = text.regionMatches(position, negativePrefix, 0, - negativePrefix.length()); + gotPositive = matchAffix(text, position, positivePrefix); + gotNegative = matchAffix(text, position, negativePrefix); if (gotPositive && gotNegative) { if (positivePrefix.length() > negativePrefix.length()) { @@ -2424,15 +2429,13 @@ public class DecimalFormat extends NumberFormat { // When lenient, text only needs to contain the suffix. if (!isExponent) { if (gotPositive) { - boolean containsPosSuffix = - text.regionMatches(position, positiveSuffix, 0, positiveSuffix.length()); + boolean containsPosSuffix = matchAffix(text, position, positiveSuffix); boolean endsWithPosSuffix = containsPosSuffix && text.length() == position + positiveSuffix.length(); gotPositive = parseStrict ? endsWithPosSuffix : containsPosSuffix; } if (gotNegative) { - boolean containsNegSuffix = - text.regionMatches(position, negativeSuffix, 0, negativeSuffix.length()); + boolean containsNegSuffix = matchAffix(text, position, negativeSuffix); boolean endsWithNegSuffix = containsNegSuffix && text.length() == position + negativeSuffix.length(); gotNegative = parseStrict ? endsWithNegSuffix : containsNegSuffix; @@ -3501,6 +3504,54 @@ public class DecimalFormat extends NumberFormat { if (needQuote) buffer.append('\''); } + /** + * {@return true if the text matches the affix} + * In lenient mode, lenient minus signs also match the hyphen-minus + * (U+002D). Package-private access, as this is called from + * CompactNumberFormat. + * + * Note: Minus signs in the supplementary character range or normalization + * equivalents are not matched, as they may alter the affix length. + */ + boolean matchAffix(String text, int position, String affix) { + var alen = affix.length(); + var tlen = text.length(); + + if (alen == 0) { + // always match with an empty affix, as affix is optional + return true; + } + if (position >= tlen) { + return false; + } + if (parseStrict) { + return text.regionMatches(position, affix, 0, alen); + } + + var lms = symbols.getLenientMinusSigns(); + int i = 0; + int limit = Math.min(tlen, position + alen); + for (; position + i < limit; i++) { + char t = text.charAt(position + i); + char a = affix.charAt(i); + int tIndex = lms.indexOf(t); + int aIndex = lms.indexOf(a); + // Non LMS. Match direct + if (tIndex < 0 && aIndex < 0) { + if (t != a) { + return false; + } + } else { + // By here, at least one LMS. Ensure both LMS. + if (tIndex < 0 || aIndex < 0) { + return false; + } + } + } + // Return true if entire affix was matched + return i == alen; + } + /** * Implementation of producing a pattern. This method returns a positive and * negative (if needed), pattern string in the form of : Prefix (optional) diff --git a/src/java.base/share/classes/java/text/DecimalFormatSymbols.java b/src/java.base/share/classes/java/text/DecimalFormatSymbols.java index da20af60662..dfb344f26a7 100644 --- a/src/java.base/share/classes/java/text/DecimalFormatSymbols.java +++ b/src/java.base/share/classes/java/text/DecimalFormatSymbols.java @@ -718,6 +718,17 @@ public class DecimalFormatSymbols implements Cloneable, Serializable { this.minusSign = findNonFormatChar(minusSignText, '-'); } + /** + * {@return the lenient minus signs} Multiple lenient minus signs + * are concatenated to form the returned string. Each codepoint + * in the string is a valid minus sign pattern. If there are no + * lenient minus signs defined in this locale, {@code minusSignText} + * is returned. + */ + String getLenientMinusSigns() { + return lenientMinusSigns; + } + //------------------------------------------------------------ // END Package Private methods ... to be made public later //------------------------------------------------------------ @@ -818,18 +829,7 @@ public class DecimalFormatSymbols implements Cloneable, Serializable { private void initialize(Locale locale) { this.locale = locale; - // check for region override - Locale override = locale.getUnicodeLocaleType("nu") == null ? - CalendarDataUtility.findRegionOverride(locale) : - locale; - - // get resource bundle data - LocaleProviderAdapter adapter = LocaleProviderAdapter.getAdapter(DecimalFormatSymbolsProvider.class, override); - // Avoid potential recursions - if (!(adapter instanceof ResourceBundleBasedAdapter)) { - adapter = LocaleProviderAdapter.getResourceBundleBased(); - } - Object[] data = adapter.getLocaleResources(override).getDecimalFormatSymbolsData(); + Object[] data = loadNumberData(locale); String[] numberElements = (String[]) data[0]; decimalSeparator = numberElements[0].charAt(0); @@ -854,11 +854,30 @@ public class DecimalFormatSymbols implements Cloneable, Serializable { monetaryGroupingSeparator = numberElements.length < 13 || numberElements[12].isEmpty() ? groupingSeparator : numberElements[12].charAt(0); + // Lenient minus signs + lenientMinusSigns = numberElements.length < 14 ? minusSignText : numberElements[13]; + // maybe filled with previously cached values, or null. intlCurrencySymbol = (String) data[1]; currencySymbol = (String) data[2]; } + private Object[] loadNumberData(Locale locale) { + // check for region override + Locale override = locale.getUnicodeLocaleType("nu") == null ? + CalendarDataUtility.findRegionOverride(locale) : + locale; + + // get resource bundle data + LocaleProviderAdapter adapter = LocaleProviderAdapter.getAdapter(DecimalFormatSymbolsProvider.class, override); + // Avoid potential recursions + if (!(adapter instanceof ResourceBundleBasedAdapter)) { + adapter = LocaleProviderAdapter.getResourceBundleBased(); + } + + return adapter.getLocaleResources(override).getDecimalFormatSymbolsData(); + } + /** * Obtains non-format single character from String */ @@ -995,6 +1014,14 @@ public class DecimalFormatSymbols implements Cloneable, Serializable { } currencyInitialized = true; } + + if (loadNumberData(locale) instanceof Object[] d && + d[0] instanceof String[] numberElements && + numberElements.length >= 14) { + lenientMinusSigns = numberElements[13]; + } else { + lenientMinusSigns = minusSignText; + } } /** @@ -1174,6 +1201,9 @@ public class DecimalFormatSymbols implements Cloneable, Serializable { private transient Currency currency; private transient volatile boolean currencyInitialized; + // Lenient minus. No need to be set by applications + private transient String lenientMinusSigns; + /** * Cached hash code. */ diff --git a/src/java.base/share/classes/java/text/NumberFormat.java b/src/java.base/share/classes/java/text/NumberFormat.java index 0f4da56de0c..759ed7ae5ea 100644 --- a/src/java.base/share/classes/java/text/NumberFormat.java +++ b/src/java.base/share/classes/java/text/NumberFormat.java @@ -195,7 +195,11 @@ import sun.util.locale.provider.LocaleServiceProviderPool; * Lenient parsing should be used when attempting to parse a number * out of a String that contains non-numerical or non-format related values. * For example, using a {@link Locale#US} currency format to parse the number - * {@code 1000} out of the String "$1,000.00 was paid". + * {@code 1000} out of the String "$1,000.00 was paid". Lenient parsing also + * allows loose matching of characters in the source text. For example, an + * implementation of the {@code NumberFormat} class may allow matching "−" + * (U+2212 MINUS SIGN) to the "-" (U+002D HYPHEN-MINUS) pattern character + * when used as a negative prefix. *

    * Strict parsing should be used when attempting to ensure a String adheres exactly * to a locale's conventions, and can thus serve to validate input. For example, successfully diff --git a/test/jdk/java/text/Format/CompactNumberFormat/TestCompactNumber.java b/test/jdk/java/text/Format/CompactNumberFormat/TestCompactNumber.java index 16de2247779..e9972f62f3e 100644 --- a/test/jdk/java/text/Format/CompactNumberFormat/TestCompactNumber.java +++ b/test/jdk/java/text/Format/CompactNumberFormat/TestCompactNumber.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 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 @@ -22,7 +22,7 @@ */ /* * @test - * @bug 8177552 8217721 8222756 8295372 8306116 8319990 8338690 + * @bug 8177552 8217721 8222756 8295372 8306116 8319990 8338690 8363972 * @summary Checks the functioning of compact number format * @modules jdk.localedata * @run testng/othervm TestCompactNumber @@ -462,6 +462,8 @@ public class TestCompactNumber { {FORMAT_SE_SHORT, "12345679,89\u00a0bn", 1.2345679890000001E19, Double.class}, {FORMAT_SE_SHORT, "\u2212999", -999L, Long.class}, {FORMAT_SE_SHORT, "\u22128\u00a0mn", -8000000L, Long.class}, + // lenient parsing. Hyphen-minus should match the localized minus sign + {FORMAT_SE_SHORT, "-8\u00a0mn", -8000000L, Long.class}, {FORMAT_SE_SHORT, "\u22128\u00a0dt", -8000L, Long.class}, {FORMAT_SE_SHORT, "\u221212345679\u00a0bn", -1.2345679E19, Double.class}, {FORMAT_SE_SHORT, "\u221212345679,89\u00a0bn", -1.2345679890000001E19, Double.class}, @@ -503,8 +505,7 @@ public class TestCompactNumber { {FORMAT_EN_US_SHORT, "K12,347", null}, // Invalid prefix for ja_JP {FORMAT_JA_JP_SHORT, "\u4E071", null}, - // Localized minus sign should be used - {FORMAT_SE_SHORT, "-8\u00a0mn", null},}; + }; } @DataProvider(name = "invalidParse") diff --git a/test/jdk/java/text/Format/NumberFormat/LenientMinusSignTest.java b/test/jdk/java/text/Format/NumberFormat/LenientMinusSignTest.java new file mode 100644 index 00000000000..251cac7c9cb --- /dev/null +++ b/test/jdk/java/text/Format/NumberFormat/LenientMinusSignTest.java @@ -0,0 +1,251 @@ +/* + * Copyright (c) 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 8363972 + * @summary Unit tests for lenient minus parsing + * @modules jdk.localedata + * java.base/java.text:+open + * @run junit LenientMinusSignTest + */ + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.invoke.MethodHandles; +import java.text.CompactNumberFormat; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.Locale; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class LenientMinusSignTest { + private static final Locale FINNISH = Locale.of("fi"); + private static final DecimalFormatSymbols DFS = + new DecimalFormatSymbols(Locale.ROOT); + private static final String MINUS_PATTERN = "\u002D"; + + // "parseLenient" data from CLDR v47. These data are subject to change + private static Stream minus() { + return Stream.of( + MINUS_PATTERN, // "-" Hyphen-Minus + "\uFF0D", // "-" Fullwidth Hyphen-Minus + "\uFE63", // "﹣" Small Hyphen-Minus + "\u2010", // "‐" Hyphen + "\u2011", // "‑" Non-Breaking Hyphen + "\u2012", // "‒" Figure Dash + "\u2013", // "–" En Dash + "\u2212", // "−" Minus Sign + "\u207B", // "⁻" Superscript Minus + "\u208B", // "₋" Subscript Minus + "\u2796" // "➖" Heavy Minus Sign + ); + } + + @Test + void testFinnishMinus() throws ParseException { + // originally reported in JDK-8189097 + // Should not throw a ParseException + assertEquals(NumberFormat.getInstance(FINNISH).parse(MINUS_PATTERN + "1,5"), -1.5); + } + + @Test + void testFinnishMinusStrict() { + // Should throw a ParseException + var nf = NumberFormat.getInstance(FINNISH); + nf.setStrict(true); + assertThrows(ParseException.class, () -> nf.parse(MINUS_PATTERN + "1,5")); + } + + @Test + void testReadObject() throws IOException, ClassNotFoundException, ParseException { + // check if deserialized NF works with lenient minus. Using the Finnish example + var nf = NumberFormat.getInstance(FINNISH); + NumberFormat nfDeser; + byte[] serialized; + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream out = new ObjectOutputStream(bos)) { + out.writeObject(nf); + out.flush(); + serialized = bos.toByteArray(); + } + try (ByteArrayInputStream bis = new ByteArrayInputStream(serialized); + ObjectInputStream in = new ObjectInputStream(bis)) { + nfDeser = (NumberFormat) in.readObject(); + } + assertEquals(nfDeser.parse(MINUS_PATTERN + "1,5"), -1.5); + } + + // White box test. modifies the private `lenientMinusSigns` field in the DFS + @Test + void testSupplementary() throws IllegalAccessException, NoSuchFieldException, ParseException { + var dfs = new DecimalFormatSymbols(Locale.ROOT); + MethodHandles.privateLookupIn(DecimalFormatSymbols.class, MethodHandles.lookup()) + .findVarHandle(DecimalFormatSymbols.class, "lenientMinusSigns", String.class) + .set(dfs, "-🙂"); + // Direct match. Should succeed + var df = new DecimalFormat("#.#;🙂#.#", dfs); + assertEquals(df.parse("🙂1.5"), -1.5); + + // Fail if the lengths of negative prefixes differ + assertThrows(ParseException.class, () -> df.parse("-1.5")); + var df2= new DecimalFormat("#.#;-#.#", dfs); + assertThrows(ParseException.class, () -> df2.parse("🙂1.5")); + } + + @Nested + class DecimalFormatTest { + private static final String PREFIX = "+#;-#"; + private static final String SUFFIX = "#+;#-"; + private static final String LONG_PREFIX = "pos#;-neg#"; + private static final String LONG_SUFFIX = "#pos;#neg-"; + + @ParameterizedTest + @MethodSource("LenientMinusSignTest#minus") + public void testLenientPrefix(String sign) throws ParseException { + var df = new DecimalFormat(PREFIX, DFS); + df.setStrict(false); + assertEquals(MINUS_PATTERN + "1", df.format(df.parse(sign + "1"))); + } + + @ParameterizedTest + @MethodSource("LenientMinusSignTest#minus") + public void testLenientSuffix(String sign) throws ParseException { + var df = new DecimalFormat(SUFFIX, DFS); + df.setStrict(false); + assertEquals("1" + MINUS_PATTERN, df.format(df.parse("1" + sign))); + } + + @ParameterizedTest + @MethodSource("LenientMinusSignTest#minus") + public void testStrictPrefix(String sign) throws ParseException { + var df = new DecimalFormat(PREFIX, DFS); + df.setStrict(true); + if (sign.equals(MINUS_PATTERN)) { + assertEquals(MINUS_PATTERN + "1", df.format(df.parse(sign + "1"))); + } else { + assertThrows(ParseException.class, () -> df.parse(sign + "1")); + } + } + + @ParameterizedTest + @MethodSource("LenientMinusSignTest#minus") + public void testStrictSuffix(String sign) throws ParseException { + var df = new DecimalFormat(SUFFIX, DFS); + df.setStrict(true); + if (sign.equals(MINUS_PATTERN)) { + assertEquals("1" + MINUS_PATTERN, df.format(df.parse("1" + sign))); + } else { + assertThrows(ParseException.class, () -> df.parse("1" + sign)); + } + } + + @ParameterizedTest + @MethodSource("LenientMinusSignTest#minus") + public void testLongPrefix(String sign) throws ParseException { + var df = new DecimalFormat(LONG_PREFIX, DFS); + assertEquals(MINUS_PATTERN + "neg1", df.format(df.parse(sign + "neg1"))); + } + + @ParameterizedTest + @MethodSource("LenientMinusSignTest#minus") + public void testLongSuffix(String sign) throws ParseException { + var df = new DecimalFormat(LONG_SUFFIX, DFS); + assertEquals("1neg" + MINUS_PATTERN, df.format(df.parse("1neg" + sign))); + } + } + + @Nested + class CompactNumberFormatTest { + private static final String[] PREFIX = {"+0;-0"}; + private static final String[] SUFFIX = {"0+;0-"}; + private static final String[] LONG_PREFIX = {"pos0;-neg0"}; + private static final String[] LONG_SUFFIX = {"0pos;0neg-"}; + + @ParameterizedTest + @MethodSource("LenientMinusSignTest#minus") + public void testLenientPrefix(String sign) throws ParseException { + var cnf = new CompactNumberFormat("0", DFS, PREFIX); + cnf.setStrict(false); + assertEquals(MINUS_PATTERN + "1", cnf.format(cnf.parse(sign + "1"))); + } + + @ParameterizedTest + @MethodSource("LenientMinusSignTest#minus") + public void testLenientSuffix(String sign) throws ParseException { + var cnf = new CompactNumberFormat("0", DFS, SUFFIX); + cnf.setStrict(false); + assertEquals("1" + MINUS_PATTERN, cnf.format(cnf.parse("1" + sign))); + } + + @ParameterizedTest + @MethodSource("LenientMinusSignTest#minus") + public void testStrictPrefix(String sign) throws ParseException { + var cnf = new CompactNumberFormat("0", DFS, PREFIX); + cnf.setStrict(true); + if (sign.equals(MINUS_PATTERN)) { + assertEquals(MINUS_PATTERN + "1", cnf.format(cnf.parse(sign + "1"))); + } else { + assertThrows(ParseException.class, () -> cnf.parse(sign + "1")); + } + } + + @ParameterizedTest + @MethodSource("LenientMinusSignTest#minus") + public void testStrictSuffix(String sign) throws ParseException { + var cnf = new CompactNumberFormat("0", DFS, SUFFIX); + cnf.setStrict(true); + if (sign.equals(MINUS_PATTERN)) { + assertEquals("1" + MINUS_PATTERN, cnf.format(cnf.parse("1" + sign))); + } else { + assertThrows(ParseException.class, () -> cnf.parse("1" + sign)); + } + } + + @ParameterizedTest + @MethodSource("LenientMinusSignTest#minus") + public void testLongPrefix(String sign) throws ParseException { + var cnf = new CompactNumberFormat("0", DFS, LONG_PREFIX); + assertEquals(MINUS_PATTERN + "neg1", cnf.format(cnf.parse(sign + "neg1"))); + } + + @ParameterizedTest + @MethodSource("LenientMinusSignTest#minus") + public void testLongSuffix(String sign) throws ParseException { + var cnf = new CompactNumberFormat("0", DFS, LONG_SUFFIX); + assertEquals("1neg" + MINUS_PATTERN, cnf.format(cnf.parse( "1neg" + sign))); + } + } +}