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

@ -157,9 +157,9 @@ abstract class AbstractLDMLHandler<V> extends DefaultHandler {
}
}
void pushStringListElement(String qName, Attributes attributes, int index) {
void pushStringListElement(String qName, Attributes attributes, int index, String count) {
if (!pushIfIgnored(qName, attributes)) {
currentContainer = new StringListElement(qName, currentContainer, index);
currentContainer = new StringListElement(qName, currentContainer, index, count);
}
}

View File

@ -242,14 +242,14 @@ class Bundle {
if (i < size) {
pattern = patterns.get(i);
if (!pattern.isEmpty()) {
return pattern;
return "{" + pattern + "}";
}
}
// if not found, try parent
if (i < psize) {
pattern = pList.get(i);
if (!pattern.isEmpty()) {
return pattern;
return "{" + pattern + "}";
}
}
// bail out with empty string

View File

@ -70,6 +70,7 @@ public class CLDRConverter {
private static String LIKELYSUBTAGS_SOURCE_FILE;
private static String TIMEZONE_SOURCE_FILE;
private static String WINZONES_SOURCE_FILE;
private static String PLURALS_SOURCE_FILE;
static String DESTINATION_DIR = "build/gensrc";
static final String LOCALE_NAME_PREFIX = "locale.displayname.";
@ -93,6 +94,7 @@ public class CLDRConverter {
private static SupplementDataParseHandler handlerSuppl;
private static LikelySubtagsParseHandler handlerLikelySubtags;
private static WinZonesParseHandler handlerWinZones;
static PluralsParseHandler handlerPlurals;
static SupplementalMetadataParseHandler handlerSupplMeta;
static NumberingSystemsParseHandler handlerNumbering;
static MetaZonesParseHandler handlerMetaZones;
@ -244,6 +246,7 @@ public class CLDRConverter {
TIMEZONE_SOURCE_FILE = CLDR_BASE + "/bcp47/timezone.xml";
SPPL_META_SOURCE_FILE = CLDR_BASE + "/supplemental/supplementalMetadata.xml";
WINZONES_SOURCE_FILE = CLDR_BASE + "/supplemental/windowsZones.xml";
PLURALS_SOURCE_FILE = CLDR_BASE + "/supplemental/plurals.xml";
if (BASE_LOCALES.isEmpty()) {
setupBaseLocales("en-US");
@ -264,6 +267,9 @@ public class CLDRConverter {
// Generate Windows tzmappings
generateWindowsTZMappings();
// Generate Plural rules
generatePluralRules();
}
}
@ -451,6 +457,10 @@ public class CLDRConverter {
// Parse windowsZones
handlerWinZones = new WinZonesParseHandler();
parseLDMLFile(new File(WINZONES_SOURCE_FILE), handlerWinZones);
// Parse plurals
handlerPlurals = new PluralsParseHandler();
parseLDMLFile(new File(PLURALS_SOURCE_FILE), handlerPlurals);
}
// Parsers for data in "bcp47" directory
@ -1161,6 +1171,52 @@ public class CLDRConverter {
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
}
/**
* Generate ResourceBundle source file for plural rules. The generated
* class is {@code sun.text.resources.PluralRules} which has one public
* two dimensional array {@code rulesArray}. Each array element consists
* of two elements that designate the locale and the locale's plural rules
* string. The latter has the syntax from Unicode Consortium's
* <a href="http://unicode.org/reports/tr35/tr35-numbers.html#Plural_rules_syntax">
* Plural rules syntax</a>. {@code samples} and {@code "other"} are being ommited.
*
* @throws Exception
*/
private static void generatePluralRules() throws Exception {
Files.createDirectories(Paths.get(DESTINATION_DIR, "sun", "text", "resources"));
Files.write(Paths.get(DESTINATION_DIR, "sun", "text", "resources", "PluralRules.java"),
Stream.concat(
Stream.concat(
Stream.of(
"package sun.text.resources;",
"public final class PluralRules {",
" public static final String[][] rulesArray = {"
),
pluralRulesStream().sorted()
),
Stream.of(
" };",
"}"
)
)
.collect(Collectors.toList()),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
}
private static Stream<String> pluralRulesStream() {
return handlerPlurals.getData().entrySet().stream()
.filter(e -> !((Map<String, String>)e.getValue()).isEmpty())
.map(e -> {
String loc = e.getKey();
Map<String, String> rules = (Map<String, String>)e.getValue();
return " {\"" + loc + "\", \"" +
rules.entrySet().stream()
.map(rule -> rule.getKey() + ":" + rule.getValue().replaceFirst("@.*", ""))
.map(String::trim)
.collect(Collectors.joining(";")) + "\"},";
});
}
// for debug
static void dumpMap(Map<String, Object> map) {
map.entrySet().stream()
@ -1179,3 +1235,4 @@ public class CLDRConverter {
.forEach(System.out::println);
}
}

View File

@ -54,7 +54,6 @@ class LDMLParseHandler extends AbstractLDMLHandler<Object> {
private String currentContext = ""; // "format"/"stand-alone"
private String currentWidth = ""; // "wide"/"narrow"/"abbreviated"
private String currentStyle = ""; // short, long for decimalFormat
private String compactCount = ""; // one or other for decimalFormat
LDMLParseHandler(String id) {
this.id = id;
@ -577,32 +576,12 @@ class LDMLParseHandler extends AbstractLDMLHandler<Object> {
if (currentStyle == null) {
pushContainer(qName, attributes);
} else {
// The compact number patterns parsing assumes that the order
// of patterns are always in the increasing order of their
// type attribute i.e. type = 1000...
// Between the inflectional forms for a type (e.g.
// count = "one" and count = "other" for type = 1000), it is
// assumed that the count = "one" always appears before
// count = "other"
switch (currentStyle) {
case "short":
case "long":
String count = attributes.getValue("count");
// first pattern of count = "one" or count = "other"
if ((count.equals("one") || count.equals("other"))
&& compactCount.equals("")) {
compactCount = count;
pushStringListElement(qName, attributes,
(int) Math.log10(Double.parseDouble(attributes.getValue("type"))));
} else if ((count.equals("one") || count.equals("other"))
&& compactCount.equals(count)) {
// extract patterns with similar "count"
// attribute value
pushStringListElement(qName, attributes,
(int) Math.log10(Double.parseDouble(attributes.getValue("type"))));
} else {
pushIgnoredContainer(qName);
}
pushStringListElement(qName, attributes,
(int) Math.log10(Double.parseDouble(attributes.getValue("type"))),
attributes.getValue("count"));
break;
default:
pushIgnoredContainer(qName);
@ -1051,7 +1030,6 @@ class LDMLParseHandler extends AbstractLDMLHandler<Object> {
break;
case "decimalFormatLength":
currentStyle = "";
compactCount = "";
putIfEntry();
break;
case "currencyFormats":

View File

@ -0,0 +1,105 @@
/*
* Copyright (c) 2019, 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.
*/
package build.tools.cldrconverter;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* Handles parsing of files in Locale Data Markup Language for
* plurals.xml
*/
class PluralsParseHandler extends AbstractLDMLHandler<Object> {
@Override
public InputSource resolveEntity(String publicID, String systemID) throws IOException, SAXException {
// avoid HTTP traffic to unicode.org
if (systemID.startsWith(CLDRConverter.SPPL_LDML_DTD_SYSTEM_ID)) {
return new InputSource((new File(CLDRConverter.LOCAL_SPPL_LDML_DTD)).toURI().toString());
}
return null;
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
switch (qName) {
case "plurals":
// Only deal with "cardinal" type for now.
if (attributes.getValue("type").equals("cardinal")) {
pushContainer(qName, attributes);
} else {
// ignore
pushIgnoredContainer(qName);
}
break;
case "pluralRules":
// key: locales
pushKeyContainer(qName, attributes, attributes.getValue("locales"));
break;
case "pluralRule":
pushStringEntry(qName, attributes, attributes.getValue("count"));
break;
default:
// treat anything else as a container
pushContainer(qName, attributes);
break;
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
assert qName.equals(currentContainer.getqName()) : "current=" + currentContainer.getqName() + ", param=" + qName;
switch (qName) {
case "pluralRule":
assert !(currentContainer instanceof Entry);
Entry entry = (Entry)currentContainer;
final String count = entry.getKey();
final String rule = (String)entry.getValue();
String locales = ((KeyContainer)(currentContainer.getParent())).getKey();
Arrays.stream(locales.split("\\s"))
.forEach(loc -> {
Map<String, String> rules = (Map<String, String>)get(loc);
if (rules == null) {
rules = new HashMap<>();
put(loc, rules);
}
if (!count.equals("other")) {
rules.put(count, rule);
}
});
break;
}
currentContainer = currentContainer.getParent();
}
}

View File

@ -309,7 +309,7 @@ class ResourceBundleGenerator implements BundleGenerator {
// for languageAliasMap
if (CLDRConverter.isBaseModule) {
CLDRConverter.handlerSupplMeta.getLanguageAliasData().forEach((key, value) -> {
out.printf(" languageAliasMap.put(\"%s\", \"%s\");\n", key, value);
out.printf(" languageAliasMap.put(\"%s\", \"%s\");\n", key, value);
});
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018, 2019, 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
@ -28,20 +28,22 @@ package build.tools.cldrconverter;
class StringListElement extends Container {
StringListEntry list;
String count;
int index;
StringListElement(String qName, Container parent, int index) {
StringListElement(String qName, Container parent, int index, String count) {
super(qName, parent);
while (!(parent instanceof StringListEntry)) {
parent = parent.getParent();
}
list = (StringListEntry) parent;
this.index = index;
this.count = count;
}
@Override
void addCharacters(char[] characters, int start, int length) {
list.addCharacters(index, characters, start, length);
list.addCharacters(index, count, characters, start, length);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018, 2019, 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
@ -38,13 +38,22 @@ class StringListEntry extends Entry<List<String>> {
value = new ArrayList<>();
}
void addCharacters(int index, char[] characters, int start, int length) {
// fill with empty strings when the patterns start from index > 0
if (value.size() < index) {
IntStream.range(0, index).forEach(i -> value.add(i, ""));
value.add(index, new String(characters, start, length));
void addCharacters(int index, String count, char[] characters, int start, int length) {
int size = value.size();
String elem = count + ":" + new String(characters, start, length);
// quote embedded spaces, if any
elem = elem.replaceAll(" ", "' '");
if (size < index) {
// fill with empty strings when the patterns start from index > size
IntStream.range(size, index).forEach(i -> value.add(i, ""));
value.add(index, elem);
} else if (size == index) {
value.add(index, elem);
} else {
value.add(index, new String(characters, start, length));
// concatenate the pattern with the delimiter ' '
value.set(index, value.get(index) + " " + elem);
}
}

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,

View File

@ -45,10 +45,14 @@ import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.spi.NumberFormatProvider;
import java.util.Arrays;
import java.util.Currency;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import sun.text.resources.PluralRules;
/**
* Concrete implementation of the {@link java.text.spi.NumberFormatProvider
@ -69,6 +73,12 @@ public class NumberFormatProviderImpl extends NumberFormatProvider implements Av
private final LocaleProviderAdapter.Type type;
private final Set<String> langtags;
private static Map<String, String> rulesMap =
Arrays.stream(PluralRules.rulesArray).collect(Collectors.toMap(
sa -> sa[0],
sa -> sa[1])
);
public NumberFormatProviderImpl(LocaleProviderAdapter.Type type, Set<String> langtags) {
this.type = type;
this.langtags = langtags;
@ -271,8 +281,12 @@ public class NumberFormatProviderImpl extends NumberFormatProvider implements Av
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(override);
String[] cnPatterns = resource.getCNPatterns(formatStyle);
// plural rules
String pluralRules = rulesMap.getOrDefault(override.toString(),
rulesMap.getOrDefault(override.getLanguage(), ""));
CompactNumberFormat format = new CompactNumberFormat(numberPatterns[0],
symbols, cnPatterns);
symbols, cnPatterns, pluralRules);
return format;
}

View File

@ -378,6 +378,14 @@ public class SPILocaleProviderAdapter extends AuxLocaleProviderAdapter {
NumberFormatProvider nfp = getImpl(locale);
return nfp.getPercentInstance(locale);
}
@Override
public NumberFormat getCompactNumberInstance(Locale locale,
NumberFormat.Style style) {
locale = CalendarDataUtility.findRegionOverride(locale);
NumberFormatProvider nfp = getImpl(locale);
return nfp.getCompactNumberInstance(locale, style);
}
}
static class CalendarDataProviderDelegate extends CalendarDataProvider

View File

@ -0,0 +1,113 @@
/*
* Copyright (c) 2019, 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 8222756
* @summary Checks the plurals work with SPI provider
* @modules jdk.localedata
* @library provider
* @build provider/module-info provider/test.NumberFormatProviderImpl
* @run main/othervm -Djava.locale.providers=SPI,CLDR SPIProviderTest
*/
import java.text.CompactNumberFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Locale;
public class SPIProviderTest {
private static final Locale QAA = Locale.forLanguageTag("qaa");
private static final Locale QAB = Locale.forLanguageTag("qab");
public static void main(String... args) {
new SPIProviderTest();
}
SPIProviderTest() {
Arrays.stream(testData())
.forEach(SPIProviderTest::testSPIProvider);
}
Object[][] testData() {
return new Object[][]{
// Locale, Number, expected
{QAA, 1_000, "1K"},
{QAA, -1_000, "-1K"},
{QAA, 2_000, "2K"},
{QAA, -2_000, "-2K"},
{QAA, 1_000_000, "1M"},
{QAA, -1_000_000, "-1M"},
{QAA, 2_000_000, "2M"},
{QAA, -2_000_000, "-2M"},
{QAB, 1_000, "1K"},
{QAB, -1_000, "(1K)"},
{QAB, 2_000, "2KK"},
{QAB, -2_000, "-2KK"},
{QAB, 3_000, "3KKK"},
{QAB, -3_000, "-3KKK"},
{QAB, 5_000, "5KKKK"},
{QAB, -5_000, "-5KKKK"},
{QAB, 10_000, "10000"},
{QAB, -10_000, "-10000"},
{QAB, 1_000_000, "1 M"},
{QAB, -1_000_000, "(1 M)"},
{QAB, 2_000_000, "2 MM"},
{QAB, -2_000_000, "(2 MM)"},
{QAB, 3_000_000, "3 MMM"},
{QAB, -3_000_000, "-3 MMM"},
{QAB, 5_000_000, "5 MMMM"},
{QAB, -5_000_000, "-5 MMMM"},
};
}
public static void testSPIProvider(Object... args) {
Locale loc = (Locale)args[0];
Number number = (Number)args[1];
String expected = (String)args[2];
System.out.printf("Testing locale: %s, number: %d, expected: %s\n", loc, number, expected);
NumberFormat nf =
NumberFormat.getCompactNumberInstance(loc, NumberFormat.Style.SHORT);
String formatted = nf.format(number);
System.out.printf(" formatted: %s\n", formatted);
if (!formatted.equals(expected)) {
throw new RuntimeException("formatted and expected strings do not match.");
}
try {
Number parsed = nf.parse(formatted);
System.out.printf(" parsed: %s\n", parsed);
if (parsed.intValue() != number.intValue()) {
throw new RuntimeException("parsed and input numbers do not match.");
}
} catch (ParseException pe) {
throw new RuntimeException(pe);
}
}
}

View File

@ -22,7 +22,7 @@
*/
/*
* @test
* @bug 8177552 8217721
* @bug 8177552 8217721 8222756
* @summary Checks the functioning of compact number format
* @modules jdk.localedata
* @run testng/othervm TestCompactNumber
@ -75,6 +75,12 @@ public class TestCompactNumber {
private static final NumberFormat FORMAT_SE_SHORT = NumberFormat
.getCompactNumberInstance(new Locale("se"), NumberFormat.Style.SHORT);
private static final NumberFormat FORMAT_DE_LONG = NumberFormat
.getCompactNumberInstance(Locale.GERMAN, NumberFormat.Style.LONG);
private static final NumberFormat FORMAT_SL_LONG = NumberFormat
.getCompactNumberInstance(new Locale("sl"), NumberFormat.Style.LONG);
@DataProvider(name = "format")
Object[][] compactFormatData() {
return new Object[][]{
@ -248,7 +254,7 @@ public class TestCompactNumber {
{FORMAT_CA_LONG, 999.99, "1 miler"},
{FORMAT_CA_LONG, 99000, "99 milers"},
{FORMAT_CA_LONG, 330000, "330 milers"},
{FORMAT_CA_LONG, 3000.90, "3 miler"},
{FORMAT_CA_LONG, 3000.90, "3 milers"},
{FORMAT_CA_LONG, 1000000, "1 mili\u00f3"},
{FORMAT_CA_LONG, new BigInteger("12345678901234567890"),
"12345679 bilions"},
@ -320,7 +326,20 @@ public class TestCompactNumber {
// BigInteger
{FORMAT_SE_SHORT, new BigInteger("-12345678901234567890"), "\u221212345679\u00a0bn"},
// BigDecimal
{FORMAT_SE_SHORT, new BigDecimal("-12345678901234567890.98"), "\u221212345679\u00a0bn"},};
{FORMAT_SE_SHORT, new BigDecimal("-12345678901234567890.98"), "\u221212345679\u00a0bn"},
// Plurals
// DE: one:i = 1 and v = 0
{FORMAT_DE_LONG, 1_000_000, "1 Million"},
{FORMAT_DE_LONG, 2_000_000, "2 Millionen"},
// SL: one:v = 0 and i % 100 = 1
// two:v = 0 and i % 100 = 2
// few:v = 0 and i % 100 = 3..4 or v != 0
{FORMAT_SL_LONG, 1_000_000, "1 milijon"},
{FORMAT_SL_LONG, 2_000_000, "2 milijona"},
{FORMAT_SL_LONG, 3_000_000, "3 milijone"},
{FORMAT_SL_LONG, 5_000_000, "5 milijonov"},
};
}
@DataProvider(name = "parse")
@ -409,7 +428,20 @@ public class TestCompactNumber {
{FORMAT_SE_SHORT, "\u22128\u00a0mn", -8000000L, Long.class},
{FORMAT_SE_SHORT, "\u22128\u00a0dt", -8000L, Long.class},
{FORMAT_SE_SHORT, "\u221212345679\u00a0bn", -1.2345679E19, Double.class},
{FORMAT_SE_SHORT, "\u221212345679,89\u00a0bn", -1.2345679890000001E19, Double.class},};
{FORMAT_SE_SHORT, "\u221212345679,89\u00a0bn", -1.2345679890000001E19, Double.class},
// Plurals
// DE: one:i = 1 and v = 0
{FORMAT_DE_LONG, "1 Million", 1_000_000L, Long.class},
{FORMAT_DE_LONG, "2 Millionen", 2_000_000L, Long.class},
// SL: one:v = 0 and i % 100 = 1
// two:v = 0 and i % 100 = 2
// few:v = 0 and i % 100 = 3..4 or v != 0
{FORMAT_SL_LONG, "1 milijon", 1_000_000L, Long.class},
{FORMAT_SL_LONG, "2 milijona", 2_000_000L, Long.class},
{FORMAT_SL_LONG, "3 milijone", 3_000_000L, Long.class},
{FORMAT_SL_LONG, "5 milijonov", 5_000_000L, Long.class},
};
}
@DataProvider(name = "exceptionParse")
@ -444,7 +476,20 @@ public class TestCompactNumber {
// Take partial suffix "K" as 1000 for en_US_SHORT patterns
{FORMAT_EN_US_SHORT, "12KM", 12000L},
// Invalid suffix
{FORMAT_HI_IN_LONG, "-1 \u00a0\u0915.", -1L},};
{FORMAT_HI_IN_LONG, "-1 \u00a0\u0915.", -1L},
// invalid plurals
{FORMAT_DE_LONG, "2 Million", 2L},
{FORMAT_SL_LONG, "2 milijon", 2L},
{FORMAT_SL_LONG, "2 milijone", 2L},
{FORMAT_SL_LONG, "2 milijonv", 2L},
{FORMAT_SL_LONG, "3 milijon", 3L},
{FORMAT_SL_LONG, "3 milijona", 3L},
{FORMAT_SL_LONG, "3 milijonv", 3L},
{FORMAT_SL_LONG, "5 milijon", 5L},
{FORMAT_SL_LONG, "5 milijona", 5L},
{FORMAT_SL_LONG, "5 milijone", 5L},
};
}
@DataProvider(name = "fieldPosition")

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018, 2019, 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 8222756
* @summary Checks the equals and hashCode method of CompactNumberFormat
* @modules jdk.localedata
* @run testng/othervm TestEquality
@ -48,9 +48,26 @@ public class TestEquality {
// A custom compact instance with the same state as
// compact number instance of "en_US" locale with SHORT style
String decimalPattern = "#,##0.###";
String[] compactPatterns = new String[]{"", "", "", "0K", "00K", "000K", "0M", "00M", "000M", "0B", "00B", "000B", "0T", "00T", "000T"};
String[] compactPatterns = new String[]{
"",
"",
"",
"{one:0K other:0K}",
"{one:00K other:00K}",
"{one:000K other:000K}",
"{one:0M other:0M}",
"{one:00M other:00M}",
"{one:000M other:000M}",
"{one:0B other:0B}",
"{one:00B other:00B}",
"{one:000B other:000B}",
"{one:0T other:0T}",
"{one:00T other:00T}",
"{one:000T other:000T}"
};
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(Locale.US);
CompactNumberFormat cnf3 = new CompactNumberFormat(decimalPattern, symbols, compactPatterns);
CompactNumberFormat cnf3 =
new CompactNumberFormat(decimalPattern, symbols, compactPatterns, "one:i = 1 and v = 0");
// A compact instance created with different decimalPattern than cnf3
CompactNumberFormat cnf4 = new CompactNumberFormat("#,#0.0#", symbols, compactPatterns);

View File

@ -0,0 +1,118 @@
/*
* Copyright (c) 2019, 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 8222756
* @summary Tests plurals support in CompactNumberFormat
* @run testng/othervm TestPlurals
*/
import java.text.CompactNumberFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
import static org.testng.Assert.*;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
public class TestPlurals {
private final static DecimalFormatSymbols DFS = DecimalFormatSymbols.getInstance(Locale.ROOT);
private final static String[] PATTERN = {
"{zero:0->zero one:0->one two:0->two few:0->few many:0->many other:0->other}"};
private final static String RULE_1 = "zero:n = 0; one:n = 1; two:n = 2; few:n = 3..4; many:n = 5..6,8";
private final static String RULE_2 = "one:n % 2 = 1 or n / 3 = 2;";
private final static String RULE_3 = "one:n%2=0andn/3=2;";
@DataProvider
Object[][] pluralRules() {
return new Object[][]{
// rules, number, expected
{RULE_1, 0, "0->zero"},
{RULE_1, 1, "1->one"},
{RULE_1, 2, "2->two"},
{RULE_1, 3, "3->few"},
{RULE_1, 4, "4->few"},
{RULE_1, 5, "5->many"},
{RULE_1, 6, "6->many"},
{RULE_1, 7, "7->other"},
{RULE_1, 8, "8->many"},
{RULE_1, 9, "9->other"},
{RULE_2, 0, "0->other"},
{RULE_2, 1, "1->one"},
{RULE_2, 2, "2->other"},
{RULE_2, 3, "3->one"},
{RULE_2, 4, "4->other"},
{RULE_2, 5, "5->one"},
{RULE_2, 6, "6->one"},
{RULE_3, 0, "0->other"},
{RULE_3, 1, "1->other"},
{RULE_3, 2, "2->other"},
{RULE_3, 3, "3->other"},
{RULE_3, 4, "4->other"},
{RULE_3, 5, "5->other"},
{RULE_3, 6, "6->one"},
};
}
@DataProvider
Object[][] invalidRules() {
return new Object [][] {
{"one:a = 1"},
{"on:n = 1"},
{"one:n = 1...2"},
{"one:n = 1.2"},
{"one:n = 1..2,"},
{"one:n = 1;one:n = 2"},
{"foo:n = 1"},
{"one:n = 1..2 andor v % 10 != 0"},
};
}
@Test(expectedExceptions = NullPointerException.class)
public void testNullPluralRules() {
String[] pattern = {""};
new CompactNumberFormat("#", DFS, PATTERN, null);
}
@Test(dataProvider = "pluralRules")
public void testPluralRules(String rules, Number n, String expected) {
var cnp = new CompactNumberFormat("#", DFS, PATTERN, rules);
assertEquals(cnp.format(n), expected);
}
@Test(dataProvider = "invalidRules", expectedExceptions = IllegalArgumentException.class)
public void testInvalidRules(String rules) {
new CompactNumberFormat("#", DFS, PATTERN, rules);
}
@Test(expectedExceptions = IllegalArgumentException.class)
public void testLimitExceedingRules() {
String andCond = " and n = 1";
String invalid = "one: n = 1" + andCond.repeat(2_048 / andCond.length());
new CompactNumberFormat("#", DFS, PATTERN, invalid);
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) 2019, 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.
*/
module provider {
exports test;
provides java.text.spi.NumberFormatProvider with test.NumberFormatProviderImpl;
}

View File

@ -0,0 +1,111 @@
/*
* Copyright (c) 2019, 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.
*/
package test;
import java.text.CompactNumberFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.spi.NumberFormatProvider;
import java.util.Locale;
public class NumberFormatProviderImpl extends NumberFormatProvider {
private static final Locale QAA = Locale.forLanguageTag("qaa");
private static final Locale QAB = Locale.forLanguageTag("qab");
private static final Locale[] locales = {QAA, QAB};
private static final String[] oldPattern = {
// old US short compact format
"",
"",
"",
"0K",
"00K",
"000K",
"0M",
"00M",
"000M",
"0B",
"00B",
"000B",
"0T",
"00T",
"000T"
};
private static final String[] newPattern = {
"",
"",
"",
"{one:0K;(0K) two:0KK few:0KKK other:0KKKK}",
"",
"",
"{one:0' 'M;(0' 'M) two:0' 'MM;(0' 'MM) few:0' 'MMM other:0' 'MMMM}"
};
@Override
public NumberFormat getCurrencyInstance(Locale locale) {
return null;
}
@Override
public NumberFormat getIntegerInstance(Locale locale) {
return null;
}
@Override
public NumberFormat getNumberInstance(Locale locale) {
return null;
}
@Override
public NumberFormat getPercentInstance(Locale locale) {
return null;
}
@Override
public NumberFormat getCompactNumberInstance(Locale locale,
NumberFormat.Style style) {
if (locale.equals(QAB)) {
return new CompactNumberFormat(
"#",
DecimalFormatSymbols.getInstance(locale),
newPattern,
"one:v = 0 and i % 100 = 1;" +
"two:v = 0 and i % 100 = 2;" +
"few:v = 0 and i % 100 = 3..4 or v != 0;" +
"other:");
} else if (locale.equals(QAA)) {
return new CompactNumberFormat(
"#",
DecimalFormatSymbols.getInstance(locale),
oldPattern);
} else {
throw new RuntimeException("unsupported locale");
}
}
@Override
public Locale[] getAvailableLocales() {
return locales;
}
}