OpenJDK / jdk / jdk
changeset 57247:7d732f6e17b2
8222756: Plural support in CompactNumberFormat
Reviewed-by: joehw, rriggs
line wrap: on
line diff
--- a/make/jdk/src/classes/build/tools/cldrconverter/AbstractLDMLHandler.java Thu Dec 05 16:43:06 2019 +0000 +++ b/make/jdk/src/classes/build/tools/cldrconverter/AbstractLDMLHandler.java Thu Dec 05 13:10:18 2019 -0800 @@ -157,9 +157,9 @@ } } - 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); } }
--- a/make/jdk/src/classes/build/tools/cldrconverter/Bundle.java Thu Dec 05 16:43:06 2019 +0000 +++ b/make/jdk/src/classes/build/tools/cldrconverter/Bundle.java Thu Dec 05 13:10:18 2019 -0800 @@ -242,14 +242,14 @@ 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
--- a/make/jdk/src/classes/build/tools/cldrconverter/CLDRConverter.java Thu Dec 05 16:43:06 2019 +0000 +++ b/make/jdk/src/classes/build/tools/cldrconverter/CLDRConverter.java Thu Dec 05 13:10:18 2019 -0800 @@ -70,6 +70,7 @@ 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 @@ 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 @@ 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 @@ // Generate Windows tzmappings generateWindowsTZMappings(); + + // Generate Plural rules + generatePluralRules(); } } @@ -451,6 +457,10 @@ // 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 @@ 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 @@ .forEach(System.out::println); } } +
--- a/make/jdk/src/classes/build/tools/cldrconverter/LDMLParseHandler.java Thu Dec 05 16:43:06 2019 +0000 +++ b/make/jdk/src/classes/build/tools/cldrconverter/LDMLParseHandler.java Thu Dec 05 13:10:18 2019 -0800 @@ -54,7 +54,6 @@ 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 @@ 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 @@ break; case "decimalFormatLength": currentStyle = ""; - compactCount = ""; putIfEntry(); break; case "currencyFormats":
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/make/jdk/src/classes/build/tools/cldrconverter/PluralsParseHandler.java Thu Dec 05 13:10:18 2019 -0800 @@ -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(); + } +}
--- a/make/jdk/src/classes/build/tools/cldrconverter/ResourceBundleGenerator.java Thu Dec 05 16:43:06 2019 +0000 +++ b/make/jdk/src/classes/build/tools/cldrconverter/ResourceBundleGenerator.java Thu Dec 05 13:10:18 2019 -0800 @@ -309,7 +309,7 @@ // 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); }); }
--- a/make/jdk/src/classes/build/tools/cldrconverter/StringListElement.java Thu Dec 05 16:43:06 2019 +0000 +++ b/make/jdk/src/classes/build/tools/cldrconverter/StringListElement.java Thu Dec 05 13:10:18 2019 -0800 @@ -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 @@ 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); } }
--- a/make/jdk/src/classes/build/tools/cldrconverter/StringListEntry.java Thu Dec 05 16:43:06 2019 +0000 +++ b/make/jdk/src/classes/build/tools/cldrconverter/StringListEntry.java Thu Dec 05 13:10:18 2019 -0800 @@ -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 @@ 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); } }
--- a/src/java.base/share/classes/java/text/CompactNumberFormat.java Thu Dec 05 16:43:06 2019 +0000 +++ b/src/java.base/share/classes/java/text/CompactNumberFormat.java Thu Dec 05 13:10:18 2019 -0800 @@ -32,11 +32,17 @@ 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 @@ * 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> - * * 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 @@ * 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 @@ * 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. @@ -299,6 +328,26 @@ 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 */ private static final String SPECIAL_PATTERN = "0"; @@ -328,20 +377,56 @@ * <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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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())); @@ -762,6 +847,18 @@ } /** + * 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} * field positions. @@ -1042,6 +1139,10 @@ * 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 @@ 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 @@ * @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 @@ // 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 @@ // 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 @@ 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 @@ 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 @@ pos.index = position; Number multiplier = computeParseMultiplier(text, pos, gotPositive ? matchedPosPrefix : matchedNegPrefix, - status, gotPositive, gotNegative); + status, gotPositive, gotNegative, num); if (multiplier.longValue() == -1L) { return null; @@ -1530,6 +1709,33 @@ } /** + * 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 @@ */ 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 @@ 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 @@ * 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 @@ 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 @@ && 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 @@ @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 @@ 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; + } }
--- a/src/java.base/share/classes/java/text/spi/NumberFormatProvider.java Thu Dec 05 16:43:06 2019 +0000 +++ b/src/java.base/share/classes/java/text/spi/NumberFormatProvider.java Thu Dec 05 13:10:18 2019 -0800 @@ -117,7 +117,8 @@ * {@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 @@ * 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,
--- a/src/java.base/share/classes/sun/util/locale/provider/NumberFormatProviderImpl.java Thu Dec 05 16:43:06 2019 +0000 +++ b/src/java.base/share/classes/sun/util/locale/provider/NumberFormatProviderImpl.java Thu Dec 05 13:10:18 2019 -0800 @@ -45,10 +45,14 @@ 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 @@ 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 @@ 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; }
--- a/src/java.base/share/classes/sun/util/locale/provider/SPILocaleProviderAdapter.java Thu Dec 05 16:43:06 2019 +0000 +++ b/src/java.base/share/classes/sun/util/locale/provider/SPILocaleProviderAdapter.java Thu Dec 05 13:10:18 2019 -0800 @@ -378,6 +378,14 @@ 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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/text/Format/CompactNumberFormat/SPIProviderTest.java Thu Dec 05 13:10:18 2019 -0800 @@ -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); + } + } +}
--- a/test/jdk/java/text/Format/CompactNumberFormat/TestCompactNumber.java Thu Dec 05 16:43:06 2019 +0000 +++ b/test/jdk/java/text/Format/CompactNumberFormat/TestCompactNumber.java Thu Dec 05 13:10:18 2019 -0800 @@ -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 @@ 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 @@ {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 @@ // 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 @@ {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 @@ // 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")
--- a/test/jdk/java/text/Format/CompactNumberFormat/TestEquality.java Thu Dec 05 16:43:06 2019 +0000 +++ b/test/jdk/java/text/Format/CompactNumberFormat/TestEquality.java Thu Dec 05 13:10:18 2019 -0800 @@ -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 @@ // 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);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/text/Format/CompactNumberFormat/TestPlurals.java Thu Dec 05 13:10:18 2019 -0800 @@ -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); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/text/Format/CompactNumberFormat/provider/module-info.java Thu Dec 05 13:10:18 2019 -0800 @@ -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; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/text/Format/CompactNumberFormat/provider/test/NumberFormatProviderImpl.java Thu Dec 05 13:10:18 2019 -0800 @@ -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; + } +}