8222756: Plural support in CompactNumberFormat

Reviewed-by: joehw, rriggs
This commit is contained in:
Naoto Sato 2019-12-05 13:10:18 -08:00
parent 3000f212f2
commit 730d0ecf19
18 changed files with 1101 additions and 130 deletions

View file

@ -32,11 +32,17 @@ import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
@ -108,27 +114,8 @@ import java.util.concurrent.atomic.AtomicLong;
* A special pattern {@code "0"} is used for any range which does not contain
* a compact pattern. This special pattern can appear explicitly for any specific
* range, or considered as a default pattern for an empty string.
* <p>
* A compact pattern has the following syntax:
* <blockquote><pre>
* <i>Pattern:</i>
* <i>PositivePattern</i>
* <i>PositivePattern</i> <i>[; NegativePattern]<sub>optional</sub></i>
* <i>PositivePattern:</i>
* <i>Prefix<sub>optional</sub></i> <i>MinimumInteger</i> <i>Suffix<sub>optional</sub></i>
* <i>NegativePattern:</i>
* <i>Prefix<sub>optional</sub></i> <i>MinimumInteger</i> <i>Suffix<sub>optional</sub></i>
* <i>Prefix:</i>
* Any Unicode characters except &#92;uFFFE, &#92;uFFFF, and
* <a href = "DecimalFormat.html#special_pattern_character">special characters</a>
* <i>Suffix:</i>
* Any Unicode characters except &#92;uFFFE, &#92;uFFFF, and
* <a href = "DecimalFormat.html#special_pattern_character">special characters</a>
* <i>MinimumInteger:</i>
* 0
* 0 <i>MinimumInteger</i>
* </pre></blockquote>
*
* <p>
* A compact pattern contains a positive and negative subpattern
* separated by a subpattern boundary character {@code ';' (U+003B)},
* for example, {@code "0K;-0K"}. Each subpattern has a prefix,
@ -151,6 +138,48 @@ import java.util.concurrent.atomic.AtomicLong;
* unless noted otherwise, if they are to appear in the prefix or suffix
* as literals. For example, 0\u0915'.'.
*
* <h3>Plurals</h3>
* <p>
* In case some localization requires compact number patterns to be different for
* plurals, each singular and plural pattern can be enumerated within a pair of
* curly brackets <code>'{' (U+007B)</code> and <code>'}' (U+007D)</code>, separated
* by a space {@code ' ' (U+0020)}. If this format is used, each pattern needs to be
* prepended by its {@code count}, followed by a single colon {@code ':' (U+003A)}.
* If the pattern includes spaces literally, they must be quoted.
* <p>
* For example, the compact number pattern representing millions in German locale can be
* specified as {@code "{one:0' 'Million other:0' 'Millionen}"}. The {@code count}
* follows LDML's
* <a href="https://unicode.org/reports/tr35/tr35-numbers.html#Language_Plural_Rules">
* Language Plural Rules</a>.
* <p>
* A compact pattern has the following syntax:
* <blockquote><pre>
* <i>Pattern:</i>
* <i>SimplePattern</i>
* '{' <i>PluralPattern</i> <i>[' ' PluralPattern]<sub>optional</sub></i> '}'
* <i>SimplePattern:</i>
* <i>PositivePattern</i>
* <i>PositivePattern</i> <i>[; NegativePattern]<sub>optional</sub></i>
* <i>PluralPattern:</i>
* <i>Count</i>:<i>SimplePattern</i>
* <i>Count:</i>
* "zero" / "one" / "two" / "few" / "many" / "other"
* <i>PositivePattern:</i>
* <i>Prefix<sub>optional</sub></i> <i>MinimumInteger</i> <i>Suffix<sub>optional</sub></i>
* <i>NegativePattern:</i>
* <i>Prefix<sub>optional</sub></i> <i>MinimumInteger</i> <i>Suffix<sub>optional</sub></i>
* <i>Prefix:</i>
* Any Unicode characters except &#92;uFFFE, &#92;uFFFF, and
* <a href = "DecimalFormat.html#special_pattern_character">special characters</a>.
* <i>Suffix:</i>
* Any Unicode characters except &#92;uFFFE, &#92;uFFFF, and
* <a href = "DecimalFormat.html#special_pattern_character">special characters</a>.
* <i>MinimumInteger:</i>
* 0
* 0 <i>MinimumInteger</i>
* </pre></blockquote>
*
* <h2>Formatting</h2>
* The default formatting behavior returns a formatted string with no fractional
* digits, however users can use the {@link #setMinimumFractionDigits(int)}
@ -207,25 +236,25 @@ public final class CompactNumberFormat extends NumberFormat {
* List of positive prefix patterns of this formatter's
* compact number patterns.
*/
private transient List<String> positivePrefixPatterns;
private transient List<Patterns> positivePrefixPatterns;
/**
* List of negative prefix patterns of this formatter's
* compact number patterns.
*/
private transient List<String> negativePrefixPatterns;
private transient List<Patterns> negativePrefixPatterns;
/**
* List of positive suffix patterns of this formatter's
* compact number patterns.
*/
private transient List<String> positiveSuffixPatterns;
private transient List<Patterns> positiveSuffixPatterns;
/**
* List of negative suffix patterns of this formatter's
* compact number patterns.
*/
private transient List<String> negativeSuffixPatterns;
private transient List<Patterns> negativeSuffixPatterns;
/**
* List of divisors of this formatter's compact number patterns.
@ -298,6 +327,26 @@ public final class CompactNumberFormat extends NumberFormat {
*/
private RoundingMode roundingMode = RoundingMode.HALF_EVEN;
/**
* The {@code pluralRules} used in this compact number format.
* {@code pluralRules} is a String designating plural rules which associate
* the {@code Count} keyword, such as "{@code one}", and the
* actual integer number. Its syntax is defined in Unicode Consortium's
* <a href = "http://unicode.org/reports/tr35/tr35-numbers.html#Plural_rules_syntax">
* Plural rules syntax</a>.
* The default value is an empty string, meaning there is no plural rules.
*
* @serial
* @since 14
*/
private String pluralRules = "";
/**
* The map for plural rules that maps LDML defined tags (e.g. "one") to
* its rule.
*/
private transient Map<String, String> rulesMap;
/**
* Special pattern used for compact numbers
*/
@ -328,20 +377,56 @@ public final class CompactNumberFormat extends NumberFormat {
* <a href = "CompactNumberFormat.html#compact_number_patterns">
* compact number patterns</a>
* @throws NullPointerException if any of the given arguments is
* {@code null}
* {@code null}
* @throws IllegalArgumentException if the given {@code decimalPattern} or the
* {@code compactPatterns} array contains an invalid pattern
* or if a {@code null} appears in the array of compact
* patterns
* {@code compactPatterns} array contains an invalid pattern
* or if a {@code null} appears in the array of compact
* patterns
* @see DecimalFormat#DecimalFormat(java.lang.String, DecimalFormatSymbols)
* @see DecimalFormatSymbols
*/
public CompactNumberFormat(String decimalPattern,
DecimalFormatSymbols symbols, String[] compactPatterns) {
DecimalFormatSymbols symbols, String[] compactPatterns) {
this(decimalPattern, symbols, compactPatterns, "");
}
/**
* Creates a {@code CompactNumberFormat} using the given decimal pattern,
* decimal format symbols, compact patterns, and plural rules.
* To obtain the instance of {@code CompactNumberFormat} with the standard
* compact patterns for a {@code Locale}, {@code Style}, and {@code pluralRules},
* it is recommended to use the factory methods given by
* {@code NumberFormat} for compact number formatting. For example,
* {@link NumberFormat#getCompactNumberInstance(Locale, Style)}.
*
* @param decimalPattern a decimal pattern for general number formatting
* @param symbols the set of symbols to be used
* @param compactPatterns an array of
* <a href = "CompactNumberFormat.html#compact_number_patterns">
* compact number patterns</a>
* @param pluralRules a String designating plural rules which associate
* the {@code Count} keyword, such as "{@code one}", and the
* actual integer number. Its syntax is defined in Unicode Consortium's
* <a href = "http://unicode.org/reports/tr35/tr35-numbers.html#Plural_rules_syntax">
* Plural rules syntax</a>
* @throws NullPointerException if any of the given arguments is
* {@code null}
* @throws IllegalArgumentException if the given {@code decimalPattern},
* the {@code compactPatterns} array contains an invalid pattern,
* a {@code null} appears in the array of compact patterns,
* or if the given {@code pluralRules} contains an invalid syntax
* @see DecimalFormat#DecimalFormat(java.lang.String, DecimalFormatSymbols)
* @see DecimalFormatSymbols
* @since 14
*/
public CompactNumberFormat(String decimalPattern,
DecimalFormatSymbols symbols, String[] compactPatterns,
String pluralRules) {
Objects.requireNonNull(decimalPattern, "decimalPattern");
Objects.requireNonNull(symbols, "symbols");
Objects.requireNonNull(compactPatterns, "compactPatterns");
Objects.requireNonNull(pluralRules, "pluralRules");
this.symbols = symbols;
// Instantiating the DecimalFormat with "0" pattern; this acts just as a
@ -371,6 +456,9 @@ public final class CompactNumberFormat extends NumberFormat {
defaultDecimalFormat = new DecimalFormat(this.decimalPattern,
this.symbols);
defaultDecimalFormat.setMaximumFractionDigits(0);
this.pluralRules = pluralRules;
// Process compact patterns to extract the prefixes, suffixes and
// divisors
processCompactPatterns();
@ -494,14 +582,13 @@ public final class CompactNumberFormat extends NumberFormat {
double roundedNumber = dList.getDouble();
int compactDataIndex = selectCompactPattern((long) roundedNumber);
if (compactDataIndex != -1) {
String prefix = isNegative ? negativePrefixPatterns.get(compactDataIndex)
: positivePrefixPatterns.get(compactDataIndex);
String suffix = isNegative ? negativeSuffixPatterns.get(compactDataIndex)
: positiveSuffixPatterns.get(compactDataIndex);
long divisor = (Long) divisors.get(compactDataIndex);
int iPart = getIntegerPart(number, divisor);
String prefix = getAffix(false, true, isNegative, compactDataIndex, iPart);
String suffix = getAffix(false, false, isNegative, compactDataIndex, iPart);
if (!prefix.isEmpty() || !suffix.isEmpty()) {
appendPrefix(result, prefix, delegate);
long divisor = (Long) divisors.get(compactDataIndex);
roundedNumber = roundedNumber / divisor;
decimalFormat.setDigitList(roundedNumber, isNegative, getMaximumFractionDigits());
decimalFormat.subformatNumber(result, delegate, isNegative,
@ -562,13 +649,12 @@ public final class CompactNumberFormat extends NumberFormat {
int compactDataIndex = selectCompactPattern(number);
if (compactDataIndex != -1) {
String prefix = isNegative ? negativePrefixPatterns.get(compactDataIndex)
: positivePrefixPatterns.get(compactDataIndex);
String suffix = isNegative ? negativeSuffixPatterns.get(compactDataIndex)
: positiveSuffixPatterns.get(compactDataIndex);
long divisor = (Long) divisors.get(compactDataIndex);
int iPart = getIntegerPart(number, divisor);
String prefix = getAffix(false, true, isNegative, compactDataIndex, iPart);
String suffix = getAffix(false, false, isNegative, compactDataIndex, iPart);
if (!prefix.isEmpty() || !suffix.isEmpty()) {
appendPrefix(result, prefix, delegate);
long divisor = (Long) divisors.get(compactDataIndex);
if ((number % divisor == 0)) {
number = number / divisor;
decimalFormat.setDigitList(number, isNegative, 0);
@ -649,19 +735,19 @@ public final class CompactNumberFormat extends NumberFormat {
int compactDataIndex;
if (number.toBigInteger().bitLength() < 64) {
compactDataIndex = selectCompactPattern(number.toBigInteger().longValue());
long longNumber = number.toBigInteger().longValue();
compactDataIndex = selectCompactPattern(longNumber);
} else {
compactDataIndex = selectCompactPattern(number.toBigInteger());
}
if (compactDataIndex != -1) {
String prefix = isNegative ? negativePrefixPatterns.get(compactDataIndex)
: positivePrefixPatterns.get(compactDataIndex);
String suffix = isNegative ? negativeSuffixPatterns.get(compactDataIndex)
: positiveSuffixPatterns.get(compactDataIndex);
Number divisor = divisors.get(compactDataIndex);
int iPart = getIntegerPart(number.doubleValue(), divisor.doubleValue());
String prefix = getAffix(false, true, isNegative, compactDataIndex, iPart);
String suffix = getAffix(false, false, isNegative, compactDataIndex, iPart);
if (!prefix.isEmpty() || !suffix.isEmpty()) {
appendPrefix(result, prefix, delegate);
Number divisor = divisors.get(compactDataIndex);
number = number.divide(new BigDecimal(divisor.toString()), getRoundingMode());
decimalFormat.setDigitList(number, isNegative, getMaximumFractionDigits());
decimalFormat.subformatNumber(result, delegate, isNegative,
@ -721,13 +807,12 @@ public final class CompactNumberFormat extends NumberFormat {
int compactDataIndex = selectCompactPattern(number);
if (compactDataIndex != -1) {
String prefix = isNegative ? negativePrefixPatterns.get(compactDataIndex)
: positivePrefixPatterns.get(compactDataIndex);
String suffix = isNegative ? negativeSuffixPatterns.get(compactDataIndex)
: positiveSuffixPatterns.get(compactDataIndex);
Number divisor = divisors.get(compactDataIndex);
int iPart = getIntegerPart(number.doubleValue(), divisor.doubleValue());
String prefix = getAffix(false, true, isNegative, compactDataIndex, iPart);
String suffix = getAffix(false, false, isNegative, compactDataIndex, iPart);
if (!prefix.isEmpty() || !suffix.isEmpty()) {
appendPrefix(result, prefix, delegate);
Number divisor = divisors.get(compactDataIndex);
if (number.mod(new BigInteger(divisor.toString()))
.compareTo(BigInteger.ZERO) == 0) {
number = number.divide(new BigInteger(divisor.toString()));
@ -761,6 +846,18 @@ public final class CompactNumberFormat extends NumberFormat {
return result;
}
/**
* Obtain the designated affix from the appropriate list of affixes,
* based on the given arguments.
*/
private String getAffix(boolean isExpanded, boolean isPrefix, boolean isNegative, int compactDataIndex, int iPart) {
return (isExpanded ? (isPrefix ? (isNegative ? negativePrefixes : positivePrefixes) :
(isNegative ? negativeSuffixes : positiveSuffixes)) :
(isPrefix ? (isNegative ? negativePrefixPatterns : positivePrefixPatterns) :
(isNegative ? negativeSuffixPatterns : positiveSuffixPatterns)))
.get(compactDataIndex).get(iPart);
}
/**
* Appends the {@code prefix} to the {@code result} and also set the
* {@code NumberFormat.Field.SIGN} and {@code NumberFormat.Field.PREFIX}
@ -1042,6 +1139,10 @@ public final class CompactNumberFormat extends NumberFormat {
* value.
*
*/
private static final Pattern PLURALS =
Pattern.compile("^\\{(?<plurals>.*)\\}$");
private static final Pattern COUNT_PATTERN =
Pattern.compile("(zero|one|two|few|many|other):((' '|[^ ])+)[ ]*");
private void processCompactPatterns() {
int size = compactPatterns.length;
positivePrefixPatterns = new ArrayList<>(size);
@ -1051,8 +1152,80 @@ public final class CompactNumberFormat extends NumberFormat {
divisors = new ArrayList<>(size);
for (int index = 0; index < size; index++) {
applyPattern(compactPatterns[index], index);
String text = compactPatterns[index];
positivePrefixPatterns.add(new Patterns());
negativePrefixPatterns.add(new Patterns());
positiveSuffixPatterns.add(new Patterns());
negativeSuffixPatterns.add(new Patterns());
// check if it is the old style
Matcher m = text != null ? PLURALS.matcher(text) : null;
if (m != null && m.matches()) {
final int idx = index;
String plurals = m.group("plurals");
COUNT_PATTERN.matcher(plurals).results()
.forEach(mr -> applyPattern(mr.group(1), mr.group(2), idx));
} else {
applyPattern("other", text, index);
}
}
rulesMap = buildPluralRulesMap();
}
/**
* Build the plural rules map.
*
* @throws IllegalArgumentException if the {@code pluralRules} has invalid syntax,
* or its length exceeds 2,048 chars
*/
private Map<String, String> buildPluralRulesMap() {
// length limitation check. 2K for now.
if (pluralRules.length() > 2_048) {
throw new IllegalArgumentException("plural rules is too long (> 2,048)");
}
try {
return Arrays.stream(pluralRules.split(";"))
.map(this::validateRule)
.collect(Collectors.toMap(
r -> r.replaceFirst(":.*", ""),
r -> r.replaceFirst("[^:]+:", "")
));
} catch (IllegalStateException ise) {
throw new IllegalArgumentException(ise);
}
}
// Patterns for plurals syntax validation
private final static String EXPR = "([niftvw]{1})\\s*(([/\\%])\\s*(\\d+))*";
private final static String RELATION = "(!{0,1}=)";
private final static String VALUE_RANGE = "((\\d+)\\.\\.(\\d+)|\\d+)";
private final static String CONDITION = EXPR + "\\s*" +
RELATION + "\\s*" +
VALUE_RANGE + "\\s*" +
"(\\,\\s*" + VALUE_RANGE + ")*";
private final static Pattern PLURALRULES_PATTERN =
Pattern.compile("(zero|one|two|few|many):\\s*" +
CONDITION +
"(\\s*(and|or)\\s*" + CONDITION + ")*");
/**
* Validates a plural rule.
* @param rule rule to validate
* @throws IllegalArgumentException if the {@code rule} has invalid syntax
* @return the input rule (trimmed)
*/
private String validateRule(String rule) {
rule = rule.trim();
if (!rule.isEmpty() && !rule.equals("other:")) {
Matcher validator = PLURALRULES_PATTERN.matcher(rule);
if (!validator.matches()) {
throw new IllegalArgumentException("Invalid plural rules syntax: " + rule);
}
}
return rule;
}
/**
@ -1061,7 +1234,7 @@ public final class CompactNumberFormat extends NumberFormat {
* @param index index in the array of compact patterns
*
*/
private void applyPattern(String pattern, int index) {
private void applyPattern(String count, String pattern, int index) {
if (pattern == null) {
throw new IllegalArgumentException("A null compact pattern" +
@ -1236,17 +1409,21 @@ public final class CompactNumberFormat extends NumberFormat {
// Only if positive affix exists; else put empty strings
if (!positivePrefix.isEmpty() || !positiveSuffix.isEmpty()) {
positivePrefixPatterns.add(positivePrefix);
negativePrefixPatterns.add(negativePrefix);
positiveSuffixPatterns.add(positiveSuffix);
negativeSuffixPatterns.add(negativeSuffix);
divisors.add(computeDivisor(zeros, index));
positivePrefixPatterns.get(index).put(count, positivePrefix);
negativePrefixPatterns.get(index).put(count, negativePrefix);
positiveSuffixPatterns.get(index).put(count, positiveSuffix);
negativeSuffixPatterns.get(index).put(count, negativeSuffix);
if (divisors.size() <= index) {
divisors.add(computeDivisor(zeros, index));
}
} else {
positivePrefixPatterns.add("");
negativePrefixPatterns.add("");
positiveSuffixPatterns.add("");
negativeSuffixPatterns.add("");
divisors.add(1L);
positivePrefixPatterns.get(index).put(count, "");
negativePrefixPatterns.get(index).put(count, "");
positiveSuffixPatterns.get(index).put(count, "");
negativeSuffixPatterns.get(index).put(count, "");
if (divisors.size() <= index) {
divisors.add(1L);
}
}
}
@ -1270,10 +1447,10 @@ public final class CompactNumberFormat extends NumberFormat {
// the expanded form contains special characters in
// its localized form, which are used for matching
// while parsing a string to number
private transient List<String> positivePrefixes;
private transient List<String> negativePrefixes;
private transient List<String> positiveSuffixes;
private transient List<String> negativeSuffixes;
private transient List<Patterns> positivePrefixes;
private transient List<Patterns> negativePrefixes;
private transient List<Patterns> positiveSuffixes;
private transient List<Patterns> negativeSuffixes;
private void expandAffixPatterns() {
positivePrefixes = new ArrayList<>(compactPatterns.length);
@ -1281,10 +1458,10 @@ public final class CompactNumberFormat extends NumberFormat {
positiveSuffixes = new ArrayList<>(compactPatterns.length);
negativeSuffixes = new ArrayList<>(compactPatterns.length);
for (int index = 0; index < compactPatterns.length; index++) {
positivePrefixes.add(expandAffix(positivePrefixPatterns.get(index)));
negativePrefixes.add(expandAffix(negativePrefixPatterns.get(index)));
positiveSuffixes.add(expandAffix(positiveSuffixPatterns.get(index)));
negativeSuffixes.add(expandAffix(negativeSuffixPatterns.get(index)));
positivePrefixes.add(positivePrefixPatterns.get(index).expandAffix());
negativePrefixes.add(negativePrefixPatterns.get(index).expandAffix());
positiveSuffixes.add(positiveSuffixPatterns.get(index).expandAffix());
negativeSuffixes.add(negativeSuffixPatterns.get(index).expandAffix());
}
}
@ -1382,10 +1559,12 @@ public final class CompactNumberFormat extends NumberFormat {
String matchedNegPrefix = "";
String defaultPosPrefix = defaultDecimalFormat.getPositivePrefix();
String defaultNegPrefix = defaultDecimalFormat.getNegativePrefix();
double num = parseNumberPart(text, position);
// Prefix matching
for (int compactIndex = 0; compactIndex < compactPatterns.length; compactIndex++) {
String positivePrefix = positivePrefixes.get(compactIndex);
String negativePrefix = negativePrefixes.get(compactIndex);
String positivePrefix = getAffix(true, true, false, compactIndex, (int)num);
String negativePrefix = getAffix(true, true, true, compactIndex, (int)num);
// Do not break if a match occur; there is a possibility that the
// subsequent affixes may match the longer subsequence in the given
@ -1487,7 +1666,7 @@ public final class CompactNumberFormat extends NumberFormat {
pos.index = position;
Number multiplier = computeParseMultiplier(text, pos,
gotPositive ? matchedPosPrefix : matchedNegPrefix,
status, gotPositive, gotNegative);
status, gotPositive, gotNegative, num);
if (multiplier.longValue() == -1L) {
return null;
@ -1529,6 +1708,33 @@ public final class CompactNumberFormat extends NumberFormat {
}
}
/**
* Parse the number part in the input text into a number
*
* @param text input text to be parsed
* @param position starting position
* @return the number
*/
private static Pattern DIGITS = Pattern.compile("\\p{Nd}+");
private double parseNumberPart(String text, int position) {
if (text.startsWith(symbols.getInfinity(), position)) {
return Double.POSITIVE_INFINITY;
} else if (!text.startsWith(symbols.getNaN(), position)) {
Matcher m = DIGITS.matcher(text);
if (m.find(position)) {
String digits = m.group();
int cp = digits.codePointAt(0);
if (Character.isDigit(cp)) {
return Double.parseDouble(digits.codePoints()
.map(Character::getNumericValue)
.mapToObj(Integer::toString)
.collect(Collectors.joining()));
}
}
}
return Double.NaN;
}
/**
* Returns the parsed result by multiplying the parsed number
* with the multiplier representing the prefix and suffix.
@ -1664,7 +1870,7 @@ public final class CompactNumberFormat extends NumberFormat {
*/
private Number computeParseMultiplier(String text, ParsePosition parsePosition,
String matchedPrefix, boolean[] status, boolean gotPositive,
boolean gotNegative) {
boolean gotNegative, double num) {
int position = parsePosition.index;
boolean gotPos = false;
@ -1674,10 +1880,10 @@ public final class CompactNumberFormat extends NumberFormat {
String matchedPosSuffix = "";
String matchedNegSuffix = "";
for (int compactIndex = 0; compactIndex < compactPatterns.length; compactIndex++) {
String positivePrefix = positivePrefixes.get(compactIndex);
String negativePrefix = negativePrefixes.get(compactIndex);
String positiveSuffix = positiveSuffixes.get(compactIndex);
String negativeSuffix = negativeSuffixes.get(compactIndex);
String positivePrefix = getAffix(true, true, false, compactIndex, (int)num);
String negativePrefix = getAffix(true, true, true, compactIndex, (int)num);
String positiveSuffix = getAffix(true, false, false, compactIndex, (int)num);
String negativeSuffix = getAffix(true, false, true, compactIndex, (int)num);
// Do not break if a match occur; there is a possibility that the
// subsequent affixes may match the longer subsequence in the given
@ -1779,6 +1985,8 @@ public final class CompactNumberFormat extends NumberFormat {
* if the minimum or maximum fraction digit count is larger than 340.
* <li> If the grouping size is negative or larger than 127.
* </ul>
* If the {@code pluralRules} field is not deserialized from the stream, it
* will be set to an empty string.
*
* @param inStream the stream
* @throws IOException if an I/O error occurs
@ -1810,6 +2018,11 @@ public final class CompactNumberFormat extends NumberFormat {
throw new InvalidObjectException("Grouping size is negative");
}
// pluralRules is since 14. Fill in empty string if it is null
if (pluralRules == null) {
pluralRules = "";
}
try {
processCompactPatterns();
} catch (IllegalArgumentException ex) {
@ -2111,6 +2324,7 @@ public final class CompactNumberFormat extends NumberFormat {
&& symbols.equals(other.symbols)
&& Arrays.equals(compactPatterns, other.compactPatterns)
&& roundingMode.equals(other.roundingMode)
&& pluralRules.equals(other.pluralRules)
&& groupingSize == other.groupingSize
&& parseBigDecimal == other.parseBigDecimal;
}
@ -2123,7 +2337,7 @@ public final class CompactNumberFormat extends NumberFormat {
@Override
public int hashCode() {
return 31 * super.hashCode() +
Objects.hash(decimalPattern, symbols, roundingMode)
Objects.hash(decimalPattern, symbols, roundingMode, pluralRules)
+ Arrays.hashCode(compactPatterns) + groupingSize
+ Boolean.hashCode(parseBigDecimal);
}
@ -2142,4 +2356,155 @@ public final class CompactNumberFormat extends NumberFormat {
return other;
}
/**
* Abstraction of affix patterns for each "count" tag.
*/
private final class Patterns {
private Map<String, String> patternsMap = new HashMap<>();
void put(String count, String pattern) {
patternsMap.put(count, pattern);
}
String get(double num) {
return patternsMap.getOrDefault(getPluralCategory(num),
patternsMap.getOrDefault("other", ""));
}
Patterns expandAffix() {
Patterns ret = new Patterns();
patternsMap.entrySet().stream()
.forEach(e -> ret.put(e.getKey(), CompactNumberFormat.this.expandAffix(e.getValue())));
return ret;
}
}
private final int getIntegerPart(double number, double divisor) {
return BigDecimal.valueOf(number)
.divide(BigDecimal.valueOf(divisor), roundingMode).intValue();
}
/**
* Returns LDML's tag from the plurals rules
*
* @param input input number in double type
* @return LDML "count" tag
*/
private String getPluralCategory(double input) {
if (rulesMap != null) {
return rulesMap.entrySet().stream()
.filter(e -> matchPluralRule(e.getValue(), input))
.map(e -> e.getKey())
.findFirst()
.orElse("other");
}
// defaults to "other"
return "other";
}
private static boolean matchPluralRule(String condition, double input) {
return Arrays.stream(condition.split("or"))
.anyMatch(and_condition -> {
return Arrays.stream(and_condition.split("and"))
.allMatch(r -> relationCheck(r, input));
});
}
private final static String NAMED_EXPR = "(?<op>[niftvw]{1})\\s*((?<div>[/\\%])\\s*(?<val>\\d+))*";
private final static String NAMED_RELATION = "(?<rel>!{0,1}=)";
private final static String NAMED_VALUE_RANGE = "(?<start>\\d+)\\.\\.(?<end>\\d+)|(?<value>\\d+)";
private final static Pattern EXPR_PATTERN = Pattern.compile(NAMED_EXPR);
private final static Pattern RELATION_PATTERN = Pattern.compile(NAMED_RELATION);
private final static Pattern VALUE_RANGE_PATTERN = Pattern.compile(NAMED_VALUE_RANGE);
/**
* Checks if the 'input' equals the value, or within the range.
*
* @param valueOrRange A string representing either a single value or a range
* @param input to examine in double
* @return match indicator
*/
private static boolean valOrRangeMatches(String valueOrRange, double input) {
Matcher m = VALUE_RANGE_PATTERN.matcher(valueOrRange);
if (m.find()) {
String value = m.group("value");
if (value != null) {
return input == Double.parseDouble(value);
} else {
return input >= Double.parseDouble(m.group("start")) &&
input <= Double.parseDouble(m.group("end"));
}
}
return false;
}
/**
* Checks if the input value satisfies the relation. Each possible value or range is
* separated by a comma ','
*
* @param relation relation string, e.g, "n = 1, 3..5", or "n != 1, 3..5"
* @param input value to examine in double
* @return boolean to indicate whether the relation satisfies or not. If the relation
* is '=', true if any of the possible value/range satisfies. If the relation is '!=',
* none of the possible value/range should satisfy to return true.
*/
private static boolean relationCheck(String relation, double input) {
Matcher expr = EXPR_PATTERN.matcher(relation);
if (expr.find()) {
double lop = evalLOperand(expr, input);
Matcher rel = RELATION_PATTERN.matcher(relation);
if (rel.find(expr.end())) {
var conditions =
Arrays.stream(relation.substring(rel.end()).split(","));
if (rel.group("rel").equals("!=")) {
return conditions.noneMatch(c -> valOrRangeMatches(c, lop));
} else {
return conditions.anyMatch(c -> valOrRangeMatches(c, lop));
}
}
}
return false;
}
/**
* Evaluates the left operand value.
*
* @param expr Match result
* @param input value to examine in double
* @return resulting double value
*/
private static double evalLOperand(Matcher expr, double input) {
double ret = 0;
if (input == Double.POSITIVE_INFINITY) {
ret =input;
} else {
String op = expr.group("op");
if (op.equals("n") || op.equals("i")) {
ret = input;
}
String divop = expr.group("div");
if (divop != null) {
String divisor = expr.group("val");
switch (divop) {
case "%":
ret %= Double.parseDouble(divisor);
break;
case "/":
ret /= Double.parseDouble(divisor);
break;
}
}
}
return ret;
}
}

View file

@ -117,7 +117,8 @@ public abstract class NumberFormatProvider extends LocaleServiceProvider {
* {@code locale} and {@code formatStyle}.
*
* @implSpec The default implementation of this method throws
* {@code UnSupportedOperationException}. Overriding the implementation
* {@link java.lang.UnsupportedOperationException
* UnsupportedOperationException}. Overriding the implementation
* of this method returns the compact number formatter instance
* of the given {@code locale} with specified {@code formatStyle}.
*
@ -129,6 +130,8 @@ public abstract class NumberFormatProvider extends LocaleServiceProvider {
* one of the locales returned from
* {@link java.util.spi.LocaleServiceProvider#getAvailableLocales()
* getAvailableLocales()}.
* @throws UnsupportedOperationException if the implementation does not
* support this method
* @return a compact number formatter
*
* @see java.text.NumberFormat#getCompactNumberInstance(Locale,