changeset 57247:7d732f6e17b2

8222756: Plural support in CompactNumberFormat Reviewed-by: joehw, rriggs
author naoto
date Thu, 05 Dec 2019 13:10:18 -0800
parents 1d3c5da689d0
children a112b0a6d91e 366c0f357ee6
files make/jdk/src/classes/build/tools/cldrconverter/AbstractLDMLHandler.java make/jdk/src/classes/build/tools/cldrconverter/Bundle.java make/jdk/src/classes/build/tools/cldrconverter/CLDRConverter.java make/jdk/src/classes/build/tools/cldrconverter/LDMLParseHandler.java make/jdk/src/classes/build/tools/cldrconverter/PluralsParseHandler.java make/jdk/src/classes/build/tools/cldrconverter/ResourceBundleGenerator.java make/jdk/src/classes/build/tools/cldrconverter/StringListElement.java make/jdk/src/classes/build/tools/cldrconverter/StringListEntry.java src/java.base/share/classes/java/text/CompactNumberFormat.java src/java.base/share/classes/java/text/spi/NumberFormatProvider.java src/java.base/share/classes/sun/util/locale/provider/NumberFormatProviderImpl.java src/java.base/share/classes/sun/util/locale/provider/SPILocaleProviderAdapter.java test/jdk/java/text/Format/CompactNumberFormat/SPIProviderTest.java test/jdk/java/text/Format/CompactNumberFormat/TestCompactNumber.java test/jdk/java/text/Format/CompactNumberFormat/TestEquality.java test/jdk/java/text/Format/CompactNumberFormat/TestPlurals.java test/jdk/java/text/Format/CompactNumberFormat/provider/module-info.java test/jdk/java/text/Format/CompactNumberFormat/provider/test/NumberFormatProviderImpl.java
diffstat 18 files changed, 1101 insertions(+), 130 deletions(-) [+]
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 &#92;uFFFE, &#92;uFFFF, and
- *      <a href = "DecimalFormat.html#special_pattern_character">special characters</a>
- * <i>Suffix:</i>
- *      Any Unicode characters except &#92;uFFFE, &#92;uFFFF, and
- *      <a href = "DecimalFormat.html#special_pattern_character">special characters</a>
- * <i>MinimumInteger:</i>
- *      0
- *      0 <i>MinimumInteger</i>
- * </pre></blockquote>
- *
  * 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 &#92;uFFFE, &#92;uFFFF, and
+ *      <a href = "DecimalFormat.html#special_pattern_character">special characters</a>.
+ * <i>Suffix:</i>
+ *      Any Unicode characters except &#92;uFFFE, &#92;uFFFF, and
+ *      <a href = "DecimalFormat.html#special_pattern_character">special characters</a>.
+ * <i>MinimumInteger:</i>
+ *      0
+ *      0 <i>MinimumInteger</i>
+ * </pre></blockquote>
+ *
  * <h2>Formatting</h2>
  * The default formatting behavior returns a formatted string with no fractional
  * digits, however users can use the {@link #setMinimumFractionDigits(int)}
@@ -207,25 +236,25 @@
      * 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;
+    }
+}