diff --git a/src/java.base/share/classes/java/text/ChoiceFormat.java b/src/java.base/share/classes/java/text/ChoiceFormat.java index 1252efe33b5..d9f7557dded 100644 --- a/src/java.base/share/classes/java/text/ChoiceFormat.java +++ b/src/java.base/share/classes/java/text/ChoiceFormat.java @@ -68,71 +68,18 @@ * doesn't require any complex setup for a given locale. In fact, * {@code ChoiceFormat} doesn't implement any locale specific behavior. * - *

- * A {@code ChoiceFormat} can be constructed using either an array of formats - * and an array of limits or a string pattern. When constructing with - * format and limit arrays, the length of these arrays must be the same. - * - * For example, - *

- * - *

- * Below is an example of constructing a ChoiceFormat with arrays to format - * and parse values: - * {@snippet lang=java : - * double[] limits = {1,2,3,4,5,6,7}; - * String[] dayOfWeekNames = {"Sun","Mon","Tue","Wed","Thur","Fri","Sat"}; - * ChoiceFormat form = new ChoiceFormat(limits, dayOfWeekNames); - * ParsePosition status = new ParsePosition(0); - * for (double i = 0.0; i <= 8.0; ++i) { - * status.setIndex(0); - * System.out.println(i + " -> " + form.format(i) + " -> " - * + form.parse(form.format(i),status)); - * } - * } - * - *

- * For more sophisticated patterns, {@code ChoiceFormat} can be used with - * {@link MessageFormat} to produce accurate forms for singular and plural: - * {@snippet lang=java : - * MessageFormat msgFmt = new MessageFormat("The disk \"{0}\" contains {1}."); - * double[] fileLimits = {0,1,2}; - * String[] filePart = {"no files","one file","{1,number} files"}; - * ChoiceFormat fileChoices = new ChoiceFormat(fileLimits, filePart); - * msgFmt.setFormatByArgumentIndex(1, fileChoices); - * Object[] args = {"MyDisk", 1273}; - * System.out.println(msgFmt.format(args)); - * } - * The output with different values for {@code fileCount}: - *

