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)) {
|
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) {
|
if (i < size) {
|
||||||
pattern = patterns.get(i);
|
pattern = patterns.get(i);
|
||||||
if (!pattern.isEmpty()) {
|
if (!pattern.isEmpty()) {
|
||||||
return pattern;
|
return "{" + pattern + "}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if not found, try parent
|
// if not found, try parent
|
||||||
if (i < psize) {
|
if (i < psize) {
|
||||||
pattern = pList.get(i);
|
pattern = pList.get(i);
|
||||||
if (!pattern.isEmpty()) {
|
if (!pattern.isEmpty()) {
|
||||||
return pattern;
|
return "{" + pattern + "}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// bail out with empty string
|
// bail out with empty string
|
||||||
|
@ -70,6 +70,7 @@ public class CLDRConverter {
|
|||||||
private static String LIKELYSUBTAGS_SOURCE_FILE;
|
private static String LIKELYSUBTAGS_SOURCE_FILE;
|
||||||
private static String TIMEZONE_SOURCE_FILE;
|
private static String TIMEZONE_SOURCE_FILE;
|
||||||
private static String WINZONES_SOURCE_FILE;
|
private static String WINZONES_SOURCE_FILE;
|
||||||
|
private static String PLURALS_SOURCE_FILE;
|
||||||
static String DESTINATION_DIR = "build/gensrc";
|
static String DESTINATION_DIR = "build/gensrc";
|
||||||
|
|
||||||
static final String LOCALE_NAME_PREFIX = "locale.displayname.";
|
static final String LOCALE_NAME_PREFIX = "locale.displayname.";
|
||||||
@ -93,6 +94,7 @@ public class CLDRConverter {
|
|||||||
private static SupplementDataParseHandler handlerSuppl;
|
private static SupplementDataParseHandler handlerSuppl;
|
||||||
private static LikelySubtagsParseHandler handlerLikelySubtags;
|
private static LikelySubtagsParseHandler handlerLikelySubtags;
|
||||||
private static WinZonesParseHandler handlerWinZones;
|
private static WinZonesParseHandler handlerWinZones;
|
||||||
|
static PluralsParseHandler handlerPlurals;
|
||||||
static SupplementalMetadataParseHandler handlerSupplMeta;
|
static SupplementalMetadataParseHandler handlerSupplMeta;
|
||||||
static NumberingSystemsParseHandler handlerNumbering;
|
static NumberingSystemsParseHandler handlerNumbering;
|
||||||
static MetaZonesParseHandler handlerMetaZones;
|
static MetaZonesParseHandler handlerMetaZones;
|
||||||
@ -244,6 +246,7 @@ public class CLDRConverter {
|
|||||||
TIMEZONE_SOURCE_FILE = CLDR_BASE + "/bcp47/timezone.xml";
|
TIMEZONE_SOURCE_FILE = CLDR_BASE + "/bcp47/timezone.xml";
|
||||||
SPPL_META_SOURCE_FILE = CLDR_BASE + "/supplemental/supplementalMetadata.xml";
|
SPPL_META_SOURCE_FILE = CLDR_BASE + "/supplemental/supplementalMetadata.xml";
|
||||||
WINZONES_SOURCE_FILE = CLDR_BASE + "/supplemental/windowsZones.xml";
|
WINZONES_SOURCE_FILE = CLDR_BASE + "/supplemental/windowsZones.xml";
|
||||||
|
PLURALS_SOURCE_FILE = CLDR_BASE + "/supplemental/plurals.xml";
|
||||||
|
|
||||||
if (BASE_LOCALES.isEmpty()) {
|
if (BASE_LOCALES.isEmpty()) {
|
||||||
setupBaseLocales("en-US");
|
setupBaseLocales("en-US");
|
||||||
@ -264,6 +267,9 @@ public class CLDRConverter {
|
|||||||
|
|
||||||
// Generate Windows tzmappings
|
// Generate Windows tzmappings
|
||||||
generateWindowsTZMappings();
|
generateWindowsTZMappings();
|
||||||
|
|
||||||
|
// Generate Plural rules
|
||||||
|
generatePluralRules();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -451,6 +457,10 @@ public class CLDRConverter {
|
|||||||
// Parse windowsZones
|
// Parse windowsZones
|
||||||
handlerWinZones = new WinZonesParseHandler();
|
handlerWinZones = new WinZonesParseHandler();
|
||||||
parseLDMLFile(new File(WINZONES_SOURCE_FILE), handlerWinZones);
|
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
|
// Parsers for data in "bcp47" directory
|
||||||
@ -1161,6 +1171,52 @@ public class CLDRConverter {
|
|||||||
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
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
|
// for debug
|
||||||
static void dumpMap(Map<String, Object> map) {
|
static void dumpMap(Map<String, Object> map) {
|
||||||
map.entrySet().stream()
|
map.entrySet().stream()
|
||||||
@ -1179,3 +1235,4 @@ public class CLDRConverter {
|
|||||||
.forEach(System.out::println);
|
.forEach(System.out::println);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +54,6 @@ class LDMLParseHandler extends AbstractLDMLHandler<Object> {
|
|||||||
private String currentContext = ""; // "format"/"stand-alone"
|
private String currentContext = ""; // "format"/"stand-alone"
|
||||||
private String currentWidth = ""; // "wide"/"narrow"/"abbreviated"
|
private String currentWidth = ""; // "wide"/"narrow"/"abbreviated"
|
||||||
private String currentStyle = ""; // short, long for decimalFormat
|
private String currentStyle = ""; // short, long for decimalFormat
|
||||||
private String compactCount = ""; // one or other for decimalFormat
|
|
||||||
|
|
||||||
LDMLParseHandler(String id) {
|
LDMLParseHandler(String id) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@ -577,32 +576,12 @@ class LDMLParseHandler extends AbstractLDMLHandler<Object> {
|
|||||||
if (currentStyle == null) {
|
if (currentStyle == null) {
|
||||||
pushContainer(qName, attributes);
|
pushContainer(qName, attributes);
|
||||||
} else {
|
} 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) {
|
switch (currentStyle) {
|
||||||
case "short":
|
case "short":
|
||||||
case "long":
|
case "long":
|
||||||
String count = attributes.getValue("count");
|
pushStringListElement(qName, attributes,
|
||||||
// first pattern of count = "one" or count = "other"
|
(int) Math.log10(Double.parseDouble(attributes.getValue("type"))),
|
||||||
if ((count.equals("one") || count.equals("other"))
|
attributes.getValue("count"));
|
||||||
&& 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);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
pushIgnoredContainer(qName);
|
pushIgnoredContainer(qName);
|
||||||
@ -1051,7 +1030,6 @@ class LDMLParseHandler extends AbstractLDMLHandler<Object> {
|
|||||||
break;
|
break;
|
||||||
case "decimalFormatLength":
|
case "decimalFormatLength":
|
||||||
currentStyle = "";
|
currentStyle = "";
|
||||||
compactCount = "";
|
|
||||||
putIfEntry();
|
putIfEntry();
|
||||||
break;
|
break;
|
||||||
case "currencyFormats":
|
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
|
// for languageAliasMap
|
||||||
if (CLDRConverter.isBaseModule) {
|
if (CLDRConverter.isBaseModule) {
|
||||||
CLDRConverter.handlerSupplMeta.getLanguageAliasData().forEach((key, value) -> {
|
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.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* 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 {
|
class StringListElement extends Container {
|
||||||
|
|
||||||
StringListEntry list;
|
StringListEntry list;
|
||||||
|
String count;
|
||||||
int index;
|
int index;
|
||||||
|
|
||||||
StringListElement(String qName, Container parent, int index) {
|
StringListElement(String qName, Container parent, int index, String count) {
|
||||||
super(qName, parent);
|
super(qName, parent);
|
||||||
while (!(parent instanceof StringListEntry)) {
|
while (!(parent instanceof StringListEntry)) {
|
||||||
parent = parent.getParent();
|
parent = parent.getParent();
|
||||||
}
|
}
|
||||||
list = (StringListEntry) parent;
|
list = (StringListEntry) parent;
|
||||||
this.index = index;
|
this.index = index;
|
||||||
|
this.count = count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void addCharacters(char[] characters, int start, int length) {
|
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.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* 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<>();
|
value = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
void addCharacters(int index, char[] characters, int start, int length) {
|
void addCharacters(int index, String count, char[] characters, int start, int length) {
|
||||||
// fill with empty strings when the patterns start from index > 0
|
int size = value.size();
|
||||||
if (value.size() < index) {
|
String elem = count + ":" + new String(characters, start, length);
|
||||||
IntStream.range(0, index).forEach(i -> value.add(i, ""));
|
|
||||||
value.add(index, 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 {
|
} 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.math.RoundingMode;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
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 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
|
* a compact pattern. This special pattern can appear explicitly for any specific
|
||||||
* range, or considered as a default pattern for an empty string.
|
* 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
|
* A compact pattern contains a positive and negative subpattern
|
||||||
* separated by a subpattern boundary character {@code ';' (U+003B)},
|
* separated by a subpattern boundary character {@code ';' (U+003B)},
|
||||||
* for example, {@code "0K;-0K"}. Each subpattern has a prefix,
|
* 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
|
* unless noted otherwise, if they are to appear in the prefix or suffix
|
||||||
* as literals. For example, 0\u0915'.'.
|
* 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>
|
* <h2>Formatting</h2>
|
||||||
* The default formatting behavior returns a formatted string with no fractional
|
* The default formatting behavior returns a formatted string with no fractional
|
||||||
* digits, however users can use the {@link #setMinimumFractionDigits(int)}
|
* 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
|
* List of positive prefix patterns of this formatter's
|
||||||
* compact number patterns.
|
* compact number patterns.
|
||||||
*/
|
*/
|
||||||
private transient List<String> positivePrefixPatterns;
|
private transient List<Patterns> positivePrefixPatterns;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of negative prefix patterns of this formatter's
|
* List of negative prefix patterns of this formatter's
|
||||||
* compact number patterns.
|
* compact number patterns.
|
||||||
*/
|
*/
|
||||||
private transient List<String> negativePrefixPatterns;
|
private transient List<Patterns> negativePrefixPatterns;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of positive suffix patterns of this formatter's
|
* List of positive suffix patterns of this formatter's
|
||||||
* compact number patterns.
|
* compact number patterns.
|
||||||
*/
|
*/
|
||||||
private transient List<String> positiveSuffixPatterns;
|
private transient List<Patterns> positiveSuffixPatterns;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of negative suffix patterns of this formatter's
|
* List of negative suffix patterns of this formatter's
|
||||||
* compact number patterns.
|
* compact number patterns.
|
||||||
*/
|
*/
|
||||||
private transient List<String> negativeSuffixPatterns;
|
private transient List<Patterns> negativeSuffixPatterns;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of divisors of this formatter's compact number patterns.
|
* 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;
|
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
|
* Special pattern used for compact numbers
|
||||||
*/
|
*/
|
||||||
@ -328,20 +377,56 @@ public final class CompactNumberFormat extends NumberFormat {
|
|||||||
* <a href = "CompactNumberFormat.html#compact_number_patterns">
|
* <a href = "CompactNumberFormat.html#compact_number_patterns">
|
||||||
* compact number patterns</a>
|
* compact number patterns</a>
|
||||||
* @throws NullPointerException if any of the given arguments is
|
* @throws NullPointerException if any of the given arguments is
|
||||||
* {@code null}
|
* {@code null}
|
||||||
* @throws IllegalArgumentException if the given {@code decimalPattern} or the
|
* @throws IllegalArgumentException if the given {@code decimalPattern} or the
|
||||||
* {@code compactPatterns} array contains an invalid pattern
|
* {@code compactPatterns} array contains an invalid pattern
|
||||||
* or if a {@code null} appears in the array of compact
|
* or if a {@code null} appears in the array of compact
|
||||||
* patterns
|
* patterns
|
||||||
* @see DecimalFormat#DecimalFormat(java.lang.String, DecimalFormatSymbols)
|
* @see DecimalFormat#DecimalFormat(java.lang.String, DecimalFormatSymbols)
|
||||||
* @see DecimalFormatSymbols
|
* @see DecimalFormatSymbols
|
||||||
*/
|
*/
|
||||||
public CompactNumberFormat(String decimalPattern,
|
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(decimalPattern, "decimalPattern");
|
||||||
Objects.requireNonNull(symbols, "symbols");
|
Objects.requireNonNull(symbols, "symbols");
|
||||||
Objects.requireNonNull(compactPatterns, "compactPatterns");
|
Objects.requireNonNull(compactPatterns, "compactPatterns");
|
||||||
|
Objects.requireNonNull(pluralRules, "pluralRules");
|
||||||
|
|
||||||
this.symbols = symbols;
|
this.symbols = symbols;
|
||||||
// Instantiating the DecimalFormat with "0" pattern; this acts just as a
|
// 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,
|
defaultDecimalFormat = new DecimalFormat(this.decimalPattern,
|
||||||
this.symbols);
|
this.symbols);
|
||||||
defaultDecimalFormat.setMaximumFractionDigits(0);
|
defaultDecimalFormat.setMaximumFractionDigits(0);
|
||||||
|
|
||||||
|
this.pluralRules = pluralRules;
|
||||||
|
|
||||||
// Process compact patterns to extract the prefixes, suffixes and
|
// Process compact patterns to extract the prefixes, suffixes and
|
||||||
// divisors
|
// divisors
|
||||||
processCompactPatterns();
|
processCompactPatterns();
|
||||||
@ -494,14 +582,13 @@ public final class CompactNumberFormat extends NumberFormat {
|
|||||||
double roundedNumber = dList.getDouble();
|
double roundedNumber = dList.getDouble();
|
||||||
int compactDataIndex = selectCompactPattern((long) roundedNumber);
|
int compactDataIndex = selectCompactPattern((long) roundedNumber);
|
||||||
if (compactDataIndex != -1) {
|
if (compactDataIndex != -1) {
|
||||||
String prefix = isNegative ? negativePrefixPatterns.get(compactDataIndex)
|
long divisor = (Long) divisors.get(compactDataIndex);
|
||||||
: positivePrefixPatterns.get(compactDataIndex);
|
int iPart = getIntegerPart(number, divisor);
|
||||||
String suffix = isNegative ? negativeSuffixPatterns.get(compactDataIndex)
|
String prefix = getAffix(false, true, isNegative, compactDataIndex, iPart);
|
||||||
: positiveSuffixPatterns.get(compactDataIndex);
|
String suffix = getAffix(false, false, isNegative, compactDataIndex, iPart);
|
||||||
|
|
||||||
if (!prefix.isEmpty() || !suffix.isEmpty()) {
|
if (!prefix.isEmpty() || !suffix.isEmpty()) {
|
||||||
appendPrefix(result, prefix, delegate);
|
appendPrefix(result, prefix, delegate);
|
||||||
long divisor = (Long) divisors.get(compactDataIndex);
|
|
||||||
roundedNumber = roundedNumber / divisor;
|
roundedNumber = roundedNumber / divisor;
|
||||||
decimalFormat.setDigitList(roundedNumber, isNegative, getMaximumFractionDigits());
|
decimalFormat.setDigitList(roundedNumber, isNegative, getMaximumFractionDigits());
|
||||||
decimalFormat.subformatNumber(result, delegate, isNegative,
|
decimalFormat.subformatNumber(result, delegate, isNegative,
|
||||||
@ -562,13 +649,12 @@ public final class CompactNumberFormat extends NumberFormat {
|
|||||||
|
|
||||||
int compactDataIndex = selectCompactPattern(number);
|
int compactDataIndex = selectCompactPattern(number);
|
||||||
if (compactDataIndex != -1) {
|
if (compactDataIndex != -1) {
|
||||||
String prefix = isNegative ? negativePrefixPatterns.get(compactDataIndex)
|
long divisor = (Long) divisors.get(compactDataIndex);
|
||||||
: positivePrefixPatterns.get(compactDataIndex);
|
int iPart = getIntegerPart(number, divisor);
|
||||||
String suffix = isNegative ? negativeSuffixPatterns.get(compactDataIndex)
|
String prefix = getAffix(false, true, isNegative, compactDataIndex, iPart);
|
||||||
: positiveSuffixPatterns.get(compactDataIndex);
|
String suffix = getAffix(false, false, isNegative, compactDataIndex, iPart);
|
||||||
if (!prefix.isEmpty() || !suffix.isEmpty()) {
|
if (!prefix.isEmpty() || !suffix.isEmpty()) {
|
||||||
appendPrefix(result, prefix, delegate);
|
appendPrefix(result, prefix, delegate);
|
||||||
long divisor = (Long) divisors.get(compactDataIndex);
|
|
||||||
if ((number % divisor == 0)) {
|
if ((number % divisor == 0)) {
|
||||||
number = number / divisor;
|
number = number / divisor;
|
||||||
decimalFormat.setDigitList(number, isNegative, 0);
|
decimalFormat.setDigitList(number, isNegative, 0);
|
||||||
@ -649,19 +735,19 @@ public final class CompactNumberFormat extends NumberFormat {
|
|||||||
|
|
||||||
int compactDataIndex;
|
int compactDataIndex;
|
||||||
if (number.toBigInteger().bitLength() < 64) {
|
if (number.toBigInteger().bitLength() < 64) {
|
||||||
compactDataIndex = selectCompactPattern(number.toBigInteger().longValue());
|
long longNumber = number.toBigInteger().longValue();
|
||||||
|
compactDataIndex = selectCompactPattern(longNumber);
|
||||||
} else {
|
} else {
|
||||||
compactDataIndex = selectCompactPattern(number.toBigInteger());
|
compactDataIndex = selectCompactPattern(number.toBigInteger());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (compactDataIndex != -1) {
|
if (compactDataIndex != -1) {
|
||||||
String prefix = isNegative ? negativePrefixPatterns.get(compactDataIndex)
|
Number divisor = divisors.get(compactDataIndex);
|
||||||
: positivePrefixPatterns.get(compactDataIndex);
|
int iPart = getIntegerPart(number.doubleValue(), divisor.doubleValue());
|
||||||
String suffix = isNegative ? negativeSuffixPatterns.get(compactDataIndex)
|
String prefix = getAffix(false, true, isNegative, compactDataIndex, iPart);
|
||||||
: positiveSuffixPatterns.get(compactDataIndex);
|
String suffix = getAffix(false, false, isNegative, compactDataIndex, iPart);
|
||||||
if (!prefix.isEmpty() || !suffix.isEmpty()) {
|
if (!prefix.isEmpty() || !suffix.isEmpty()) {
|
||||||
appendPrefix(result, prefix, delegate);
|
appendPrefix(result, prefix, delegate);
|
||||||
Number divisor = divisors.get(compactDataIndex);
|
|
||||||
number = number.divide(new BigDecimal(divisor.toString()), getRoundingMode());
|
number = number.divide(new BigDecimal(divisor.toString()), getRoundingMode());
|
||||||
decimalFormat.setDigitList(number, isNegative, getMaximumFractionDigits());
|
decimalFormat.setDigitList(number, isNegative, getMaximumFractionDigits());
|
||||||
decimalFormat.subformatNumber(result, delegate, isNegative,
|
decimalFormat.subformatNumber(result, delegate, isNegative,
|
||||||
@ -721,13 +807,12 @@ public final class CompactNumberFormat extends NumberFormat {
|
|||||||
|
|
||||||
int compactDataIndex = selectCompactPattern(number);
|
int compactDataIndex = selectCompactPattern(number);
|
||||||
if (compactDataIndex != -1) {
|
if (compactDataIndex != -1) {
|
||||||
String prefix = isNegative ? negativePrefixPatterns.get(compactDataIndex)
|
Number divisor = divisors.get(compactDataIndex);
|
||||||
: positivePrefixPatterns.get(compactDataIndex);
|
int iPart = getIntegerPart(number.doubleValue(), divisor.doubleValue());
|
||||||
String suffix = isNegative ? negativeSuffixPatterns.get(compactDataIndex)
|
String prefix = getAffix(false, true, isNegative, compactDataIndex, iPart);
|
||||||
: positiveSuffixPatterns.get(compactDataIndex);
|
String suffix = getAffix(false, false, isNegative, compactDataIndex, iPart);
|
||||||
if (!prefix.isEmpty() || !suffix.isEmpty()) {
|
if (!prefix.isEmpty() || !suffix.isEmpty()) {
|
||||||
appendPrefix(result, prefix, delegate);
|
appendPrefix(result, prefix, delegate);
|
||||||
Number divisor = divisors.get(compactDataIndex);
|
|
||||||
if (number.mod(new BigInteger(divisor.toString()))
|
if (number.mod(new BigInteger(divisor.toString()))
|
||||||
.compareTo(BigInteger.ZERO) == 0) {
|
.compareTo(BigInteger.ZERO) == 0) {
|
||||||
number = number.divide(new BigInteger(divisor.toString()));
|
number = number.divide(new BigInteger(divisor.toString()));
|
||||||
@ -761,6 +846,18 @@ public final class CompactNumberFormat extends NumberFormat {
|
|||||||
return result;
|
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
|
* Appends the {@code prefix} to the {@code result} and also set the
|
||||||
* {@code NumberFormat.Field.SIGN} and {@code NumberFormat.Field.PREFIX}
|
* {@code NumberFormat.Field.SIGN} and {@code NumberFormat.Field.PREFIX}
|
||||||
@ -1042,6 +1139,10 @@ public final class CompactNumberFormat extends NumberFormat {
|
|||||||
* value.
|
* 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() {
|
private void processCompactPatterns() {
|
||||||
int size = compactPatterns.length;
|
int size = compactPatterns.length;
|
||||||
positivePrefixPatterns = new ArrayList<>(size);
|
positivePrefixPatterns = new ArrayList<>(size);
|
||||||
@ -1051,8 +1152,80 @@ public final class CompactNumberFormat extends NumberFormat {
|
|||||||
divisors = new ArrayList<>(size);
|
divisors = new ArrayList<>(size);
|
||||||
|
|
||||||
for (int index = 0; index < size; index++) {
|
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
|
* @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) {
|
if (pattern == null) {
|
||||||
throw new IllegalArgumentException("A null compact pattern" +
|
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
|
// Only if positive affix exists; else put empty strings
|
||||||
if (!positivePrefix.isEmpty() || !positiveSuffix.isEmpty()) {
|
if (!positivePrefix.isEmpty() || !positiveSuffix.isEmpty()) {
|
||||||
positivePrefixPatterns.add(positivePrefix);
|
positivePrefixPatterns.get(index).put(count, positivePrefix);
|
||||||
negativePrefixPatterns.add(negativePrefix);
|
negativePrefixPatterns.get(index).put(count, negativePrefix);
|
||||||
positiveSuffixPatterns.add(positiveSuffix);
|
positiveSuffixPatterns.get(index).put(count, positiveSuffix);
|
||||||
negativeSuffixPatterns.add(negativeSuffix);
|
negativeSuffixPatterns.get(index).put(count, negativeSuffix);
|
||||||
divisors.add(computeDivisor(zeros, index));
|
if (divisors.size() <= index) {
|
||||||
|
divisors.add(computeDivisor(zeros, index));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
positivePrefixPatterns.add("");
|
positivePrefixPatterns.get(index).put(count, "");
|
||||||
negativePrefixPatterns.add("");
|
negativePrefixPatterns.get(index).put(count, "");
|
||||||
positiveSuffixPatterns.add("");
|
positiveSuffixPatterns.get(index).put(count, "");
|
||||||
negativeSuffixPatterns.add("");
|
negativeSuffixPatterns.get(index).put(count, "");
|
||||||
divisors.add(1L);
|
if (divisors.size() <= index) {
|
||||||
|
divisors.add(1L);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1270,10 +1447,10 @@ public final class CompactNumberFormat extends NumberFormat {
|
|||||||
// the expanded form contains special characters in
|
// the expanded form contains special characters in
|
||||||
// its localized form, which are used for matching
|
// its localized form, which are used for matching
|
||||||
// while parsing a string to number
|
// while parsing a string to number
|
||||||
private transient List<String> positivePrefixes;
|
private transient List<Patterns> positivePrefixes;
|
||||||
private transient List<String> negativePrefixes;
|
private transient List<Patterns> negativePrefixes;
|
||||||
private transient List<String> positiveSuffixes;
|
private transient List<Patterns> positiveSuffixes;
|
||||||
private transient List<String> negativeSuffixes;
|
private transient List<Patterns> negativeSuffixes;
|
||||||
|
|
||||||
private void expandAffixPatterns() {
|
private void expandAffixPatterns() {
|
||||||
positivePrefixes = new ArrayList<>(compactPatterns.length);
|
positivePrefixes = new ArrayList<>(compactPatterns.length);
|
||||||
@ -1281,10 +1458,10 @@ public final class CompactNumberFormat extends NumberFormat {
|
|||||||
positiveSuffixes = new ArrayList<>(compactPatterns.length);
|
positiveSuffixes = new ArrayList<>(compactPatterns.length);
|
||||||
negativeSuffixes = new ArrayList<>(compactPatterns.length);
|
negativeSuffixes = new ArrayList<>(compactPatterns.length);
|
||||||
for (int index = 0; index < compactPatterns.length; index++) {
|
for (int index = 0; index < compactPatterns.length; index++) {
|
||||||
positivePrefixes.add(expandAffix(positivePrefixPatterns.get(index)));
|
positivePrefixes.add(positivePrefixPatterns.get(index).expandAffix());
|
||||||
negativePrefixes.add(expandAffix(negativePrefixPatterns.get(index)));
|
negativePrefixes.add(negativePrefixPatterns.get(index).expandAffix());
|
||||||
positiveSuffixes.add(expandAffix(positiveSuffixPatterns.get(index)));
|
positiveSuffixes.add(positiveSuffixPatterns.get(index).expandAffix());
|
||||||
negativeSuffixes.add(expandAffix(negativeSuffixPatterns.get(index)));
|
negativeSuffixes.add(negativeSuffixPatterns.get(index).expandAffix());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1382,10 +1559,12 @@ public final class CompactNumberFormat extends NumberFormat {
|
|||||||
String matchedNegPrefix = "";
|
String matchedNegPrefix = "";
|
||||||
String defaultPosPrefix = defaultDecimalFormat.getPositivePrefix();
|
String defaultPosPrefix = defaultDecimalFormat.getPositivePrefix();
|
||||||
String defaultNegPrefix = defaultDecimalFormat.getNegativePrefix();
|
String defaultNegPrefix = defaultDecimalFormat.getNegativePrefix();
|
||||||
|
double num = parseNumberPart(text, position);
|
||||||
|
|
||||||
// Prefix matching
|
// Prefix matching
|
||||||
for (int compactIndex = 0; compactIndex < compactPatterns.length; compactIndex++) {
|
for (int compactIndex = 0; compactIndex < compactPatterns.length; compactIndex++) {
|
||||||
String positivePrefix = positivePrefixes.get(compactIndex);
|
String positivePrefix = getAffix(true, true, false, compactIndex, (int)num);
|
||||||
String negativePrefix = negativePrefixes.get(compactIndex);
|
String negativePrefix = getAffix(true, true, true, compactIndex, (int)num);
|
||||||
|
|
||||||
// Do not break if a match occur; there is a possibility that the
|
// Do not break if a match occur; there is a possibility that the
|
||||||
// subsequent affixes may match the longer subsequence in the given
|
// subsequent affixes may match the longer subsequence in the given
|
||||||
@ -1487,7 +1666,7 @@ public final class CompactNumberFormat extends NumberFormat {
|
|||||||
pos.index = position;
|
pos.index = position;
|
||||||
Number multiplier = computeParseMultiplier(text, pos,
|
Number multiplier = computeParseMultiplier(text, pos,
|
||||||
gotPositive ? matchedPosPrefix : matchedNegPrefix,
|
gotPositive ? matchedPosPrefix : matchedNegPrefix,
|
||||||
status, gotPositive, gotNegative);
|
status, gotPositive, gotNegative, num);
|
||||||
|
|
||||||
if (multiplier.longValue() == -1L) {
|
if (multiplier.longValue() == -1L) {
|
||||||
return null;
|
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
|
* Returns the parsed result by multiplying the parsed number
|
||||||
* with the multiplier representing the prefix and suffix.
|
* 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,
|
private Number computeParseMultiplier(String text, ParsePosition parsePosition,
|
||||||
String matchedPrefix, boolean[] status, boolean gotPositive,
|
String matchedPrefix, boolean[] status, boolean gotPositive,
|
||||||
boolean gotNegative) {
|
boolean gotNegative, double num) {
|
||||||
|
|
||||||
int position = parsePosition.index;
|
int position = parsePosition.index;
|
||||||
boolean gotPos = false;
|
boolean gotPos = false;
|
||||||
@ -1674,10 +1880,10 @@ public final class CompactNumberFormat extends NumberFormat {
|
|||||||
String matchedPosSuffix = "";
|
String matchedPosSuffix = "";
|
||||||
String matchedNegSuffix = "";
|
String matchedNegSuffix = "";
|
||||||
for (int compactIndex = 0; compactIndex < compactPatterns.length; compactIndex++) {
|
for (int compactIndex = 0; compactIndex < compactPatterns.length; compactIndex++) {
|
||||||
String positivePrefix = positivePrefixes.get(compactIndex);
|
String positivePrefix = getAffix(true, true, false, compactIndex, (int)num);
|
||||||
String negativePrefix = negativePrefixes.get(compactIndex);
|
String negativePrefix = getAffix(true, true, true, compactIndex, (int)num);
|
||||||
String positiveSuffix = positiveSuffixes.get(compactIndex);
|
String positiveSuffix = getAffix(true, false, false, compactIndex, (int)num);
|
||||||
String negativeSuffix = negativeSuffixes.get(compactIndex);
|
String negativeSuffix = getAffix(true, false, true, compactIndex, (int)num);
|
||||||
|
|
||||||
// Do not break if a match occur; there is a possibility that the
|
// Do not break if a match occur; there is a possibility that the
|
||||||
// subsequent affixes may match the longer subsequence in the given
|
// 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.
|
* if the minimum or maximum fraction digit count is larger than 340.
|
||||||
* <li> If the grouping size is negative or larger than 127.
|
* <li> If the grouping size is negative or larger than 127.
|
||||||
* </ul>
|
* </ul>
|
||||||
|
* If the {@code pluralRules} field is not deserialized from the stream, it
|
||||||
|
* will be set to an empty string.
|
||||||
*
|
*
|
||||||
* @param inStream the stream
|
* @param inStream the stream
|
||||||
* @throws IOException if an I/O error occurs
|
* @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");
|
throw new InvalidObjectException("Grouping size is negative");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pluralRules is since 14. Fill in empty string if it is null
|
||||||
|
if (pluralRules == null) {
|
||||||
|
pluralRules = "";
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
processCompactPatterns();
|
processCompactPatterns();
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
@ -2111,6 +2324,7 @@ public final class CompactNumberFormat extends NumberFormat {
|
|||||||
&& symbols.equals(other.symbols)
|
&& symbols.equals(other.symbols)
|
||||||
&& Arrays.equals(compactPatterns, other.compactPatterns)
|
&& Arrays.equals(compactPatterns, other.compactPatterns)
|
||||||
&& roundingMode.equals(other.roundingMode)
|
&& roundingMode.equals(other.roundingMode)
|
||||||
|
&& pluralRules.equals(other.pluralRules)
|
||||||
&& groupingSize == other.groupingSize
|
&& groupingSize == other.groupingSize
|
||||||
&& parseBigDecimal == other.parseBigDecimal;
|
&& parseBigDecimal == other.parseBigDecimal;
|
||||||
}
|
}
|
||||||
@ -2123,7 +2337,7 @@ public final class CompactNumberFormat extends NumberFormat {
|
|||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return 31 * super.hashCode() +
|
return 31 * super.hashCode() +
|
||||||
Objects.hash(decimalPattern, symbols, roundingMode)
|
Objects.hash(decimalPattern, symbols, roundingMode, pluralRules)
|
||||||
+ Arrays.hashCode(compactPatterns) + groupingSize
|
+ Arrays.hashCode(compactPatterns) + groupingSize
|
||||||
+ Boolean.hashCode(parseBigDecimal);
|
+ Boolean.hashCode(parseBigDecimal);
|
||||||
}
|
}
|
||||||
@ -2142,4 +2356,155 @@ public final class CompactNumberFormat extends NumberFormat {
|
|||||||
return other;
|
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}.
|
* {@code locale} and {@code formatStyle}.
|
||||||
*
|
*
|
||||||
* @implSpec The default implementation of this method throws
|
* @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 this method returns the compact number formatter instance
|
||||||
* of the given {@code locale} with specified {@code formatStyle}.
|
* 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
|
* one of the locales returned from
|
||||||
* {@link java.util.spi.LocaleServiceProvider#getAvailableLocales()
|
* {@link java.util.spi.LocaleServiceProvider#getAvailableLocales()
|
||||||
* getAvailableLocales()}.
|
* getAvailableLocales()}.
|
||||||
|
* @throws UnsupportedOperationException if the implementation does not
|
||||||
|
* support this method
|
||||||
* @return a compact number formatter
|
* @return a compact number formatter
|
||||||
*
|
*
|
||||||
* @see java.text.NumberFormat#getCompactNumberInstance(Locale,
|
* @see java.text.NumberFormat#getCompactNumberInstance(Locale,
|
||||||
|
@ -45,10 +45,14 @@ import java.text.DecimalFormat;
|
|||||||
import java.text.DecimalFormatSymbols;
|
import java.text.DecimalFormatSymbols;
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
import java.text.spi.NumberFormatProvider;
|
import java.text.spi.NumberFormatProvider;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Currency;
|
import java.util.Currency;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import sun.text.resources.PluralRules;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Concrete implementation of the {@link java.text.spi.NumberFormatProvider
|
* 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 LocaleProviderAdapter.Type type;
|
||||||
private final Set<String> langtags;
|
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) {
|
public NumberFormatProviderImpl(LocaleProviderAdapter.Type type, Set<String> langtags) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.langtags = langtags;
|
this.langtags = langtags;
|
||||||
@ -271,8 +281,12 @@ public class NumberFormatProviderImpl extends NumberFormatProvider implements Av
|
|||||||
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(override);
|
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(override);
|
||||||
String[] cnPatterns = resource.getCNPatterns(formatStyle);
|
String[] cnPatterns = resource.getCNPatterns(formatStyle);
|
||||||
|
|
||||||
|
// plural rules
|
||||||
|
String pluralRules = rulesMap.getOrDefault(override.toString(),
|
||||||
|
rulesMap.getOrDefault(override.getLanguage(), ""));
|
||||||
|
|
||||||
CompactNumberFormat format = new CompactNumberFormat(numberPatterns[0],
|
CompactNumberFormat format = new CompactNumberFormat(numberPatterns[0],
|
||||||
symbols, cnPatterns);
|
symbols, cnPatterns, pluralRules);
|
||||||
return format;
|
return format;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -378,6 +378,14 @@ public class SPILocaleProviderAdapter extends AuxLocaleProviderAdapter {
|
|||||||
NumberFormatProvider nfp = getImpl(locale);
|
NumberFormatProvider nfp = getImpl(locale);
|
||||||
return nfp.getPercentInstance(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
|
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
|
* @test
|
||||||
* @bug 8177552 8217721
|
* @bug 8177552 8217721 8222756
|
||||||
* @summary Checks the functioning of compact number format
|
* @summary Checks the functioning of compact number format
|
||||||
* @modules jdk.localedata
|
* @modules jdk.localedata
|
||||||
* @run testng/othervm TestCompactNumber
|
* @run testng/othervm TestCompactNumber
|
||||||
@ -75,6 +75,12 @@ public class TestCompactNumber {
|
|||||||
private static final NumberFormat FORMAT_SE_SHORT = NumberFormat
|
private static final NumberFormat FORMAT_SE_SHORT = NumberFormat
|
||||||
.getCompactNumberInstance(new Locale("se"), NumberFormat.Style.SHORT);
|
.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")
|
@DataProvider(name = "format")
|
||||||
Object[][] compactFormatData() {
|
Object[][] compactFormatData() {
|
||||||
return new Object[][]{
|
return new Object[][]{
|
||||||
@ -248,7 +254,7 @@ public class TestCompactNumber {
|
|||||||
{FORMAT_CA_LONG, 999.99, "1 miler"},
|
{FORMAT_CA_LONG, 999.99, "1 miler"},
|
||||||
{FORMAT_CA_LONG, 99000, "99 milers"},
|
{FORMAT_CA_LONG, 99000, "99 milers"},
|
||||||
{FORMAT_CA_LONG, 330000, "330 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, 1000000, "1 mili\u00f3"},
|
||||||
{FORMAT_CA_LONG, new BigInteger("12345678901234567890"),
|
{FORMAT_CA_LONG, new BigInteger("12345678901234567890"),
|
||||||
"12345679 bilions"},
|
"12345679 bilions"},
|
||||||
@ -320,7 +326,20 @@ public class TestCompactNumber {
|
|||||||
// BigInteger
|
// BigInteger
|
||||||
{FORMAT_SE_SHORT, new BigInteger("-12345678901234567890"), "\u221212345679\u00a0bn"},
|
{FORMAT_SE_SHORT, new BigInteger("-12345678901234567890"), "\u221212345679\u00a0bn"},
|
||||||
// BigDecimal
|
// 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")
|
@DataProvider(name = "parse")
|
||||||
@ -409,7 +428,20 @@ public class TestCompactNumber {
|
|||||||
{FORMAT_SE_SHORT, "\u22128\u00a0mn", -8000000L, Long.class},
|
{FORMAT_SE_SHORT, "\u22128\u00a0mn", -8000000L, Long.class},
|
||||||
{FORMAT_SE_SHORT, "\u22128\u00a0dt", -8000L, Long.class},
|
{FORMAT_SE_SHORT, "\u22128\u00a0dt", -8000L, Long.class},
|
||||||
{FORMAT_SE_SHORT, "\u221212345679\u00a0bn", -1.2345679E19, Double.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")
|
@DataProvider(name = "exceptionParse")
|
||||||
@ -444,7 +476,20 @@ public class TestCompactNumber {
|
|||||||
// Take partial suffix "K" as 1000 for en_US_SHORT patterns
|
// Take partial suffix "K" as 1000 for en_US_SHORT patterns
|
||||||
{FORMAT_EN_US_SHORT, "12KM", 12000L},
|
{FORMAT_EN_US_SHORT, "12KM", 12000L},
|
||||||
// Invalid suffix
|
// 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")
|
@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.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
@ -22,7 +22,7 @@
|
|||||||
*/
|
*/
|
||||||
/*
|
/*
|
||||||
* @test
|
* @test
|
||||||
* @bug 8177552
|
* @bug 8177552 8222756
|
||||||
* @summary Checks the equals and hashCode method of CompactNumberFormat
|
* @summary Checks the equals and hashCode method of CompactNumberFormat
|
||||||
* @modules jdk.localedata
|
* @modules jdk.localedata
|
||||||
* @run testng/othervm TestEquality
|
* @run testng/othervm TestEquality
|
||||||
@ -48,9 +48,26 @@ public class TestEquality {
|
|||||||
// A custom compact instance with the same state as
|
// A custom compact instance with the same state as
|
||||||
// compact number instance of "en_US" locale with SHORT style
|
// compact number instance of "en_US" locale with SHORT style
|
||||||
String decimalPattern = "#,##0.###";
|
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);
|
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
|
// A compact instance created with different decimalPattern than cnf3
|
||||||
CompactNumberFormat cnf4 = new CompactNumberFormat("#,#0.0#", symbols, compactPatterns);
|
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…
x
Reference in New Issue
Block a user