Skip to content

Commit

Permalink
6285888: ChoiceFormat can support unescaped relational symbols in the…
Browse files Browse the repository at this point in the history
… Format segment

Reviewed-by: naoto
  • Loading branch information
Justin Lu committed Feb 1, 2024
1 parent 144a08e commit d3c3194
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 65 deletions.
127 changes: 66 additions & 61 deletions src/java.base/share/classes/java/text/ChoiceFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>
* 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,
* <ul>
* <li>
* <em>limits</em> = {1,2,3,4,5,6,7}<br>
* <em>formats</em> = {"Sun","Mon","Tue","Wed","Thur","Fri","Sat"}
* <li>
* <em>limits</em> = {0, 1, ChoiceFormat.nextDouble(1)}<br>
* <em>formats</em> = {"no files", "one file", "many files"}<br>
* ({@code nextDouble} can be used to get the next higher double, to
* make the half-open interval.)
* </ul>
*
* <p>
* 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));
* }
* }
*
* <p>
* 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}:
* <blockquote><pre>
* The disk "MyDisk" contains no files.
* The disk "MyDisk" contains one file.
* The disk "MyDisk" contains 1,273 files.
* </pre></blockquote>
* See {@link MessageFormat##pattern_caveats MessageFormat} for caveats regarding
* {@code MessageFormat} patterns within a {@code ChoiceFormat} pattern.
*
* <h2><a id="patterns">Patterns</a></h2>
* A {@code ChoiceFormat} pattern has the following syntax:
* <blockquote>
* <dl>
* <dt><i>Pattern:</i>
* <dd>SubPattern *("|" SubPattern)
* <dd><i>Note: Each additional SubPattern must have a Limit greater than the previous SubPattern's Limit</i>
* </dl>
*
* <dl>
* <dt><i>SubPattern:</i>
* <dd>Limit Relation Format
* <dd><sub>Note: Each additional SubPattern must have an ascending Limit-Relation interval</sub></dd>
* </dl>
*
* <dl>
Expand Down Expand Up @@ -172,20 +119,54 @@
*
* <dl>
* <dt><i>Format:</i>
* <dd>Any characters except the <i>Relation</i> symbols
* <dd>Any characters except the special pattern character '|'
* </dl>
*
* </blockquote>
*
* <i>Note:The relation &le; is not equivalent to &lt;&equals;</i>
*
* <p>If a <i>Relation</i> symbol is to be used within a <i>Format</i> pattern,
* it must be single quoted. For example,
* {@code new ChoiceFormat("1# '#'1 ").format(1)} returns {@code " #1 "}.
* <p> To use a reserved special pattern character within a <i>Format</i> 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' "}.
*
* <p>Below is an example of constructing a ChoiceFormat with a pattern:
* <h2>Usage Information</h2>
*
* <p>
* 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,
* <ul>
* <li>
* <em>limits</em> = {1,2,3,4,5,6,7}<br>
* <em>formats</em> = {"Sun","Mon","Tue","Wed","Thur","Fri","Sat"}
* <li>
* <em>limits</em> = {0, 1, ChoiceFormat.nextDouble(1)}<br>
* <em>formats</em> = {"no files", "one file", "many files"}<br>
* ({@code nextDouble} can be used to get the next higher double, to
* make the half-open interval.)
* </ul>
*
* <p>
* 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));
* }
* }
*
* <p>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<is 1+ |2#is two |2<is more than 2.");
Expand All @@ -202,6 +183,27 @@
* System.out.println(fmt.format(Double.POSITIVE_INFINITY)); // outputs "is more than 2."
* }
*
* <p>
* 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}:
* <blockquote><pre>
* The disk "MyDisk" contains no files.
* The disk "MyDisk" contains one file.
* The disk "MyDisk" contains 1,273 files.
* </pre></blockquote>
* See {@link MessageFormat##pattern_caveats MessageFormat} for caveats regarding
* {@code MessageFormat} patterns within a {@code ChoiceFormat} pattern.
*
* <h2><a id="synchronization">Synchronization</a></h2>
*
* <p>
Expand Down Expand Up @@ -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;
Expand All @@ -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");
Expand Down
11 changes: 7 additions & 4 deletions test/jdk/java/text/Format/ChoiceFormat/PatternsTest.java
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
};
}

Expand Down
80 changes: 80 additions & 0 deletions test/jdk/java/text/Format/ChoiceFormat/SymbolsInFormatSegment.java
Original file line number Diff line number Diff line change
@@ -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<Arguments> 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<foo\u2264", "foo\u2264", 1),
Arguments.of("1\u2264foo#", "foo#", 1),
Arguments.of("1#foo<#\u2264|2\u2264baz<#\u2264", "baz<#\u2264", 100),
Arguments.of("1#foo<#\u2264|2#baz<#\u2264|3<bar##\u2264", "bar##\u2264", 100),
Arguments.of("1#foo<#\u2264|2#baz<#\u2264|3\u2264bar##\u2264", "bar##\u2264", 100)
);
}
}

1 comment on commit d3c3194

@openjdk-notifier
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.