- * The disk "MyDisk" contains no files.
- * The disk "MyDisk" contains one file.
- * The disk "MyDisk" contains 1,273 files.
- * 
- * See {@link MessageFormat##pattern_caveats MessageFormat} for caveats regarding - * {@code MessageFormat} patterns within a {@code ChoiceFormat} pattern. - * *

Patterns

* A {@code ChoiceFormat} pattern has the following syntax: *
*
*
Pattern: *
SubPattern *("|" SubPattern) - *
Note: Each additional SubPattern must have a Limit greater than the previous SubPattern's Limit *
* *
*
SubPattern: *
Limit Relation Format + *
Note: Each additional SubPattern must have an ascending Limit-Relation interval
*
* *
@@ -172,20 +119,54 @@ * *
*
Format: - *
Any characters except the Relation symbols + *
Any characters except the special pattern character '|' *
* *
* * Note:The relation ≤ is not equivalent to <= * - *

If a Relation symbol is to be used within a Format pattern, - * it must be single quoted. For example, - * {@code new ChoiceFormat("1# '#'1 ").format(1)} returns {@code " #1 "}. + *

To use a reserved special pattern character within a Format pattern, + * it must be single quoted. For example, {@code new ChoiceFormat("1#'|'foo'|'").format(1)} + * returns {@code "|foo|"}. * Use two single quotes in a row to produce a literal single quote. For example, * {@code new ChoiceFormat("1# ''one'' ").format(1)} returns {@code " 'one' "}. * - *

Below is an example of constructing a ChoiceFormat with a pattern: + *

Usage Information

+ * + *

+ * A {@code ChoiceFormat} can be constructed using either an array of formats + * and an array of limits or a string pattern. When constructing with + * format and limit arrays, the length of these arrays must be the same. + * + * For example, + *

+ * + *

+ * Below is an example of constructing a ChoiceFormat with arrays to format + * and parse values: + * {@snippet lang=java : + * double[] limits = {1,2,3,4,5,6,7}; + * String[] dayOfWeekNames = {"Sun","Mon","Tue","Wed","Thur","Fri","Sat"}; + * ChoiceFormat form = new ChoiceFormat(limits, dayOfWeekNames); + * ParsePosition status = new ParsePosition(0); + * for (double i = 0.0; i <= 8.0; ++i) { + * status.setIndex(0); + * System.out.println(i + " -> " + form.format(i) + " -> " + * + form.parse(form.format(i),status)); + * } + * } + * + *

Below is an example of constructing a ChoiceFormat with a String pattern: * {@snippet lang=java : * ChoiceFormat fmt = new ChoiceFormat( * "-1#is negative| 0#is zero or fraction | 1#is one |1.0 + * For more sophisticated patterns, {@code ChoiceFormat} can be used with + * {@link MessageFormat} to produce accurate forms for singular and plural: + * {@snippet lang=java : + * MessageFormat msgFmt = new MessageFormat("The disk \"{0}\" contains {1}."); + * double[] fileLimits = {0,1,2}; + * String[] filePart = {"no files","one file","{1,number} files"}; + * ChoiceFormat fileChoices = new ChoiceFormat(fileLimits, filePart); + * msgFmt.setFormatByArgumentIndex(1, fileChoices); + * Object[] args = {"MyDisk", 1273}; + * System.out.println(msgFmt.format(args)); + * } + * The output with different values for {@code fileCount}: + *

+ * The disk "MyDisk" contains no files.
+ * The disk "MyDisk" contains one file.
+ * The disk "MyDisk" contains 1,273 files.
+ * 
+ * See {@link MessageFormat##pattern_caveats MessageFormat} for caveats regarding + * {@code MessageFormat} patterns within a {@code ChoiceFormat} pattern. + * *

Synchronization

* *

@@ -254,7 +256,7 @@ private void applyPatternImpl(String newPattern) { double[] newChoiceLimits = new double[30]; String[] newChoiceFormats = new String[30]; int count = 0; - int part = 0; + int part = 0; // 0 denotes limit, 1 denotes format double startValue = 0; double oldStartValue = Double.NaN; boolean inQuote = false; @@ -270,7 +272,10 @@ private void applyPatternImpl(String newPattern) { } } else if (inQuote) { segments[part].append(ch); - } else if (ch == '<' || ch == '#' || ch == '\u2264') { + } else if (part == 0 && (ch == '<' || ch == '#' || ch == '\u2264')) { + // Only consider relational symbols if parsing the limit segment (part == 0). + // Don't treat a relational symbol as syntactically significant + // when parsing Format segment (part == 1) if (segments[0].length() == 0) { throw new IllegalArgumentException("Each interval must" + " contain a number before a format"); diff --git a/test/jdk/java/text/Format/ChoiceFormat/PatternsTest.java b/test/jdk/java/text/Format/ChoiceFormat/PatternsTest.java index dc514f5459f..82cf256b70b 100644 --- a/test/jdk/java/text/Format/ChoiceFormat/PatternsTest.java +++ b/test/jdk/java/text/Format/ChoiceFormat/PatternsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 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 @@ -23,7 +23,7 @@ /* * @test - * @bug 6801704 + * @bug 6285888 6801704 * @summary Test the expected behavior for a wide range of patterns (both * correct and incorrect). This test documents the behavior of incorrect * ChoiceFormat patterns either throwing an exception, or discarding @@ -101,10 +101,13 @@ private static Arguments[] invalidPatternsThrowsTest() { arguments("0#foo|#|1#bar", ERR1), // Missing Relation in SubPattern arguments("#|", ERR1), // Missing Limit arguments("##|", ERR1), // Double Relations - arguments("0#foo1#", ERR1), // SubPattern not separated by '|' - arguments("0#foo#", ERR1), // Using a Relation in a format arguments("0#test|#", ERR1), // SubPattern missing Limit arguments("0#foo|3#bar|1#baz", ERR2), // Non-ascending Limits + + // No longer throw IAE after 6285888, as relational symbols + // can now be used within the Format segment. + // arguments("0#foo1#", ERR1), // SubPattern not separated by '|' + // arguments("0#foo#", ERR1), // Using a Relation in a format }; } diff --git a/test/jdk/java/text/Format/ChoiceFormat/SymbolsInFormatSegment.java b/test/jdk/java/text/Format/ChoiceFormat/SymbolsInFormatSegment.java new file mode 100644 index 00000000000..678c7854e79 --- /dev/null +++ b/test/jdk/java/text/Format/ChoiceFormat/SymbolsInFormatSegment.java @@ -0,0 +1,80 @@ +/* + * 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 6285888 + * @summary Ensure ChoiceFormat supports "#", "<", "≤" within + * the format segment of a ChoiceFormat String pattern + * @run junit SymbolsInFormatSegment + */ + +import java.text.ChoiceFormat; +import java.util.stream.Stream; + +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.assertEquals; + +/* + * These tests would previously throw IAEs on all input as ChoiceFormat would parse + * the relational symbol syntactically (when not needed). With the associated change + * set, ChoiceFormat knows to not treat any subsequent relational symbols as + * syntactically significant unless a '|' has been parsed. + */ +public class SymbolsInFormatSegment { + + // Test a variety of patterns with relational symbols in the Format segment + @ParameterizedTest + @MethodSource("patternsWithSymbols") + public void allowInConstructor(String pattern, String expected, int limit) { + var cf = new ChoiceFormat(pattern); + assertEquals(expected, cf.format(limit)); + } + + // Same as previous test, but check the applyPattern method + @ParameterizedTest + @MethodSource("patternsWithSymbols") + public void allowInApplyPattern(String pattern, String expected, int limit) { + var cf = new ChoiceFormat(""); + cf.applyPattern(pattern); + assertEquals(expected, cf.format(limit)); + } + + private static Stream patternsWithSymbols() { + return Stream.of( + // CSR example + Arguments.of("1#The code is #7281", "The code is #7281", 1), + // Other examples + Arguments.of("1#<", "<", 1), + Arguments.of("1#foo<", "foo<", 1), + Arguments.of("1