mirror of
https://github.com/openjdk/jdk.git
synced 2025-08-27 23:04:50 +02:00
8222756: Plural support in CompactNumberFormat
Reviewed-by: joehw, rriggs
This commit is contained in:
parent
3000f212f2
commit
730d0ecf19
18 changed files with 1101 additions and 130 deletions
|
@ -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 \uFFFE, \uFFFF, and
|
||||
* <a href = "DecimalFormat.html#special_pattern_character">special characters</a>
|
||||
* <i>Suffix:</i>
|
||||
* Any Unicode characters except \uFFFE, \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 \uFFFE, \uFFFF, and
|
||||
* <a href = "DecimalFormat.html#special_pattern_character">special characters</a>.
|
||||
* <i>Suffix:</i>
|
||||
* Any Unicode characters except \uFFFE, \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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue