8327640: Allow NumberFormat strict parsing

Reviewed-by: naoto
This commit is contained in:
Justin Lu 2024-04-16 16:18:09 +00:00
parent 2ede14335a
commit 941bee197f
12 changed files with 1569 additions and 103 deletions

View File

@ -586,6 +586,18 @@ public class ChoiceFormat extends NumberFormat {
return Double.valueOf(bestNumber);
}
@Override
public boolean isStrict() {
throw new UnsupportedOperationException(
"ChoiceFormat does not utilize leniency when parsing");
}
@Override
public void setStrict(boolean strict) {
throw new UnsupportedOperationException(
"ChoiceFormat does not utilize leniency when parsing");
}
/**
* Finds the least double greater than {@code d}.
* If {@code NaN}, returns same value.

View File

@ -348,6 +348,15 @@ public final class CompactNumberFormat extends NumberFormat {
*/
private String pluralRules = "";
/**
* True if this {@code CompactNumberFormat} will parse numbers with strict
* leniency.
*
* @serial
* @since 23
*/
private boolean parseStrict = false;
/**
* The map for plural rules that maps LDML defined tags (e.g. "one") to
* its rule.
@ -1498,22 +1507,40 @@ public final class CompactNumberFormat extends NumberFormat {
}
/**
* Parses a compact number from a string to produce a {@code Number}.
* {@inheritDoc NumberFormat}
* <p>
* The method attempts to parse text starting at the index given by
* {@code pos}.
* If parsing succeeds, then the index of {@code pos} is updated
* to the index after the last character used (parsing does not necessarily
* use all characters up to the end of the string), and the parsed
* number is returned. The updated {@code pos} can be used to
* indicate the starting point for the next call to this method.
* If an error occurs, then the index of {@code pos} is not
* changed, the error index of {@code pos} is set to the index of
* the character where the error occurred, and {@code null} is returned.
* <p>
* The value is the numeric part in the given text multiplied
* The returned value is the numeric part in the given text multiplied
* by the numeric equivalent of the affix attached
* (For example, "K" = 1000 in {@link java.util.Locale#US US locale}).
* <p>
* A {@code CompactNumberFormat} can match
* the default prefix/suffix to a compact prefix/suffix interchangeably.
* <p>
* Parsing can be done in either a strict or lenient manner, by default it is lenient.
* <p>
* Parsing fails when <b>lenient</b>, if the prefix and/or suffix are non-empty
* and cannot be found due to parsing ending early, or the first character
* after the prefix cannot be parsed.
* <p>
* Parsing fails when <b>strict</b>, if in {@code text},
* <ul>
* <li> The default or a compact prefix is not found. For example, the {@code
* Locale.US} currency format prefix: "{@code $}"
* <li> The default or a compact suffix is not found. For example, a {@code Locale.US}
* {@link NumberFormat.Style#SHORT} compact suffix: "{@code K}"
* <li> {@link #isGroupingUsed()} returns {@code false}, and the grouping
* symbol is found
* <li> {@link #isGroupingUsed()} returns {@code true}, and {@link
* #getGroupingSize()} is not adhered to
* <li> {@link #isParseIntegerOnly()} returns {@code true}, and the decimal
* separator is found
* <li> {@link #isGroupingUsed()} returns {@code true} and {@link
* #isParseIntegerOnly()} returns {@code false}, and the grouping
* symbol occurs after the decimal separator
* <li> Any other characters are found, that are not the expected symbols,
* and are not digits that occur within the numerical portion
* </ul>
* <p>
* The subclass returned depends on the value of
* {@link #isParseBigDecimal}.
* <ul>
@ -1553,7 +1580,6 @@ public final class CompactNumberFormat extends NumberFormat {
* @return the parsed value, or {@code null} if the parse fails
* @throws NullPointerException if {@code text} or
* {@code pos} is null
*
*/
@Override
public Number parse(String text, ParsePosition pos) {
@ -1661,6 +1687,13 @@ public final class CompactNumberFormat extends NumberFormat {
return cnfMultiplier;
}
}
} else {
// Neither prefix match, should fail now (strict or lenient), before
// position is incremented by subparseNumber(). Otherwise, an empty
// prefix could pass through here, position gets incremented by the
// numerical portion, and return a faulty errorIndex and index later.
pos.errorIndex = position;
return null;
}
digitList.setRoundingMode(getRoundingMode());
@ -1705,6 +1738,11 @@ public final class CompactNumberFormat extends NumberFormat {
status, gotPositive, gotNegative, num);
if (multiplier.longValue() == -1L) {
if (parseStrict) {
// When strict, if -1L was returned, index should be
// reset to the original index to ensure failure
pos.index = oldStart;
}
return null;
} else if (multiplier.longValue() != 1L) {
cnfMultiplier = multiplier;
@ -1886,7 +1924,10 @@ public final class CompactNumberFormat extends NumberFormat {
if (prefix.equals(matchedPrefix)
|| matchedPrefix.equals(defaultPrefix)) {
return matchAffix(text, position, suffix, defaultSuffix, matchedSuffix);
// Suffix must match exactly when strict
return parseStrict ? matchAffix(text, position, suffix, defaultSuffix, matchedSuffix)
&& text.length() == position + suffix.length()
: matchAffix(text, position, suffix, defaultSuffix, matchedSuffix);
}
return false;
}
@ -1924,10 +1965,11 @@ public final class CompactNumberFormat extends NumberFormat {
String positiveSuffix = getAffix(true, false, false, compactIndex, num);
String negativeSuffix = getAffix(true, false, true, compactIndex, num);
// Do not break if a match occur; there is a possibility that the
// When lenient, do not break if a match occurs; there is a possibility that the
// subsequent affixes may match the longer subsequence in the given
// string.
// For example, matching "3Mdx" with "M", "Md" should match with "Md"
// string. For example, matching "3Mdx" with "M", "Md" should match
// with "Md". However, when strict, break as the match should be exact,
// and thus no need to check for a longer suffix.
boolean match = matchPrefixAndSuffix(text, position, positivePrefix, matchedPrefix,
defaultDecimalFormat.getPositivePrefix(), positiveSuffix,
matchedPosSuffix, defaultDecimalFormat.getPositiveSuffix());
@ -1935,6 +1977,10 @@ public final class CompactNumberFormat extends NumberFormat {
matchedPosIndex = compactIndex;
matchedPosSuffix = positiveSuffix;
gotPos = true;
if (parseStrict) {
// when strict, exit early with exact match, same for negative
break;
}
}
match = matchPrefixAndSuffix(text, position, negativePrefix, matchedPrefix,
@ -1944,29 +1990,39 @@ public final class CompactNumberFormat extends NumberFormat {
matchedNegIndex = compactIndex;
matchedNegSuffix = negativeSuffix;
gotNeg = true;
if (parseStrict) {
break;
}
}
}
// Suffix in the given text does not match with the compact
// patterns suffixes; match with the default suffix
// When strict, text must end with the default suffix
if (!gotPos && !gotNeg) {
String positiveSuffix = defaultDecimalFormat.getPositiveSuffix();
String negativeSuffix = defaultDecimalFormat.getNegativeSuffix();
if (text.regionMatches(position, positiveSuffix, 0,
positiveSuffix.length())) {
boolean containsPosSuffix = text.regionMatches(position,
positiveSuffix, 0, positiveSuffix.length());
boolean endsWithPosSuffix = containsPosSuffix && text.length() ==
position + positiveSuffix.length();
if (parseStrict ? endsWithPosSuffix : containsPosSuffix) {
// Matches the default positive prefix
matchedPosSuffix = positiveSuffix;
gotPos = true;
}
if (text.regionMatches(position, negativeSuffix, 0,
negativeSuffix.length())) {
boolean containsNegSuffix = text.regionMatches(position,
negativeSuffix, 0, negativeSuffix.length());
boolean endsWithNegSuffix = containsNegSuffix && text.length() ==
position + negativeSuffix.length();
if (parseStrict ? endsWithNegSuffix : containsNegSuffix) {
// Matches the default negative suffix
matchedNegSuffix = negativeSuffix;
gotNeg = true;
}
}
// If both matches, take the longest one
// If both match, take the longest one
if (gotPos && gotNeg) {
if (matchedPosSuffix.length() > matchedNegSuffix.length()) {
gotNeg = false;
@ -2077,6 +2133,7 @@ public final class CompactNumberFormat extends NumberFormat {
decimalFormat.setGroupingSize(getGroupingSize());
decimalFormat.setGroupingUsed(isGroupingUsed());
decimalFormat.setParseIntegerOnly(isParseIntegerOnly());
decimalFormat.setStrict(parseStrict);
try {
defaultDecimalFormat = new DecimalFormat(decimalPattern, symbols);
@ -2316,6 +2373,31 @@ public final class CompactNumberFormat extends NumberFormat {
super.setParseIntegerOnly(value);
}
/**
* {@inheritDoc NumberFormat}
*
* @see #setStrict(boolean)
* @see #parse(String, ParsePosition)
* @since 23
*/
@Override
public boolean isStrict() {
return parseStrict;
}
/**
* {@inheritDoc NumberFormat}
*
* @see #isStrict()
* @see #parse(String, ParsePosition)
* @since 23
*/
@Override
public void setStrict(boolean strict) {
decimalFormat.setStrict(strict);
parseStrict = strict; // don't call super, default is UOE
}
/**
* Returns whether the {@link #parse(String, ParsePosition)}
* method returns {@code BigDecimal}. The default value is false.
@ -2373,7 +2455,8 @@ public final class CompactNumberFormat extends NumberFormat {
&& roundingMode.equals(other.roundingMode)
&& pluralRules.equals(other.pluralRules)
&& groupingSize == other.groupingSize
&& parseBigDecimal == other.parseBigDecimal;
&& parseBigDecimal == other.parseBigDecimal
&& parseStrict == other.parseStrict;
}
/**

View File

@ -50,6 +50,7 @@ import java.util.Currency;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import sun.util.locale.provider.LocaleProviderAdapter;
import sun.util.locale.provider.ResourceBundleBasedAdapter;
@ -2140,18 +2141,32 @@ public class DecimalFormat extends NumberFormat {
}
/**
* Parses text from a string to produce a {@code Number}.
* {@inheritDoc NumberFormat}
* <p>
* The method attempts to parse text starting at the index given by
* {@code pos}.
* If parsing succeeds, then the index of {@code pos} is updated
* to the index after the last character used (parsing does not necessarily
* use all characters up to the end of the string), and the parsed
* number is returned. The updated {@code pos} can be used to
* indicate the starting point for the next call to this method.
* If an error occurs, then the index of {@code pos} is not
* changed, the error index of {@code pos} is set to the index of
* the character where the error occurred, and null is returned.
* Parsing can be done in either a strict or lenient manner, by default it is lenient.
* <p>
* Parsing fails when <b>lenient</b>, if the prefix and/or suffix are non-empty
* and cannot be found due to parsing ending early, or the first character
* after the prefix cannot be parsed.
* <p>
* Parsing fails when <b>strict</b>, if in {@code text},
* <ul>
* <li> The prefix is not found. For example, a {@code Locale.US} currency
* format prefix: "{@code $}"
* <li> The suffix is not found. For example, a {@code Locale.US} percent
* format suffix: "{@code %}"
* <li> {@link #isGroupingUsed()} returns {@code true}, and {@link
* #getGroupingSize()} is not adhered to
* <li> {@link #isGroupingUsed()} returns {@code false}, and the grouping
* symbol is found
* <li> {@link #isParseIntegerOnly()} returns {@code true}, and the decimal
* separator is found
* <li> {@link #isGroupingUsed()} returns {@code true} and {@link
* #isParseIntegerOnly()} returns {@code false}, and the grouping
* symbol occurs after the decimal separator
* <li> Any other characters are found, that are not the expected symbols,
* and are not digits that occur within the numerical portion
* </ul>
* <p>
* The subclass returned depends on the value of {@link #isParseBigDecimal}
* as well as on the string being parsed.
@ -2371,22 +2386,34 @@ public class DecimalFormat extends NumberFormat {
return false;
}
// position will serve as new index when success, otherwise it will
// serve as errorIndex when failure
position = subparseNumber(text, position, digits, true, isExponent, status);
// First character after the prefix was un-parseable, should
// fail regardless if lenient or strict.
if (position == -1) {
parsePosition.index = oldStart;
parsePosition.errorIndex = oldStart;
return false;
}
// Check for suffix
// When strict, text should end with the suffix.
// When lenient, text only needs to contain the suffix.
if (!isExponent) {
if (gotPositive) {
gotPositive = text.regionMatches(position,positiveSuffix,0,
positiveSuffix.length());
boolean containsPosSuffix =
text.regionMatches(position, positiveSuffix, 0, positiveSuffix.length());
boolean endsWithPosSuffix =
containsPosSuffix && text.length() == position + positiveSuffix.length();
gotPositive = parseStrict ? endsWithPosSuffix : containsPosSuffix;
}
if (gotNegative) {
gotNegative = text.regionMatches(position,negativeSuffix,0,
negativeSuffix.length());
boolean containsNegSuffix =
text.regionMatches(position, negativeSuffix, 0, negativeSuffix.length());
boolean endsWithNegSuffix =
containsNegSuffix && text.length() == position + negativeSuffix.length();
gotNegative = parseStrict ? endsWithNegSuffix : containsNegSuffix;
}
// If both match, take longest
@ -2404,8 +2431,9 @@ public class DecimalFormat extends NumberFormat {
return false;
}
// No failures, thus increment the index by the suffix
parsePosition.index = position +
(gotPositive ? positiveSuffix.length() : negativeSuffix.length()); // mark success!
(gotPositive ? positiveSuffix.length() : negativeSuffix.length());
} else {
parsePosition.index = position;
}
@ -2420,7 +2448,7 @@ public class DecimalFormat extends NumberFormat {
/**
* Parses a number from the given {@code text}. The text is parsed
* beginning at position, until an unparseable character is seen.
* beginning at {@code position}, until an unparseable character is seen.
*
* @param text the string to parse
* @param position the position at which parsing begins
@ -2438,7 +2466,7 @@ public class DecimalFormat extends NumberFormat {
boolean isExponent, boolean[] status) {
// process digits or Inf, find decimal position
status[STATUS_INFINITE] = false;
if (!isExponent && text.regionMatches(position,symbols.getInfinity(),0,
if (!isExponent && text.regionMatches(position, symbols.getInfinity(), 0,
symbols.getInfinity().length())) {
position += symbols.getInfinity().length();
status[STATUS_INFINITE] = true;
@ -2467,6 +2495,8 @@ public class DecimalFormat extends NumberFormat {
// We have to track digitCount ourselves, because digits.count will
// pin when the maximum allowable digits is reached.
int digitCount = 0;
int prevSeparatorIndex = -groupingSize;
int startPos = position; // Rely on startPos as index after prefix
int backup = -1;
for (; position < text.length(); ++position) {
@ -2488,6 +2518,13 @@ public class DecimalFormat extends NumberFormat {
digit = Character.digit(ch, 10);
}
// Enforce the grouping size on the first group
if (parseStrict && isGroupingUsed() && position == startPos + groupingSize
&& prevSeparatorIndex == -groupingSize && !sawDecimal
&& digit >= 0 && digit <= 9) {
return position;
}
if (digit == 0) {
// Cancel out backup setting (see grouping handler below)
backup = -1; // Do this BEFORE continue statement below!!!
@ -2517,6 +2554,10 @@ public class DecimalFormat extends NumberFormat {
// Cancel out backup setting (see grouping handler below)
backup = -1;
} else if (!isExponent && ch == decimal) {
// Check grouping size on decimal separator
if (parseStrict && isGroupingViolation(position, prevSeparatorIndex)) {
return groupingViolationIndex(position, prevSeparatorIndex);
}
// If we're only parsing integers, or if we ALREADY saw the
// decimal, then don't parse this one.
if (isParseIntegerOnly() || sawDecimal) {
@ -2525,8 +2566,23 @@ public class DecimalFormat extends NumberFormat {
digits.decimalAt = digitCount; // Not digits.count!
sawDecimal = true;
} else if (!isExponent && ch == grouping && isGroupingUsed()) {
if (sawDecimal) {
break;
if (parseStrict) {
// text should not start with grouping when strict
if (position == startPos) {
return startPos;
}
// when strict, fail if grouping occurs after decimal OR
// current group violates grouping size
if (sawDecimal || (isGroupingViolation(position, prevSeparatorIndex))) {
return groupingViolationIndex(position, prevSeparatorIndex);
}
prevSeparatorIndex = position; // track previous
} else {
// when lenient, only exit if grouping occurs after decimal
// subsequent grouping symbols are allowed when lenient
if (sawDecimal) {
break;
}
}
// Ignore grouping characters, if we are using them, but
// require that they be followed by a digit. Otherwise
@ -2554,6 +2610,23 @@ 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 (parseStrict) {
if (!sawDecimal && 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
// should be done at pos = 4
return groupingViolationIndex(position - 1, prevSeparatorIndex);
}
}
// If a grouping symbol is not followed by a digit, it must be
// backed up to either exit early or fail depending on leniency
if (backup != -1) {
position = backup;
}
@ -2575,7 +2648,30 @@ public class DecimalFormat extends NumberFormat {
}
}
return position;
}
// 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 &&
// The check itself, - 1 to account for grouping/decimal symbol
pos - 1 != prevGroupingPos + groupingSize;
}
// Calculates the index that violated the grouping size
// Violation can be over or under the grouping size
// under - Current group has a grouping size of less than the expected
// over - Current group has a grouping size of more than the expected
private int groupingViolationIndex(int pos, int prevGroupingPos) {
// Both examples assume grouping size of 3 and 0 indexed
// under ex: "1,23,4". (4) OR "1,,2". (2) When under, violating char is grouping symbol
// over ex: "1,2345,6. (5) When over, violating char is the excess digit
// This method is only evaluated when a grouping symbol is found, thus
// we can take the minimum of either the current pos, or where we expect
// the current group to have ended
return Math.min(pos, prevGroupingPos + groupingSize + 1);
}
/**
@ -2888,6 +2984,30 @@ public class DecimalFormat extends NumberFormat {
fastPathCheckNeeded = true;
}
/**
* {@inheritDoc NumberFormat}
*
* @see #setStrict(boolean)
* @see #parse(String, ParsePosition)
* @since 23
*/
@Override
public boolean isStrict() {
return parseStrict;
}
/**
* {@inheritDoc NumberFormat}
*
* @see #isStrict()
* @see #parse(String, ParsePosition)
* @since 23
*/
@Override
public void setStrict(boolean strict) {
parseStrict = strict;
}
/**
* Returns whether the {@link #parse(java.lang.String, java.text.ParsePosition)}
* method returns {@code BigDecimal}. The default value is false.
@ -2991,7 +3111,8 @@ public class DecimalFormat extends NumberFormat {
&& maximumFractionDigits == other.maximumFractionDigits
&& minimumFractionDigits == other.minimumFractionDigits
&& roundingMode == other.roundingMode
&& symbols.equals(other.symbols);
&& symbols.equals(other.symbols)
&& parseStrict == other.parseStrict;
}
/**
@ -4176,6 +4297,15 @@ public class DecimalFormat extends NumberFormat {
*/
private boolean useExponentialNotation; // Newly persistent in the Java 2 platform v.1.2
/**
* True if this {@code DecimalFormat} will parse numbers with strict
* leniency.
*
* @serial
* @since 23
*/
private boolean parseStrict = false;
/**
* FieldPositions describing the positive prefix String. This is
* lazily created. Use {@code getPositivePrefixFieldPositions}

View File

@ -106,6 +106,9 @@ import java.io.Serializable;
* </pre>
* </blockquote>
*
* <p> Subclasses may also consider implementing leniency when parsing.
* The definition of leniency should be delegated to the subclass.
*
* <p>
* And finally subclasses may define a set of constants to identify the various
* fields in the formatted output. These constants are used to create a FieldPosition
@ -210,37 +213,36 @@ public abstract class Format implements Serializable, Cloneable {
}
/**
* Parses text from a string to produce an object.
* Parses text from the given string to produce an object.
* <p>
* The method attempts to parse text starting at the index given by
* {@code pos}.
* If parsing succeeds, then the index of {@code pos} is updated
* This method attempts to parse text starting at the index given by
* {@code pos}. If parsing succeeds, then the index of {@code pos} is updated
* to the index after the last character used (parsing does not necessarily
* use all characters up to the end of the string), and the parsed
* object is returned. The updated {@code pos} can be used to
* indicate the starting point for the next call to this method.
* If an error occurs, then the index of {@code pos} is not
* changed, the error index of {@code pos} is set to the index of
* the character where the error occurred, and null is returned.
* the character where the error occurred, and {@code null} is returned.
*
* @param source A {@code String}, part of which should be parsed.
* @param source the {@code String} to parse
* @param pos A {@code ParsePosition} object with index and error
* index information as described above.
* @return An {@code Object} parsed from the string. In case of
* error, returns null.
* @throws NullPointerException if {@code source} or {@code pos} is null.
* error, returns {@code null}.
* @throws NullPointerException if {@code source} or {@code pos} is
* {@code null}.
*/
public abstract Object parseObject (String source, ParsePosition pos);
/**
* Parses text from the beginning of the given string to produce an object.
* The method may not use the entire text of the given string.
* This method may not use the entire text of the given string.
*
* @param source A {@code String} whose beginning should be parsed.
* @param source A {@code String}, to be parsed from the beginning.
* @return An {@code Object} parsed from the string.
* @throws ParseException if the beginning of the specified string
* cannot be parsed.
* @throws NullPointerException if {@code source} is null.
* @throws ParseException if parsing fails
* @throws NullPointerException if {@code source} is {@code null}.
*/
public Object parseObject(String source) throws ParseException {
ParsePosition pos = new ParsePosition(0);

View File

@ -38,8 +38,8 @@
package java.text;
import java.io.InvalidObjectException;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.math.BigInteger;
@ -52,6 +52,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import sun.util.locale.provider.LocaleProviderAdapter;
import sun.util.locale.provider.LocaleServiceProviderPool;
@ -147,7 +148,6 @@ import sun.util.locale.provider.LocaleServiceProviderPool;
* if false, 3456.00 &rarr; "3456"
* This is independent of parsing. If you want parsing to stop at the decimal
* point, use setParseIntegerOnly.
*
* <p>
* You can also use forms of the {@code parse} and {@code format}
* methods with {@code ParsePosition} and {@code FieldPosition} to
@ -175,9 +175,23 @@ import sun.util.locale.provider.LocaleServiceProviderPool;
* numbers: "(12)" for -12.
* </ol>
*
* <h2><a id="synchronization">Synchronization</a></h2>
*
* <h2><a id="leniency">Leniency</a></h2>
* {@code NumberFormat} by default, parses leniently. Subclasses may consider
* implementing strict parsing and as such, overriding and providing
* implementations for the optional {@link #isStrict()} and {@link
* #setStrict(boolean)} methods.
* <p>
* 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".
* <p>
* 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
* parsing the number {@code 1000.55} out of the String "1.000,55" confirms the String
* exactly adhered to the {@link Locale#GERMANY} numerical conventions.
*
* <h2><a id="synchronization">Synchronization</a></h2>
* Number formats are generally not synchronized.
* It is recommended to create separate format instances for each thread.
* If multiple threads access a format concurrently, it must be synchronized
@ -285,23 +299,11 @@ public abstract class NumberFormat extends Format {
}
/**
* Parses text from a string to produce a {@code Number}.
* <p>
* The method attempts to parse text starting at the index given by
* {@code pos}.
* If parsing succeeds, then the index of {@code pos} is updated
* to the index after the last character used (parsing does not necessarily
* use all characters up to the end of the string), and the parsed
* number is returned. The updated {@code pos} can be used to
* indicate the starting point for the next call to this method.
* If an error occurs, then the index of {@code pos} is not
* changed, the error index of {@code pos} is set to the index of
* the character where the error occurred, and null is returned.
* <p>
* See the {@link #parse(String, ParsePosition)} method for more information
* on number parsing.
* {@inheritDoc Format}
*
* @param source A {@code String}, part of which should be parsed.
* @implSpec This implementation is equivalent to calling {@code parse(source,
* pos)}.
* @param source the {@code String} to parse
* @param pos A {@code ParsePosition} object with index and error
* index information as described above.
* @return A {@code Number} parsed from the string. In case of
@ -399,33 +401,44 @@ public abstract class NumberFormat extends Format {
FieldPosition pos);
/**
* Returns a Long if possible (e.g., within the range [Long.MIN_VALUE,
* Parses text from the beginning of the given string to produce a {@code Number}.
* <p>
* This method attempts to parse text starting at the index given by the
* {@code ParsePosition}. If parsing succeeds, then the index of the {@code
* ParsePosition} is updated to the index after the last character used
* (parsing does not necessarily use all characters up to the end of the
* string), and the parsed number is returned. The updated {@code
* ParsePosition} can be used to indicate the starting
* point for the next call to this method. If an error occurs, then the
* index of the {@code ParsePosition} is not changed, the error index of the
* {@code ParsePosition} is set to the index of the character where the error
* occurred, and {@code null} is returned.
* <p>
* This method will return a Long if possible (e.g., within the range [Long.MIN_VALUE,
* Long.MAX_VALUE] and with no decimals), otherwise a Double.
* If IntegerOnly is set, will stop at a decimal
* point (or equivalent; e.g., for rational numbers "1 2/3", will stop
* after the 1).
* Does not throw an exception; if no object can be parsed, index is
* unchanged!
*
* @param source the String to parse
* @param parsePosition the parse position
* @return the parsed value
* @see java.text.NumberFormat#isParseIntegerOnly
* @see java.text.Format#parseObject
* @param source the {@code String} to parse
* @param parsePosition A {@code ParsePosition} object with index and error
* index information as described above.
* @return A {@code Number} parsed from the string. In case of
* failure, returns {@code null}.
* @throws NullPointerException if {@code source} or {@code ParsePosition}
* is {@code null}.
* @see #isStrict()
*/
public abstract Number parse(String source, ParsePosition parsePosition);
/**
* Parses text from the beginning of the given string to produce a number.
* The method may not use the entire text of the given string.
* Parses text from the beginning of the given string to produce a {@code Number}.
* <p>
* See the {@link #parse(String, ParsePosition)} method for more information
* on number parsing.
* This method will return a Long if possible (e.g., within the range [Long.MIN_VALUE,
* Long.MAX_VALUE] and with no decimals), otherwise a Double.
*
* @param source A {@code String} whose beginning should be parsed.
* @param source A {@code String}, to be parsed from the beginning.
* @return A {@code Number} parsed from the string.
* @throws ParseException if the beginning of the specified string
* cannot be parsed.
* @throws ParseException if parsing fails
* @throws NullPointerException if {@code source} is {@code null}.
* @see #isStrict()
*/
public Number parse(String source) throws ParseException {
ParsePosition parsePosition = new ParsePosition(0);
@ -463,6 +476,44 @@ public abstract class NumberFormat extends Format {
parseIntegerOnly = value;
}
/**
* {@return {@code true} if this format will parse numbers strictly;
* {@code false} otherwise}
*
* @implSpec The default implementation always throws {@code
* UnsupportedOperationException}. Subclasses should override this method
* when implementing strict parsing.
* @throws UnsupportedOperationException if the implementation of this
* method does not support this operation
* @see ##leniency Leniency Section
* @see #setStrict(boolean)
* @since 23
*/
public boolean isStrict() {
throw new UnsupportedOperationException("Subclasses should override this " +
"method when implementing strict parsing");
}
/**
* Change the leniency value for parsing. Parsing can either be strict or lenient,
* by default it is lenient.
*
* @implSpec The default implementation always throws {@code
* UnsupportedOperationException}. Subclasses should override this method
* when implementing strict parsing.
* @param strict {@code true} if parsing should be done strictly;
* {@code false} otherwise
* @throws UnsupportedOperationException if the implementation of this
* method does not support this operation
* @see ##leniency Leniency Section
* @see #isStrict()
* @since 23
*/
public void setStrict(boolean strict) {
throw new UnsupportedOperationException("Subclasses should override this " +
"method when implementing strict parsing");
}
//============== Locale Stuff =====================
/**
@ -759,12 +810,12 @@ public abstract class NumberFormat extends Format {
return false;
}
NumberFormat other = (NumberFormat) obj;
return (maximumIntegerDigits == other.maximumIntegerDigits
return maximumIntegerDigits == other.maximumIntegerDigits
&& minimumIntegerDigits == other.minimumIntegerDigits
&& maximumFractionDigits == other.maximumFractionDigits
&& minimumFractionDigits == other.minimumFractionDigits
&& groupingUsed == other.groupingUsed
&& parseIntegerOnly == other.parseIntegerOnly);
&& parseIntegerOnly == other.parseIntegerOnly;
}
/**

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018, 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
@ -22,7 +22,7 @@
*/
/*
* @test
* @bug 8177552 8222756
* @bug 8177552 8222756 8327640
* @summary Checks the equals and hashCode method of CompactNumberFormat
* @modules jdk.localedata
* @run testng/othervm TestEquality
@ -131,6 +131,18 @@ public class TestEquality {
cnf1.setParseBigDecimal(true);
checkEquals(cnf1, cnf2, false, "8th", "different parse big decimal");
// Changing the parseBigDecimal of second object; objects must be equal
cnf2.setParseBigDecimal(true);
checkEquals(cnf1, cnf2, true, "9th", "");
// Changing the strict parsing value of first object; objects must not be equal
cnf1.setStrict(true);
checkEquals(cnf1, cnf2, false, "10th", "different strict parsing");
// Changing the strict parsing value of second object; objects must be equal
cnf2.setStrict(true);
checkEquals(cnf1, cnf2, true, "11th", "");
}
private void checkEquals(CompactNumberFormat cnf1, CompactNumberFormat cnf2,

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018, 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
@ -22,7 +22,7 @@
*/
/*
* @test
* @bug 8177552
* @bug 8177552 8327640
* @modules jdk.localedata
* @summary Checks the serialization feature of CompactNumberFormat
* @run testng/othervm TestSerialization
@ -71,6 +71,7 @@ public class TestSerialization {
FORMAT_FR_FR.setParseIntegerOnly(true);
FORMAT_FR_FR.setGroupingUsed(true);
FORMAT_FR_FR.setStrict(true);
// Setting minimum integer digits beyond the allowed range
FORMAT_DE_DE.setMinimumIntegerDigits(320);

View File

@ -0,0 +1,53 @@
/*
* 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 8327640
* @summary Check parseStrict correctness for DecimalFormat.equals()
* @run junit EqualityTest
*/
import org.junit.jupiter.api.Test;
import java.text.DecimalFormat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
public class EqualityTest {
private static final DecimalFormat fmt1 = new DecimalFormat();
private static final DecimalFormat fmt2 = new DecimalFormat();
// Ensure that parseStrict is reflected correctly for DecimalFormat.equals()
@Test
public void checkStrictTest() {
// parseStrict is false by default
assertEquals(fmt1, fmt2);
fmt1.setStrict(true);
assertNotEquals(fmt1, fmt2);
fmt2.setStrict(true);
assertEquals(fmt1, fmt2);
}
}

View File

@ -0,0 +1,86 @@
/*
* 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 8327640
* @summary Check parseStrict correctness for DecimalFormat serialization
* @run junit/othervm SerializationTest
*/
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.text.NumberFormat;
import java.text.ParseException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class SerializationTest {
private static final NumberFormat FORMAT = NumberFormat.getInstance();
@BeforeAll
public static void mutateFormat() {
FORMAT.setStrict(true);
}
@Test
public void testSerialization() throws IOException, ClassNotFoundException {
// Serialize
serialize("fmt.ser", FORMAT);
// Deserialize
deserialize("fmt.ser", FORMAT);
}
private void serialize(String fileName, NumberFormat... formats)
throws IOException {
try (ObjectOutputStream os = new ObjectOutputStream(
new FileOutputStream(fileName))) {
for (NumberFormat fmt : formats) {
os.writeObject(fmt);
}
}
}
private static void deserialize(String fileName, NumberFormat... formats)
throws IOException, ClassNotFoundException {
try (ObjectInputStream os = new ObjectInputStream(
new FileInputStream(fileName))) {
for (NumberFormat fmt : formats) {
NumberFormat obj = (NumberFormat) os.readObject();
assertEquals(fmt, obj, "Serialized and deserialized"
+ " objects do not match");
String badNumber = "fooofooo23foo";
assertThrows(ParseException.class, () -> fmt.parse(badNumber));
assertThrows(ParseException.class, () -> obj.parse(badNumber));
}
}
}
}

View File

@ -0,0 +1,417 @@
/*
* 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 8327640
* @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
* @run junit/othervm -Duser.language=zh -Duser.country=CN LenientParseTest
* @run junit/othervm -Duser.language=tr -Duser.country=TR LenientParseTest
* @run junit/othervm -Duser.language=de -Duser.country=DE LenientParseTest
* @run junit/othervm -Duser.language=fr -Duser.country=FR LenientParseTest
* @run junit/othervm -Duser.language=ar -Duser.country=AR LenientParseTest
*/
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.text.CompactNumberFormat;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Locale;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
// Tests lenient parsing, this is done by testing the NumberFormat factory instances
// against a number of locales with different formatting conventions. The locales
// used all use a grouping size of 3. When lenient, parsing only fails
// if the prefix and/or suffix are not found, or the first character after the
// prefix is un-parseable. The tested locales all use groupingSize of 3.
public class LenientParseTest {
// Used to retrieve the locale's expected symbols
private static final DecimalFormatSymbols dfs =
new DecimalFormatSymbols(Locale.getDefault());
private static final DecimalFormat dFmt = (DecimalFormat)
NumberFormat.getNumberInstance(Locale.getDefault());
private static final DecimalFormat cFmt =
(DecimalFormat) NumberFormat.getCurrencyInstance(Locale.getDefault());
private static final DecimalFormat pFmt =
(DecimalFormat) NumberFormat.getPercentInstance(Locale.getDefault());
private static final CompactNumberFormat cmpctFmt =
(CompactNumberFormat) NumberFormat.getCompactNumberInstance(Locale.getDefault(),
NumberFormat.Style.SHORT);
// All NumberFormats should parse leniently (which is the default)
static {
// To effectively test compactNumberFormat, these should be set accordingly
cmpctFmt.setParseIntegerOnly(false);
cmpctFmt.setGroupingUsed(true);
}
// ---- NumberFormat tests ----
// Test prefix/suffix behavior with a predefined DecimalFormat
// Non-localized, only run once
@ParameterizedTest
@MethodSource("badParseStrings")
@EnabledIfSystemProperty(named = "user.language", matches = "en")
public void numFmtFailParseTest(String toParse, int expectedErrorIndex) {
// Format with grouping size = 3, prefix = a, suffix = b
DecimalFormat nonLocalizedDFmt = new DecimalFormat("a#,#00.00b");
failParse(nonLocalizedDFmt, toParse, expectedErrorIndex);
}
// All input Strings should parse fully and return the expected value.
// Expected index should be the length of the parse string, since it parses fully
@ParameterizedTest
@MethodSource("validFullParseStrings")
public void numFmtSuccessFullParseTest(String toParse, double expectedValue) {
assertEquals(expectedValue, successParse(dFmt, toParse, toParse.length()));
}
// All input Strings should parse partially and return expected value
// with the expected final index
@ParameterizedTest
@MethodSource("validPartialParseStrings")
public void numFmtSuccessPartialParseTest(String toParse, double expectedValue,
int expectedIndex) {
assertEquals(expectedValue, successParse(dFmt, toParse, expectedIndex));
}
// Parse partially due to no grouping
@ParameterizedTest
@MethodSource("noGroupingParseStrings")
public void numFmtStrictGroupingNotUsed(String toParse, double expectedValue, int expectedIndex) {
dFmt.setGroupingUsed(false);
assertEquals(expectedValue, successParse(dFmt, toParse, expectedIndex));
dFmt.setGroupingUsed(true);
}
// Parse partially due to integer only
@ParameterizedTest
@MethodSource("integerOnlyParseStrings")
public void numFmtStrictIntegerOnlyUsed(String toParse, int expectedValue, int expectedIndex) {
dFmt.setParseIntegerOnly(true);
assertEquals(expectedValue, successParse(dFmt, toParse, expectedIndex));
dFmt.setParseIntegerOnly(false);
}
// ---- CurrencyFormat tests ----
// All input Strings should pass and return expected value.
@ParameterizedTest
@MethodSource("currencyValidFullParseStrings")
public void currFmtSuccessParseTest(String toParse, double expectedValue) {
assertEquals(expectedValue, successParse(cFmt, toParse, toParse.length()));
}
// Strings may parse partially or fail. This is because the mapped
// data may cause the error to occur before the suffix can be found, (if the locale
// uses a suffix).
@ParameterizedTest
@MethodSource("currencyValidPartialParseStrings")
public void currFmtParseTest(String toParse, double expectedValue,
int expectedIndex) {
if (cFmt.getPositiveSuffix().length() > 0) {
// Since the error will occur before suffix is found, exception is thrown.
failParse(cFmt, toParse, expectedIndex);
} else {
// Empty suffix, thus even if the error occurs, we have already found the
// prefix, and simply parse partially
assertEquals(expectedValue, successParse(cFmt, toParse, expectedIndex));
}
}
// ---- PercentFormat tests ----
// All input Strings should pass and return expected value.
@ParameterizedTest
@MethodSource("percentValidFullParseStrings")
public void percentFmtSuccessParseTest(String toParse, double expectedValue) {
assertEquals(expectedValue, successParse(pFmt, toParse, toParse.length()));
}
// Strings may parse partially or fail. This is because the mapped
// data may cause the error to occur before the suffix can be found, (if the locale
// uses a suffix).
@ParameterizedTest
@MethodSource("percentValidPartialParseStrings")
public void percentFmtParseTest(String toParse, double expectedValue,
int expectedIndex) {
if (pFmt.getPositiveSuffix().length() > 0) {
// Since the error will occur before suffix is found, exception is thrown.
failParse(pFmt, toParse, expectedIndex);
} else {
// Empty suffix, thus even if the error occurs, we have already found the
// prefix, and simply parse partially
assertEquals(expectedValue, successParse(pFmt, toParse, expectedIndex));
}
}
// ---- CompactNumberFormat tests ----
// Can match to both the decimalFormat patterns and the compact patterns
// Unlike the other tests, this test is only ran against the US Locale and
// tests against data built with the thousands format (K).
@ParameterizedTest
@MethodSource("compactValidPartialParseStrings")
@EnabledIfSystemProperty(named = "user.language", matches = "en")
public void compactFmtFailParseTest(String toParse, double expectedValue, int expectedErrorIndex) {
assertEquals(expectedValue, successParse(cmpctFmt, toParse, expectedErrorIndex));
}
@ParameterizedTest
@MethodSource("compactValidFullParseStrings")
@EnabledIfSystemProperty(named = "user.language", matches = "en")
public void compactFmtSuccessParseTest(String toParse, double expectedValue) {
assertEquals(expectedValue, successParse(cmpctFmt, toParse, toParse.length()));
}
// ---- Helper test methods ----
// Method is used when a String should parse successfully. This does not indicate
// that the entire String was used, however. The index and errorIndex values
// should be as expected.
private double successParse(NumberFormat fmt, String toParse, int expectedIndex) {
Number parsedValue = assertDoesNotThrow(() -> fmt.parse(toParse));
ParsePosition pp = new ParsePosition(0);
assertDoesNotThrow(() -> fmt.parse(toParse, pp));
assertEquals(-1, pp.getErrorIndex(),
"ParsePosition ErrorIndex is not in correct location");
assertEquals(expectedIndex, pp.getIndex(),
"ParsePosition Index is not in correct location");
return parsedValue.doubleValue();
}
// Method is used when a String should fail parsing. Indicated by either a thrown
// ParseException, or null is returned depending on which parse method is invoked.
// errorIndex should be as expected.
private void failParse(NumberFormat fmt, String toParse, int expectedErrorIndex) {
ParsePosition pp = new ParsePosition(0);
assertThrows(ParseException.class, () -> fmt.parse(toParse));
assertNull(fmt.parse(toParse, pp));
assertEquals(expectedErrorIndex, pp.getErrorIndex());
}
// ---- Data Providers ----
// Strings that should fail when parsed leniently.
// Given as Arguments<String, expectedErrorIndex>
// Non-localized data. For reference, the pattern of nonLocalizedDFmt is
// "a#,#00.00b"
private static Stream<Arguments> badParseStrings() {
return Stream.of(
// No prefix
Arguments.of("1,1b", 0),
// No suffix
Arguments.of("a1,11", 5),
// Digit does not follow the last grouping separator
// Current behavior fails on the grouping separator
Arguments.of("a1,11,z", 5),
// No suffix after grouping
Arguments.of("a1,11,", 5),
// No prefix and suffix
Arguments.of("1,11", 0),
// First character after prefix is un-parseable
// Behavior is to expect error index at 0, not 1
Arguments.of("ac1,11", 0));
}
// These data providers use US locale grouping and decimal separators
// for readability, however, the data is tested against multiple locales
// and is converted appropriately at runtime.
// Strings that should parse successfully, and consume the entire String
// Form of Arguments(parseString, expectedParsedNumber)
private static Stream<Arguments> validFullParseStrings() {
return Stream.of(
// Many subsequent grouping symbols
Arguments.of("1,,,1", 11d),
Arguments.of("11,,,11,,,11", 111111d),
// Bad grouping size (with decimal)
Arguments.of("1,1.", 11d),
Arguments.of("11,111,11.", 1111111d),
// Improper grouping size (with decimal and digits after)
Arguments.of("1,1.1", 11.1d),
Arguments.of("1,11.1", 111.1d),
Arguments.of("1,1111.1", 11111.1d),
Arguments.of("11,111,11.1", 1111111.1d),
// Starts with grouping symbol
Arguments.of(",111,,1,1", 11111d),
Arguments.of(",1", 1d),
Arguments.of(",,1", 1d),
// Leading Zeros (not digits)
Arguments.of("000,1,1", 11d),
Arguments.of("000,111,11,,1", 111111d),
Arguments.of("0,000,1,,1,1", 111d),
Arguments.of("1,234.00", 1234d),
Arguments.of("1,234.0", 1234d),
Arguments.of("1,234.", 1234d),
Arguments.of("1,234.00123", 1234.00123d),
Arguments.of("1,234.012", 1234.012d),
Arguments.of("1,234.224", 1234.224d),
Arguments.of("1", 1d),
Arguments.of("10", 10d),
Arguments.of("100", 100d),
Arguments.of("1000", 1000d),
Arguments.of("1,000", 1000d),
Arguments.of("10,000", 10000d),
Arguments.of("10000", 10000d),
Arguments.of("100,000", 100000d),
Arguments.of("1,000,000", 1000000d),
Arguments.of("10,000,000", 10000000d))
.map(args -> Arguments.of(
localizeText(String.valueOf(args.get()[0])), args.get()[1]));
}
// Strings that should parse successfully, but do not use the entire String
// Form of Arguments(parseString, expectedParsedNumber, expectedIndex)
private static Stream<Arguments> validPartialParseStrings() {
return Stream.of(
// End with grouping symbol
Arguments.of("11,", 11d, 2),
Arguments.of("11,,", 11d, 3),
Arguments.of("11,,,", 11d, 4),
// Random chars that aren't the expected symbols
Arguments.of("1,1P111", 11d, 3),
Arguments.of("1.1P111", 1.1d, 3),
Arguments.of("1P,1111", 1d, 1),
Arguments.of("1P.1111", 1d, 1),
Arguments.of("1,1111P", 11111d, 6),
// Grouping occurs after decimal separator)
Arguments.of("1.11,11", 1.11d, 4),
Arguments.of("1.,11,11", 1d, 2))
.map(args -> Arguments.of(
localizeText(String.valueOf(args.get()[0])), args.get()[1], args.get()[2]));
}
// Test data input for when parse integer only is true
// Form of Arguments(parseString, expectedParsedNumber, expectedIndex)
private static Stream<Arguments> integerOnlyParseStrings() {
return Stream.of(
Arguments.of("1234.1234", 1234, 4),
Arguments.of("1234.12", 1234, 4),
Arguments.of("1234.1a", 1234, 4),
Arguments.of("1234.", 1234, 4))
.map(args -> Arguments.of(
localizeText(String.valueOf(args.get()[0])), args.get()[1], args.get()[2]));
}
// Test data input for when no grouping is true
// Form of Arguments(parseString, expectedParsedNumber, expectedIndex)
private static Stream<Arguments> noGroupingParseStrings() {
return Stream.of(
Arguments.of("12,34", 12d, 2),
Arguments.of("1234,", 1234d, 4),
Arguments.of("123,456.789", 123d, 3))
.map(args -> Arguments.of(
localizeText(String.valueOf(args.get()[0])), args.get()[1], args.get()[2]));
}
// Mappers for respective data providers to adjust values accordingly
// Localized percent prefix/suffix is added, with appropriate expected values
// adjusted. Expected parsed number should be divided by 100.
private static Stream<Arguments> percentValidPartialParseStrings() {
return validPartialParseStrings().map(args ->
Arguments.of(pFmt.getPositivePrefix() + args.get()[0] + pFmt.getPositiveSuffix(),
(double) args.get()[1] / 100, (int) args.get()[2] + pFmt.getPositivePrefix().length())
);
}
private static Stream<Arguments> percentValidFullParseStrings() {
return validFullParseStrings().map(args -> Arguments.of(
pFmt.getPositivePrefix() + args.get()[0] + pFmt.getPositiveSuffix(),
(double) args.get()[1] / 100)
);
}
// Mappers for respective data providers to adjust values accordingly
// Localized percent prefix/suffix is added, with appropriate expected values
// adjusted. Separators replaced for monetary versions.
private static Stream<Arguments> currencyValidPartialParseStrings() {
return validPartialParseStrings().map(args -> Arguments.of(
cFmt.getPositivePrefix() + String.valueOf(args.get()[0])
.replace(dfs.getGroupingSeparator(), dfs.getMonetaryGroupingSeparator())
.replace(dfs.getDecimalSeparator(), dfs.getMonetaryDecimalSeparator())
+ cFmt.getPositiveSuffix(),
args.get()[1], (int) args.get()[2] + cFmt.getPositivePrefix().length())
);
}
private static Stream<Arguments> currencyValidFullParseStrings() {
return validFullParseStrings().map(args -> Arguments.of(
cFmt.getPositivePrefix() + String.valueOf(args.get()[0])
.replace(dfs.getGroupingSeparator(), dfs.getMonetaryGroupingSeparator())
.replace(dfs.getDecimalSeparator(), dfs.getMonetaryDecimalSeparator())
+ cFmt.getPositiveSuffix(),
args.get()[1])
);
}
// Compact Pattern Data Provider provides test input for both DecimalFormat patterns
// and the compact patterns. As there is no method to retrieve compact patterns,
// thus test only against US English locale, and use a hard coded K - 1000
private static Stream<Arguments> compactValidPartialParseStrings() {
return Stream.concat(validPartialParseStrings().map(args -> Arguments.of(args.get()[0],
args.get()[1], args.get()[2])), validPartialParseStrings().map(args -> Arguments.of(args.get()[0] + "K",
args.get()[1], args.get()[2]))
);
}
private static Stream<Arguments> compactValidFullParseStrings() {
return Stream.concat(validFullParseStrings().map(args -> Arguments.of(args.get()[0],
args.get()[1])), validFullParseStrings().map(args -> Arguments.of(args.get()[0] + "K",
(double)args.get()[1] * 1000.0))
);
}
// Replace the grouping and decimal separators with localized variants
// Used during localization of data
private static String localizeText(String text) {
// As this is a single pass conversion, this is safe for multiple replacement,
// even if a ',' could be a decimal separator for a locale.
StringBuilder sb = new StringBuilder();
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c == ',') {
sb.append(dfs.getGroupingSeparator());
} else if (c == '.') {
sb.append(dfs.getDecimalSeparator());
} else if (c == '0') {
sb.append(dfs.getZeroDigit());
} else {
sb.append(c);
}
}
return sb.toString();
}
}

View File

@ -0,0 +1,94 @@
/*
* 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 8327640
* @summary Unit test for the isStrict() and setStrict() parsing related methods
* @run junit StrictMethodsTest
*/
import org.junit.jupiter.api.Test;
import java.text.CompactNumberFormat;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.text.NumberFormat;
import java.text.ParsePosition;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class StrictMethodsTest {
// Check that DecimalFormat implements isStrict()/setStrict()
// Ensure that the default value is false, and can be set to true via API
@Test
public void decimalFormatTest() {
DecimalFormat dFmt = (DecimalFormat) NumberFormat.getInstance();
assertFalse(dFmt.isStrict());
dFmt.setStrict(true);
assertTrue(dFmt.isStrict());
}
// Check that CompactNumberFormat implements isStrict()/setStrict()
// Ensure that the default value is false, and can be set to true via API
@Test
public void compactFormatTest() {
CompactNumberFormat cFmt = (CompactNumberFormat) NumberFormat.getCompactNumberInstance();
assertFalse(cFmt.isStrict());
cFmt.setStrict(true);
assertTrue(cFmt.isStrict());
}
// Check that NumberFormat throws exception for isStrict()/setStrict()
// when subclass does not implement said methods
@Test
public void numberFormatTest() {
FooFormat fmt = new FooFormat();
assertThrows(UnsupportedOperationException.class, fmt::isStrict);
assertThrows(UnsupportedOperationException.class, () -> fmt.setStrict(false));
}
// Dummy NumberFormat class to check that isStrict() and setStrict()
// are not implemented by default
private static class FooFormat extends NumberFormat {
// Provide overrides for abstract methods
@Override
public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
return null;
}
@Override
public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) {
return null;
}
@Override
public Number parse(String source, ParsePosition parsePosition) {
return null;
}
}
}

View File

@ -0,0 +1,525 @@
/*
* 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 8327640
* @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
* @run junit/othervm -Duser.language=zh -Duser.country=CN StrictParseTest
* @run junit/othervm -Duser.language=tr -Duser.country=TR StrictParseTest
* @run junit/othervm -Duser.language=de -Duser.country=DE StrictParseTest
* @run junit/othervm -Duser.language=fr -Duser.country=FR StrictParseTest
* @run junit/othervm -Duser.language=ar -Duser.country=AR StrictParseTest
*/
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.text.CompactNumberFormat;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Locale;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
// Tests strict parsing, this is done by testing the NumberFormat factory instances
// against a number of locales with different formatting conventions. The locales
// used all use a grouping size of 3.
public class StrictParseTest {
// Used to retrieve the locale's expected symbols
private static final DecimalFormatSymbols dfs =
new DecimalFormatSymbols(Locale.getDefault());
// We re-use these formats for the respective factory tests
private static final DecimalFormat dFmt =
(DecimalFormat) NumberFormat.getNumberInstance(Locale.getDefault());
private static final DecimalFormat cFmt =
(DecimalFormat) NumberFormat.getCurrencyInstance(Locale.getDefault());
private static final DecimalFormat pFmt =
(DecimalFormat) NumberFormat.getPercentInstance(Locale.getDefault());
private static final CompactNumberFormat cmpctFmt =
(CompactNumberFormat) NumberFormat.getCompactNumberInstance(Locale.getDefault(),
NumberFormat.Style.SHORT);
// All NumberFormats should parse strictly
static {
dFmt.setStrict(true);
pFmt.setStrict(true);
cFmt.setStrict(true);
cmpctFmt.setStrict(true);
// To effectively test strict compactNumberFormat parsing
cmpctFmt.setParseIntegerOnly(false);
cmpctFmt.setGroupingUsed(true);
cmpctFmt.setGroupingSize(3);
}
// ---- NumberFormat tests ----
// Guarantee some edge case test input
@Test // Non-localized, run once
@EnabledIfSystemProperty(named = "user.language", matches = "en")
public void uniqueCaseNumberFormatTest() {
// Format with grouping size = 3, prefix = a, suffix = b
DecimalFormat nonLocalizedDFmt = new DecimalFormat("a#,#00.00b");
nonLocalizedDFmt.setStrict(true);
// Text after suffix
failParse(nonLocalizedDFmt, "a12bfoo", 3);
failParse(nonLocalizedDFmt, "a123,456.00bc", 11);
// Text after prefix
failParse(nonLocalizedDFmt, "ac123", 0);
// Missing suffix
failParse(nonLocalizedDFmt, "a123", 4);
// Prefix contains a decimal separator
failParse(nonLocalizedDFmt, ".a123", 0);
// Test non grouping size of 3
nonLocalizedDFmt.setGroupingSize(1);
successParse(nonLocalizedDFmt, "a1,2,3,4b");
failParse(nonLocalizedDFmt, "a1,2,3,45,6b", 8);
nonLocalizedDFmt.setGroupingSize(5);
successParse(nonLocalizedDFmt, "a12345,67890b");
successParse(nonLocalizedDFmt, "a1234,67890b");
failParse(nonLocalizedDFmt, "a123456,7890b", 6);
}
// All input Strings should fail
@ParameterizedTest
@MethodSource("badParseStrings")
public void numFmtFailParseTest(String toParse, int expectedErrorIndex) {
failParse(dFmt, toParse, expectedErrorIndex);
}
// All input Strings should pass and return expected value.
@ParameterizedTest
@MethodSource("validParseStrings")
public void numFmtSuccessParseTest(String toParse, double expectedValue) {
assertEquals(expectedValue, successParse(dFmt, toParse));
}
// All input Strings should fail
@ParameterizedTest
@MethodSource("negativeBadParseStrings")
public void negNumFmtFailParseTest(String toParse, int expectedErrorIndex) {
failParse(dFmt, toParse, expectedErrorIndex);
}
// All input Strings should pass and return expected value.
@ParameterizedTest
@MethodSource("negativeValidParseStrings")
public void negNumFmtSuccessParseTest(String toParse, double expectedValue) {
assertEquals(expectedValue, successParse(dFmt, toParse));
}
// Exception should be thrown if grouping separator occurs anywhere
// Don't pass badParseStrings as a data source, since they may fail for other reasons
@ParameterizedTest
@MethodSource({"validParseStrings", "noGroupingParseStrings"})
public void numFmtStrictGroupingNotUsed(String toParse) {
// When grouping is not used, if a grouping separator is found,
// a failure should occur
dFmt.setGroupingUsed(false);
int failIndex = toParse.indexOf(
dFmt.getDecimalFormatSymbols().getGroupingSeparator());
if (failIndex > -1) {
failParse(dFmt, toParse, failIndex);
} else {
successParse(dFmt, toParse);
}
dFmt.setGroupingUsed(true);
}
// Exception should be thrown if decimal separator occurs anywhere
// Don't pass badParseStrings for same reason as previous method.
@ParameterizedTest
@MethodSource({"validParseStrings", "integerOnlyParseStrings"})
public void numFmtStrictIntegerOnlyUsed(String toParse) {
// When integer only is true, if a decimal separator is found,
// a failure should occur
dFmt.setParseIntegerOnly(true);
int failIndex = toParse.indexOf(dfs.getDecimalSeparator());
if (failIndex > -1) {
failParse(dFmt, toParse, failIndex);
} else {
successParse(dFmt, toParse);
}
dFmt.setParseIntegerOnly(false);
}
// ---- CurrencyFormat tests ----
@ParameterizedTest
@MethodSource("currencyBadParseStrings")
public void currFmtFailParseTest(String toParse, int expectedErrorIndex) {
failParse(cFmt, toParse, expectedErrorIndex);
}
@ParameterizedTest
@MethodSource("currencyValidParseStrings")
public void currFmtSuccessParseTest(String toParse, double expectedValue) {
assertEquals(expectedValue, successParse(cFmt, toParse));
}
// ---- PercentFormat tests ----
@ParameterizedTest
@MethodSource("percentBadParseStrings")
public void percentFmtFailParseTest(String toParse, int expectedErrorIndex) {
failParse(pFmt, toParse, expectedErrorIndex);
}
@ParameterizedTest
@MethodSource("percentValidParseStrings")
public void percentFmtSuccessParseTest(String toParse, double expectedValue) {
assertEquals(expectedValue, successParse(pFmt, toParse));
}
// ---- CompactNumberFormat tests ----
// Can match to both the decimalFormat patterns and the compact patterns
// Thus we test leniency for both. Unlike the other tests, this test
// is only ran against the US Locale and tests against data built with the
// thousands format (K).
@ParameterizedTest
@MethodSource("compactBadParseStrings")
@EnabledIfSystemProperty(named = "user.language", matches = "en")
public void compactFmtFailParseTest(String toParse, int expectedErrorIndex) {
failParse(cmpctFmt, toParse, expectedErrorIndex);
}
@ParameterizedTest
@MethodSource("compactValidParseStrings")
@EnabledIfSystemProperty(named = "user.language", matches = "en")
public void compactFmtSuccessParseTest(String toParse, double expectedValue) {
assertEquals(expectedValue, successParse(cmpctFmt, toParse));
}
// Checks some odd leniency edge cases between matching of default pattern
// and compact pattern.
@Test // Non-localized, run once
@EnabledIfSystemProperty(named = "user.language", matches = "en")
public void compactFmtEdgeParseTest() {
// Uses a compact format with unique and non-empty prefix/suffix for both
// default and compact patterns
CompactNumberFormat cnf = new CompactNumberFormat("a##0.0#b", DecimalFormatSymbols
.getInstance(Locale.US), new String[]{"", "c0d"});
cnf.setStrict(true);
// Existing behavior of failed prefix parsing has errorIndex return
// the beginning of prefix, even if the error occurred later in the prefix.
// Prefix empty
failParse(cnf, "12345d", 0);
failParse(cnf, "1b", 0);
// Prefix bad
failParse(cnf, "aa1d", 0);
failParse(cnf, "cc1d", 0);
failParse(cnf, "aa1b", 0);
failParse(cnf, "cc1b", 0);
// Suffix error index is always the start of the failed suffix
// not necessarily where the error occurred in the suffix. This is
// consistent with the prefix error index behavior.
// Suffix empty
failParse(cnf, "a1", 2);
failParse(cnf, "c1", 2);
// Suffix bad
failParse(cnf, "a1dd", 2);
failParse(cnf, "c1dd", 2);
failParse(cnf, "a1bb", 2);
failParse(cnf, "c1bb", 2);
}
// Ensure that on failure, the original index of the PP remains the same
@Test
public void parsePositionIndexTest() {
failParse(dFmt, localizeText("123,456,,789.00"), 8, 4);
}
// ---- Helper test methods ----
// Should parse entire String successfully, and return correctly parsed value.
private double successParse(NumberFormat fmt, String toParse) {
// For Strings that don't have grouping separators, we test them with
// grouping off so that they do not fail under the expectation that
// grouping symbols should occur
if (!toParse.contains(String.valueOf(dfs.getGroupingSeparator())) &&
!toParse.contains(String.valueOf(dfs.getMonetaryGroupingSeparator()))) {
fmt.setGroupingUsed(false);
}
Number parsedValue = assertDoesNotThrow(() -> fmt.parse(toParse));
ParsePosition pp = new ParsePosition(0);
assertDoesNotThrow(() -> fmt.parse(toParse, pp));
assertEquals(-1, pp.getErrorIndex(),
"ParsePosition ErrorIndex is not in correct location");
assertEquals(toParse.length(), pp.getIndex(),
"ParsePosition Index is not in correct location");
fmt.setGroupingUsed(true);
return parsedValue.doubleValue();
}
// Method which tests a parsing failure. Either a ParseException is thrown,
// or null is returned depending on which parse method is invoked. When failing,
// index should remain the initial index set to the ParsePosition while
// errorIndex is the index of failure.
private void failParse(NumberFormat fmt, String toParse, int expectedErrorIndex) {
failParse(fmt, toParse, expectedErrorIndex, 0);
}
// Variant to check non 0 initial parse index
private void failParse(NumberFormat fmt, String toParse,
int expectedErrorIndex, int initialParseIndex) {
ParsePosition pp = new ParsePosition(initialParseIndex);
assertThrows(ParseException.class, () -> fmt.parse(toParse));
assertNull(fmt.parse(toParse, pp));
assertEquals(expectedErrorIndex, pp.getErrorIndex());
assertEquals(initialParseIndex, pp.getIndex());
}
// ---- Data Providers ----
// These data providers use US locale grouping and decimal separators
// for readability, however, the data is tested against multiple locales
// and is converted appropriately at runtime.
// Strings that should fail when parsed with strict leniency.
// Given as Arguments<String, expectedErrorIndex>
private static Stream<Arguments> badParseStrings() {
return Stream.of(
// Grouping symbol focus
// Grouping symbol right before decimal
Arguments.of("1,.", 2),
Arguments.of("1,.1", 2),
// Does not end with proper grouping size
Arguments.of("1,1", 2),
Arguments.of("1,11", 3),
Arguments.of("1,1111", 5),
Arguments.of("11,111,11", 8),
// Does not end with proper grouping size (with decimal)
Arguments.of("1,1.", 3),
Arguments.of("1,11.", 4),
Arguments.of("1,1111.", 5),
Arguments.of("11,111,11.", 9),
// Ends on a grouping symbol
// Suffix matches correctly, so failure is on the ","
Arguments.of("11,111,", 6),
Arguments.of("11,", 2),
Arguments.of("11,,", 3),
// Ends with grouping symbol. Failure should occur on grouping,
// even if non recognized char after
Arguments.of("11,a", 2),
// Improper grouping size (with decimal and digits after)
Arguments.of("1,1.1", 3),
Arguments.of("1,11.1", 4),
Arguments.of("1,1111.1", 5),
Arguments.of("11,111,11.1", 9),
// Subsequent grouping symbols
Arguments.of("1,,1", 2),
Arguments.of("1,1,,1", 3),
Arguments.of("1,,1,1", 2),
// Invalid grouping sizes
Arguments.of("1,11,111", 4),
Arguments.of("11,11,111", 5),
Arguments.of("111,11,11", 6),
// First group is too large
Arguments.of("1111,11,111", 3),
Arguments.of("00000,11,111", 3),
Arguments.of("111,1111111111", 7),
Arguments.of("111,11", 5),
Arguments.of("111,1111111111.", 7),
Arguments.of("111,11.", 6),
Arguments.of("111,1111111111.", 7),
// Starts with grouping symbol
Arguments.of(",111,,1,1", 0),
Arguments.of(",1", 0),
Arguments.of(",,1", 0),
// Leading Zeros (not digits)
Arguments.of("000,1,1", 5),
Arguments.of("000,111,11,,1", 10),
Arguments.of("0,000,1,,1,1", 7),
// Bad suffix
Arguments.of("1a", 1),
// Bad chars in numerical portion
Arguments.of("123a4", 3),
Arguments.of("123.4a5", 5),
// Variety of edge cases
Arguments.of("123,456.77a", 10),
Arguments.of("1,234a", 5),
Arguments.of("1,.a", 2),
Arguments.of("1.a", 2),
Arguments.of(".22a", 3),
Arguments.of(".1a1", 2),
Arguments.of("1,234,a", 5))
.map(args -> Arguments.of(
localizeText(String.valueOf(args.get()[0])), args.get()[1]));
}
// Strings that should parse fully. (Both in lenient and strict)
// Given as Arguments<String, expectedParsedNumber>
private static Stream<Arguments> validParseStrings() {
return Stream.of(
Arguments.of("1,234.00", 1234d),
Arguments.of("1,234.0", 1234d),
Arguments.of("1,234.", 1234d),
Arguments.of("1", 1d),
Arguments.of("10", 10d),
Arguments.of("100", 100d),
Arguments.of("1000", 1000d),
Arguments.of("1,000", 1000d),
Arguments.of("10,000", 10000d),
Arguments.of("10000", 10000d),
Arguments.of("100,000", 100000d),
Arguments.of("1,000,000", 1000000d),
Arguments.of("10,000,000", 10000000d))
.map(args -> Arguments.of(
localizeText(String.valueOf(args.get()[0])), args.get()[1]));
}
// Separate test data set for integer only. Can not use "badParseStrings", as
// there is test data where the failure may occur from some other issue,
// not related to grouping
private static Stream<Arguments> integerOnlyParseStrings() {
return Stream.of(
Arguments.of("234.a"),
Arguments.of("234.a1"),
Arguments.of("234.1"),
Arguments.of("234.1a"),
Arguments.of("234."))
.map(args -> Arguments.of(localizeText(String.valueOf(args.get()[0]))));
}
// Separate test data set for no grouping. Can not use "badParseStrings", as
// there is test data where the failure may occur from some other issue,
// not related to grouping
private static Stream<Arguments> noGroupingParseStrings() {
return Stream.of(
Arguments.of("12,34.a"),
Arguments.of("123,.a1"),
Arguments.of(",1234"),
Arguments.of("123,"))
.map(args -> Arguments.of(localizeText(String.valueOf(args.get()[0]))));
}
// Negative variant of a numerical format
private static Stream<Arguments> negativeBadParseStrings() {
return badParseStrings().map(args -> Arguments.of(
dFmt.getNegativePrefix() + args.get()[0] + dFmt.getNegativeSuffix(),
(int)args.get()[1] + dFmt.getNegativePrefix().length())
);
}
// Negative variant of a numerical format
private static Stream<Arguments> negativeValidParseStrings() {
return validParseStrings().map(args -> Arguments.of(
dFmt.getNegativePrefix() + args.get()[0] + dFmt.getNegativeSuffix(),
(double) args.get()[1] * -1)
);
}
// Same as original with a percent prefix/suffix.
// Additionally, increment expected error index if a prefix is added
private static Stream<Arguments> percentBadParseStrings() {
return badParseStrings().map(args -> Arguments.of(
pFmt.getPositivePrefix() + args.get()[0] + pFmt.getPositiveSuffix(),
(int)args.get()[1] + pFmt.getPositivePrefix().length())
);
}
// Expected parsed value should be / 100 as it is a percent format.
private static Stream<Arguments> percentValidParseStrings() {
return validParseStrings().map(args -> Arguments.of(
pFmt.getPositivePrefix() + args.get()[0] + pFmt.getPositiveSuffix(),
(double)args.get()[1] / 100.0)
);
}
// Same as original with a currency prefix/suffix, but replace separators
// with monetary variants. Additionally, increment expected error index
// if a prefix is added
private static Stream<Arguments> currencyBadParseStrings() {
return badParseStrings().map(args -> Arguments.of(
cFmt.getPositivePrefix() + String.valueOf(args.get()[0])
.replace(dfs.getGroupingSeparator(), dfs.getMonetaryGroupingSeparator())
.replace(dfs.getDecimalSeparator(), dfs.getMonetaryDecimalSeparator())
+ cFmt.getPositiveSuffix(),
(int)args.get()[1] + cFmt.getPositivePrefix().length())
);
}
private static Stream<Arguments> currencyValidParseStrings() {
return validParseStrings().map(args -> Arguments.of(
cFmt.getPositivePrefix() + String.valueOf(args.get()[0])
.replace(dfs.getGroupingSeparator(), dfs.getMonetaryGroupingSeparator())
.replace(dfs.getDecimalSeparator(), dfs.getMonetaryDecimalSeparator())
+ cFmt.getPositiveSuffix(),
args.get()[1])
);
}
// Compact Pattern Data Provider provides test input for both DecimalFormat patterns
// and the compact patterns. As there is no method to retrieve compact patterns,
// thus test only against US English locale, and use a hard coded K - 1000
private static Stream<Arguments> compactBadParseStrings() {
return Stream.concat(
badParseStrings().map(args -> Arguments.of(args.get()[0], args.get()[1])),
badParseStrings().map(args -> Arguments.of(args.get()[0] + "K", args.get()[1]))
);
}
private static Stream<Arguments> compactValidParseStrings() {
return Stream.concat(
validParseStrings().map(args -> Arguments.of(
args.get()[0], args.get()[1])),
validParseStrings().map(args -> Arguments.of(
args.get()[0] + "K", (double) args.get()[1] * 1000))
);
}
// Replace the grouping and decimal separators with localized variants
// Used during localization of data
private static String localizeText(String text) {
// As this is a single pass conversion, this is safe for multiple replacement,
// even if a ',' could be a decimal separator for a locale.
StringBuilder sb = new StringBuilder();
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c == ',') {
sb.append(dfs.getGroupingSeparator());
} else if (c == '.') {
sb.append(dfs.getDecimalSeparator());
} else if (c == '0') {
sb.append(dfs.getZeroDigit());
} else {
sb.append(c);
}
}
return sb.toString();
}
}