mirror of
https://github.com/openjdk/jdk.git
synced 2025-08-27 14:54:52 +02:00
8325898: ChoiceFormat returns erroneous result when formatting bad pattern
Reviewed-by: naoto
This commit is contained in:
parent
93feda3d9a
commit
d22d890cac
2 changed files with 115 additions and 112 deletions
|
@ -41,7 +41,9 @@ package java.text;
|
||||||
import java.io.InvalidObjectException;
|
import java.io.InvalidObjectException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.ObjectInputStream;
|
import java.io.ObjectInputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@code ChoiceFormat} is a concrete subclass of {@code NumberFormat} that
|
* {@code ChoiceFormat} is a concrete subclass of {@code NumberFormat} that
|
||||||
|
@ -238,6 +240,7 @@ public class ChoiceFormat extends NumberFormat {
|
||||||
* @see #ChoiceFormat(String)
|
* @see #ChoiceFormat(String)
|
||||||
*/
|
*/
|
||||||
public void applyPattern(String newPattern) {
|
public void applyPattern(String newPattern) {
|
||||||
|
Objects.requireNonNull(newPattern, "newPattern must not be null");
|
||||||
applyPatternImpl(newPattern);
|
applyPatternImpl(newPattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,86 +252,92 @@ public class ChoiceFormat extends NumberFormat {
|
||||||
* further understanding of certain special characters: "#", "<", "\u2264", "|".
|
* further understanding of certain special characters: "#", "<", "\u2264", "|".
|
||||||
*/
|
*/
|
||||||
private void applyPatternImpl(String newPattern) {
|
private void applyPatternImpl(String newPattern) {
|
||||||
StringBuilder[] segments = new StringBuilder[2];
|
// Set up components
|
||||||
for (int i = 0; i < segments.length; ++i) {
|
ArrayList<Double> limits = new ArrayList<>();
|
||||||
segments[i] = new StringBuilder();
|
ArrayList<String> formats = new ArrayList<>();
|
||||||
}
|
StringBuilder[] segments = new StringBuilder[]{new StringBuilder(),
|
||||||
double[] newChoiceLimits = new double[30];
|
new StringBuilder()};
|
||||||
String[] newChoiceFormats = new String[30];
|
int part = 0; // 0 denotes LIMIT. 1 denotes FORMAT.
|
||||||
int count = 0;
|
double limit = 0;
|
||||||
int part = 0; // 0 denotes limit, 1 denotes format
|
|
||||||
double startValue = 0;
|
|
||||||
double oldStartValue = Double.NaN;
|
|
||||||
boolean inQuote = false;
|
boolean inQuote = false;
|
||||||
|
|
||||||
|
// Parse the string, alternating the value of part
|
||||||
for (int i = 0; i < newPattern.length(); ++i) {
|
for (int i = 0; i < newPattern.length(); ++i) {
|
||||||
char ch = newPattern.charAt(i);
|
char ch = newPattern.charAt(i);
|
||||||
if (ch=='\'') {
|
switch (ch) {
|
||||||
// Check for "''" indicating a literal quote
|
case '\'':
|
||||||
if ((i+1)<newPattern.length() && newPattern.charAt(i+1)==ch) {
|
// Check for "''" indicating a literal quote
|
||||||
|
if ((i + 1) < newPattern.length() && newPattern.charAt(i + 1) == ch) {
|
||||||
|
segments[part].append(ch);
|
||||||
|
++i;
|
||||||
|
} else {
|
||||||
|
inQuote = !inQuote;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '<', '#', '\u2264':
|
||||||
|
if (inQuote || part == 1) {
|
||||||
|
// Don't interpret relational symbols if parsing the format
|
||||||
|
segments[part].append(ch);
|
||||||
|
} else {
|
||||||
|
// Build the numerical value of the limit
|
||||||
|
// and switch to parsing format
|
||||||
|
if (segments[0].isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Each interval must" +
|
||||||
|
" contain a number before a format");
|
||||||
|
}
|
||||||
|
limit = stringToNum(segments[0].toString());
|
||||||
|
if (ch == '<' && Double.isFinite(limit)) {
|
||||||
|
limit = nextDouble(limit);
|
||||||
|
}
|
||||||
|
if (!limits.isEmpty() && limit <= limits.getLast()) {
|
||||||
|
throw new IllegalArgumentException("Incorrect order " +
|
||||||
|
"of intervals, must be in ascending order");
|
||||||
|
}
|
||||||
|
segments[0].setLength(0);
|
||||||
|
part = 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '|':
|
||||||
|
if (inQuote) {
|
||||||
|
segments[part].append(ch);
|
||||||
|
} else {
|
||||||
|
if (part != 1) {
|
||||||
|
// Discard incorrect portion and finish building cFmt
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Insert an entry into the format and limit arrays
|
||||||
|
// and switch to parsing limit
|
||||||
|
limits.add(limit);
|
||||||
|
formats.add(segments[1].toString());
|
||||||
|
segments[1].setLength(0);
|
||||||
|
part = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
segments[part].append(ch);
|
segments[part].append(ch);
|
||||||
++i;
|
|
||||||
} else {
|
|
||||||
inQuote = !inQuote;
|
|
||||||
}
|
|
||||||
} else if (inQuote) {
|
|
||||||
segments[part].append(ch);
|
|
||||||
} 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");
|
|
||||||
}
|
|
||||||
|
|
||||||
String tempBuffer = segments[0].toString();
|
|
||||||
if (tempBuffer.equals("\u221E")) {
|
|
||||||
startValue = Double.POSITIVE_INFINITY;
|
|
||||||
} else if (tempBuffer.equals("-\u221E")) {
|
|
||||||
startValue = Double.NEGATIVE_INFINITY;
|
|
||||||
} else {
|
|
||||||
startValue = Double.parseDouble(tempBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ch == '<' && startValue != Double.POSITIVE_INFINITY &&
|
|
||||||
startValue != Double.NEGATIVE_INFINITY) {
|
|
||||||
startValue = nextDouble(startValue);
|
|
||||||
}
|
|
||||||
if (startValue <= oldStartValue) {
|
|
||||||
throw new IllegalArgumentException("Incorrect order of"
|
|
||||||
+ " intervals, must be in ascending order");
|
|
||||||
}
|
|
||||||
segments[0].setLength(0);
|
|
||||||
part = 1;
|
|
||||||
} else if (ch == '|') {
|
|
||||||
if (count == newChoiceLimits.length) {
|
|
||||||
newChoiceLimits = doubleArraySize(newChoiceLimits);
|
|
||||||
newChoiceFormats = doubleArraySize(newChoiceFormats);
|
|
||||||
}
|
|
||||||
newChoiceLimits[count] = startValue;
|
|
||||||
newChoiceFormats[count] = segments[1].toString();
|
|
||||||
++count;
|
|
||||||
oldStartValue = startValue;
|
|
||||||
segments[1].setLength(0);
|
|
||||||
part = 0;
|
|
||||||
} else {
|
|
||||||
segments[part].append(ch);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// clean up last one
|
|
||||||
|
// clean up last one (SubPattern without trailing '|')
|
||||||
if (part == 1) {
|
if (part == 1) {
|
||||||
if (count == newChoiceLimits.length) {
|
limits.add(limit);
|
||||||
newChoiceLimits = doubleArraySize(newChoiceLimits);
|
formats.add(segments[1].toString());
|
||||||
newChoiceFormats = doubleArraySize(newChoiceFormats);
|
|
||||||
}
|
|
||||||
newChoiceLimits[count] = startValue;
|
|
||||||
newChoiceFormats[count] = segments[1].toString();
|
|
||||||
++count;
|
|
||||||
}
|
}
|
||||||
choiceLimits = new double[count];
|
choiceLimits = limits.stream().mapToDouble(d -> d).toArray();
|
||||||
System.arraycopy(newChoiceLimits, 0, choiceLimits, 0, count);
|
choiceFormats = formats.toArray(new String[0]);
|
||||||
choiceFormats = new String[count];
|
}
|
||||||
System.arraycopy(newChoiceFormats, 0, choiceFormats, 0, count);
|
|
||||||
|
/**
|
||||||
|
* Converts a string value to its double representation; this is used
|
||||||
|
* to create the limit segment while applying a pattern.
|
||||||
|
* Handles "\u221E", as specified by the pattern syntax.
|
||||||
|
*/
|
||||||
|
private static double stringToNum(String str) {
|
||||||
|
return switch (str) {
|
||||||
|
case "\u221E" -> Double.POSITIVE_INFINITY;
|
||||||
|
case "-\u221E" -> Double.NEGATIVE_INFINITY;
|
||||||
|
default -> Double.parseDouble(str);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -402,6 +411,7 @@ public class ChoiceFormat extends NumberFormat {
|
||||||
* @see #applyPattern
|
* @see #applyPattern
|
||||||
*/
|
*/
|
||||||
public ChoiceFormat(String newPattern) {
|
public ChoiceFormat(String newPattern) {
|
||||||
|
Objects.requireNonNull(newPattern, "newPattern must not be null");
|
||||||
applyPatternImpl(newPattern);
|
applyPatternImpl(newPattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -574,6 +584,24 @@ public class ChoiceFormat extends NumberFormat {
|
||||||
return Math.nextUp(d);
|
return Math.nextUp(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the least double greater than {@code d} (if {@code positive} is
|
||||||
|
* {@code true}), or the greatest double less than {@code d} (if
|
||||||
|
* {@code positive} is {@code false}).
|
||||||
|
* If {@code NaN}, returns same value.
|
||||||
|
*
|
||||||
|
* @implNote This is equivalent to calling
|
||||||
|
* {@code positive ? Math.nextUp(d) : Math.nextDown(d)}
|
||||||
|
*
|
||||||
|
* @param d the reference value
|
||||||
|
* @param positive {@code true} if the least double is desired;
|
||||||
|
* {@code false} otherwise
|
||||||
|
* @return the least or greater double value
|
||||||
|
*/
|
||||||
|
public static double nextDouble (double d, boolean positive) {
|
||||||
|
return positive ? Math.nextUp(d) : Math.nextDown(d);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the greatest double less than {@code d}.
|
* Finds the greatest double less than {@code d}.
|
||||||
* If {@code NaN}, returns same value.
|
* If {@code NaN}, returns same value.
|
||||||
|
@ -593,8 +621,7 @@ public class ChoiceFormat extends NumberFormat {
|
||||||
* Overrides Cloneable
|
* Overrides Cloneable
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Object clone()
|
public Object clone() {
|
||||||
{
|
|
||||||
ChoiceFormat other = (ChoiceFormat) super.clone();
|
ChoiceFormat other = (ChoiceFormat) super.clone();
|
||||||
// for primitives or immutables, shallow clone is enough
|
// for primitives or immutables, shallow clone is enough
|
||||||
other.choiceLimits = choiceLimits.clone();
|
other.choiceLimits = choiceLimits.clone();
|
||||||
|
@ -685,37 +712,4 @@ public class ChoiceFormat extends NumberFormat {
|
||||||
* @serial
|
* @serial
|
||||||
*/
|
*/
|
||||||
private String[] choiceFormats;
|
private String[] choiceFormats;
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the least double greater than {@code d} (if {@code positive} is
|
|
||||||
* {@code true}), or the greatest double less than {@code d} (if
|
|
||||||
* {@code positive} is {@code false}).
|
|
||||||
* If {@code NaN}, returns same value.
|
|
||||||
*
|
|
||||||
* @implNote This is equivalent to calling
|
|
||||||
* {@code positive ? Math.nextUp(d) : Math.nextDown(d)}
|
|
||||||
*
|
|
||||||
* @param d the reference value
|
|
||||||
* @param positive {@code true} if the least double is desired;
|
|
||||||
* {@code false} otherwise
|
|
||||||
* @return the least or greater double value
|
|
||||||
*/
|
|
||||||
public static double nextDouble (double d, boolean positive) {
|
|
||||||
return positive ? Math.nextUp(d) : Math.nextDown(d);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double[] doubleArraySize(double[] array) {
|
|
||||||
int oldSize = array.length;
|
|
||||||
double[] newArray = new double[oldSize * 2];
|
|
||||||
System.arraycopy(array, 0, newArray, 0, oldSize);
|
|
||||||
return newArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String[] doubleArraySize(String[] array) {
|
|
||||||
int oldSize = array.length;
|
|
||||||
String[] newArray = new String[oldSize * 2];
|
|
||||||
System.arraycopy(array, 0, newArray, 0, oldSize);
|
|
||||||
return newArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @test
|
* @test
|
||||||
* @bug 6285888 6801704
|
* @bug 6285888 6801704 8325898
|
||||||
* @summary Test the expected behavior for a wide range of patterns (both
|
* @summary Test the expected behavior for a wide range of patterns (both
|
||||||
* correct and incorrect). This test documents the behavior of incorrect
|
* correct and incorrect). This test documents the behavior of incorrect
|
||||||
* ChoiceFormat patterns either throwing an exception, or discarding
|
* ChoiceFormat patterns either throwing an exception, or discarding
|
||||||
|
@ -31,14 +31,13 @@
|
||||||
* @run junit PatternsTest
|
* @run junit PatternsTest
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import java.text.ChoiceFormat;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.Arguments;
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
import org.junit.jupiter.params.provider.MethodSource;
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
import java.text.ChoiceFormat;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.params.provider.Arguments.arguments;
|
import static org.junit.jupiter.params.provider.Arguments.arguments;
|
||||||
|
@ -97,6 +96,7 @@ public class PatternsTest {
|
||||||
// an exception.
|
// an exception.
|
||||||
private static Arguments[] invalidPatternsThrowsTest() {
|
private static Arguments[] invalidPatternsThrowsTest() {
|
||||||
return new Arguments[] {
|
return new Arguments[] {
|
||||||
|
arguments("#", ERR1), // Only relation
|
||||||
arguments("#foo", ERR1), // No Limit
|
arguments("#foo", ERR1), // No Limit
|
||||||
arguments("0#foo|#|1#bar", ERR1), // Missing Relation in SubPattern
|
arguments("0#foo|#|1#bar", ERR1), // Missing Relation in SubPattern
|
||||||
arguments("#|", ERR1), // Missing Limit
|
arguments("#|", ERR1), // Missing Limit
|
||||||
|
@ -127,11 +127,20 @@ public class PatternsTest {
|
||||||
// after discarding occurs.
|
// after discarding occurs.
|
||||||
private static Arguments[] invalidPatternsDiscardedTest() {
|
private static Arguments[] invalidPatternsDiscardedTest() {
|
||||||
return new Arguments[] {
|
return new Arguments[] {
|
||||||
|
// Incomplete SubPattern (limit only) at end of Pattern
|
||||||
|
arguments("1#bar|2", "1#bar"),
|
||||||
// Incomplete SubPattern at the end of the Pattern
|
// Incomplete SubPattern at the end of the Pattern
|
||||||
arguments("0#foo|1#bar|baz", "0#foo|1#bar"),
|
arguments("0#foo|1#bar|baz", "0#foo|1#bar"),
|
||||||
|
// Incomplete SubPattern with trailing | at the end of the Pattern
|
||||||
|
// Prior to 6801704, it created the broken "0#foo|1#bar|1#"
|
||||||
|
// which caused formatting 1 to return an empty string
|
||||||
|
arguments("0#foo|1#bar|baz|", "0#foo|1#bar"),
|
||||||
|
// Same as previous, with additional incomplete subPatterns
|
||||||
|
arguments("0#foo|1#bar|baz|quux", "0#foo|1#bar"),
|
||||||
|
|
||||||
// --- These throw an ArrayIndexOutOfBoundsException
|
// --- These throw an ArrayIndexOutOfBoundsException
|
||||||
// when attempting to format with them ---
|
// when attempting to format with them as the incomplete patterns
|
||||||
|
// are discarded, initializing the cFmt with empty limits and formats ---
|
||||||
// SubPattern with only a Limit (which is interpreted as a Format)
|
// SubPattern with only a Limit (which is interpreted as a Format)
|
||||||
arguments("0", ""),
|
arguments("0", ""),
|
||||||
// SubPattern with only a Format
|
// SubPattern with only a Format
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue