mirror of
https://github.com/darlinghq/darling-openjdk.git
synced 2024-11-27 22:30:28 +00:00
8222756: Plural support in CompactNumberFormat
Reviewed-by: joehw, rriggs
This commit is contained in:
parent
3000f212f2
commit
730d0ecf19
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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":
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
@ -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);
|
||||
|
118
test/jdk/java/text/Format/CompactNumberFormat/TestPlurals.java
Normal file
118
test/jdk/java/text/Format/CompactNumberFormat/TestPlurals.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user