changeset 57541:d8fbcf86ce72

8227313: Support monetary grouping separator in DecimalFormat/DecimalFormatSymbols Reviewed-by: joehw
author naoto
date Mon, 06 Jan 2020 10:31:20 -0800
parents b9d1ce20dd4b
children b0a5beaac2b3
files make/jdk/src/classes/build/tools/cldrconverter/Bundle.java make/jdk/src/classes/build/tools/cldrconverter/LDMLParseHandler.java src/java.base/share/classes/java/text/DecimalFormat.java src/java.base/share/classes/java/text/DecimalFormatSymbols.java test/jdk/java/text/Format/NumberFormat/CurrencyFormat.java test/jdk/java/text/Format/NumberFormat/NumberRegression.java test/jdk/sun/text/resources/LocaleData.cldr
diffstat 7 files changed, 371 insertions(+), 193 deletions(-) [+]
line wrap: on
line diff
--- a/make/jdk/src/classes/build/tools/cldrconverter/Bundle.java	Sun Jan 05 21:04:39 2020 -0800
+++ b/make/jdk/src/classes/build/tools/cldrconverter/Bundle.java	Mon Jan 06 10:31:20 2020 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2020, 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
@@ -72,7 +72,9 @@
         "NumberElements/exponential",
         "NumberElements/permille",
         "NumberElements/infinity",
-        "NumberElements/nan"
+        "NumberElements/nan",
+        "NumberElements/currencyDecimal",
+        "NumberElements/currencyGroup",
     };
 
     private final static String[] TIME_PATTERN_KEYS = {
@@ -810,7 +812,10 @@
                         assert keys == NUMBER_ELEMENT_KEYS;
                         if (key.endsWith("/pattern")) {
                             numArray[idx] = "#";
-                        } else {
+                        } else if (!key.endsWith("currencyDecimal") &&
+                                   !key.endsWith("currencyGroup")) {
+                            // throw error unless it is for "currencyDecimal/Group",
+                            // which may be missing.
                             throw new InternalError("NumberElements: null for " +
                                                     key + ", id: " + id);
                         }
--- a/make/jdk/src/classes/build/tools/cldrconverter/LDMLParseHandler.java	Sun Jan 05 21:04:39 2020 -0800
+++ b/make/jdk/src/classes/build/tools/cldrconverter/LDMLParseHandler.java	Mon Jan 06 10:31:20 2020 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2020, 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
@@ -644,19 +644,13 @@
             }
             break;
         case "decimal":
+        case "group":
+        case "currencyDecimal":
+        case "currencyGroup":
             // for FormatData
             // copy string for later assembly into NumberElements
             if (currentContainer.getqName().equals("symbols")) {
-                pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/decimal");
-            } else {
-                pushIgnoredContainer(qName);
-            }
-            break;
-        case "group":
-            // for FormatData
-            // copy string for later assembly into NumberElements
-            if (currentContainer.getqName().equals("symbols")) {
-                pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/group");
+                pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/" + qName);
             } else {
                 pushIgnoredContainer(qName);
             }
--- a/src/java.base/share/classes/java/text/DecimalFormat.java	Sun Jan 05 21:04:39 2020 -0800
+++ b/src/java.base/share/classes/java/text/DecimalFormat.java	Mon Jan 06 10:31:20 2020 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1996, 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1996, 2020, 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
@@ -136,14 +136,14 @@
  * the same behavior as {@code "#,##0.0#;(#,##0.0#)"}.
  *
  * <p>The prefixes, suffixes, and various symbols used for infinity, digits,
- * thousands separators, decimal separators, etc. may be set to arbitrary
+ * grouping separators, decimal separators, etc. may be set to arbitrary
  * values, and they will appear properly during formatting.  However, care must
  * be taken that the symbols and strings do not conflict, or parsing will be
  * unreliable.  For example, either the positive and negative prefixes or the
  * suffixes must be distinct for {@code DecimalFormat.parse()} to be able
  * to distinguish positive from negative values.  (If they are identical, then
  * {@code DecimalFormat} will behave as if no negative subpattern was
- * specified.)  Another example is that the decimal separator and thousands
+ * specified.)  Another example is that the decimal separator and grouping
  * separator should be distinct characters, or parsing will be impossible.
  *
  * <p>The grouping separator is commonly used for thousands, but in some
@@ -203,7 +203,7 @@
  *          <th scope="row">{@code ,}
  *          <td>Number
  *          <td>Yes
- *          <td>Grouping separator
+ *          <td>Grouping separator or monetary grouping separator
  *     <tr style="vertical-align: top">
  *          <th scope="row">{@code E}
  *          <td>Number
@@ -231,8 +231,8 @@
  *          <td>No
  *          <td>Currency sign, replaced by currency symbol.  If
  *              doubled, replaced by international currency symbol.
- *              If present in a pattern, the monetary decimal separator
- *              is used instead of the decimal separator.
+ *              If present in a pattern, the monetary decimal/grouping separators
+ *              are used instead of the decimal/grouping separators.
  *     <tr style="vertical-align:top">
  *          <th scope="row">{@code '}
  *          <td>Prefix or suffix
@@ -1103,7 +1103,9 @@
             // Sets up the locale specific constants used when formatting.
             // '0' is our default representation of zero.
             fastPathData.zeroDelta = symbols.getZeroDigit() - '0';
-            fastPathData.groupingChar = symbols.getGroupingSeparator();
+            fastPathData.groupingChar = isCurrencyFormat ?
+                    symbols.getMonetaryGroupingSeparator() :
+                    symbols.getGroupingSeparator();
 
             // Sets up fractional constants related to currency/decimal pattern.
             fastPathData.fractionalMaxIntBound = (isCurrencyFormat)
@@ -1774,7 +1776,9 @@
             int maxIntDigits, int minIntDigits,
             int maxFraDigits, int minFraDigits) {
 
-        char grouping = symbols.getGroupingSeparator();
+        char grouping = isCurrencyFormat ?
+                symbols.getMonetaryGroupingSeparator() :
+                symbols.getGroupingSeparator();
         char zero = symbols.getZeroDigit();
         int zeroDelta = zero - '0'; // '0' is the DigitList representation of zero
 
@@ -2393,7 +2397,9 @@
             char decimal = isCurrencyFormat ?
                     symbols.getMonetaryDecimalSeparator() :
                     symbols.getDecimalSeparator();
-            char grouping = symbols.getGroupingSeparator();
+            char grouping = isCurrencyFormat ?
+                    symbols.getMonetaryGroupingSeparator() :
+                    symbols.getGroupingSeparator();
             String exponentString = symbols.getExponentSeparator();
             boolean sawDecimal = false;
             boolean sawExponent = false;
@@ -4061,7 +4067,7 @@
 
     /**
      * True if this object represents a currency format.  This determines
-     * whether the monetary decimal separator is used instead of the normal one.
+     * whether the monetary decimal/grouping separators are used instead of the normal ones.
      */
     private transient boolean isCurrencyFormat = false;
 
@@ -4346,8 +4352,8 @@
      * The CURRENCY_SIGN is the standard Unicode symbol for currency.  It
      * is used in patterns and substituted with either the currency symbol,
      * or if it is doubled, with the international currency symbol.  If the
-     * CURRENCY_SIGN is seen in a pattern, then the decimal separator is
-     * replaced with the monetary decimal separator.
+     * CURRENCY_SIGN is seen in a pattern, then the decimal/grouping separators
+     * are replaced with the monetary decimal/grouping separators.
      *
      * The CURRENCY_SIGN is not localized.
      */
--- a/src/java.base/share/classes/java/text/DecimalFormatSymbols.java	Sun Jan 05 21:04:39 2020 -0800
+++ b/src/java.base/share/classes/java/text/DecimalFormatSymbols.java	Mon Jan 06 10:31:20 2020 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1996, 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1996, 2020, 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
@@ -202,11 +202,12 @@
      * @param zeroDigit the character used for zero
      */
     public void setZeroDigit(char zeroDigit) {
+        hashCode = 0;
         this.zeroDigit = zeroDigit;
     }
 
     /**
-     * Gets the character used for thousands separator. Different for French, etc.
+     * Gets the character used for grouping separator. Different for French, etc.
      *
      * @return the grouping separator
      */
@@ -215,11 +216,12 @@
     }
 
     /**
-     * Sets the character used for thousands separator. Different for French, etc.
+     * Sets the character used for grouping separator. Different for French, etc.
      *
      * @param groupingSeparator the grouping separator
      */
     public void setGroupingSeparator(char groupingSeparator) {
+        hashCode = 0;
         this.groupingSeparator = groupingSeparator;
     }
 
@@ -238,6 +240,7 @@
      * @param decimalSeparator the character used for decimal sign
      */
     public void setDecimalSeparator(char decimalSeparator) {
+        hashCode = 0;
         this.decimalSeparator = decimalSeparator;
     }
 
@@ -256,45 +259,12 @@
      * @param perMill the character used for per mille sign
      */
     public void setPerMill(char perMill) {
+        hashCode = 0;
         this.perMill = perMill;
         this.perMillText = Character.toString(perMill);
     }
 
     /**
-     * Gets the string used for per mille sign. Different for Arabic, etc.
-     *
-     * @return the string used for per mille sign
-     * @since 13
-     */
-    String getPerMillText() {
-        return perMillText;
-    }
-
-    /**
-     * Sets the string used for per mille sign. Different for Arabic, etc.
-     *
-     * Setting the {@code perMillText} affects the return value of
-     * {@link #getPerMill()}, in which the first non-format character of
-     * {@code perMillText} is returned.
-     *
-     * @param perMillText the string used for per mille sign
-     * @throws NullPointerException if {@code perMillText} is null
-     * @throws IllegalArgumentException if {@code perMillText} is an empty string
-     * @see #getPerMill()
-     * @see #getPerMillText()
-     * @since 13
-     */
-    void setPerMillText(String perMillText) {
-        Objects.requireNonNull(perMillText);
-        if (perMillText.isEmpty()) {
-            throw new IllegalArgumentException("Empty argument string");
-        }
-
-        this.perMillText = perMillText;
-        this.perMill = findNonFormatChar(perMillText, '\u2030');
-    }
-
-    /**
      * Gets the character used for percent sign. Different for Arabic, etc.
      *
      * @return the character used for percent sign
@@ -309,45 +279,12 @@
      * @param percent the character used for percent sign
      */
     public void setPercent(char percent) {
+        hashCode = 0;
         this.percent = percent;
         this.percentText = Character.toString(percent);
     }
 
     /**
-     * Gets the string used for percent sign. Different for Arabic, etc.
-     *
-     * @return the string used for percent sign
-     * @since 13
-     */
-    String getPercentText() {
-        return percentText;
-    }
-
-    /**
-     * Sets the string used for percent sign. Different for Arabic, etc.
-     *
-     * Setting the {@code percentText} affects the return value of
-     * {@link #getPercent()}, in which the first non-format character of
-     * {@code percentText} is returned.
-     *
-     * @param percentText the string used for percent sign
-     * @throws NullPointerException if {@code percentText} is null
-     * @throws IllegalArgumentException if {@code percentText} is an empty string
-     * @see #getPercent()
-     * @see #getPercentText()
-     * @since 13
-     */
-    void setPercentText(String percentText) {
-        Objects.requireNonNull(percentText);
-        if (percentText.isEmpty()) {
-            throw new IllegalArgumentException("Empty argument string");
-        }
-
-        this.percentText = percentText;
-        this.percent = findNonFormatChar(percentText, '%');
-    }
-
-    /**
      * Gets the character used for a digit in a pattern.
      *
      * @return the character used for a digit in a pattern
@@ -362,6 +299,7 @@
      * @param digit the character used for a digit in a pattern
      */
     public void setDigit(char digit) {
+        hashCode = 0;
         this.digit = digit;
     }
 
@@ -382,6 +320,7 @@
      * @param patternSeparator the pattern separator
      */
     public void setPatternSeparator(char patternSeparator) {
+        hashCode = 0;
         this.patternSeparator = patternSeparator;
     }
 
@@ -402,6 +341,7 @@
      * @param infinity the string representing infinity
      */
     public void setInfinity(String infinity) {
+        hashCode = 0;
         this.infinity = infinity;
     }
 
@@ -422,6 +362,7 @@
      * @param NaN the string representing "not a number"
      */
     public void setNaN(String NaN) {
+        hashCode = 0;
         this.NaN = NaN;
     }
 
@@ -444,50 +385,12 @@
      * @param minusSign the character representing minus sign
      */
     public void setMinusSign(char minusSign) {
+        hashCode = 0;
         this.minusSign = minusSign;
         this.minusSignText = Character.toString(minusSign);
     }
 
     /**
-     * Gets the string used to represent minus sign. If no explicit
-     * negative format is specified, one is formed by prefixing
-     * minusSignText to the positive format.
-     *
-     * @return the string representing minus sign
-     * @since 13
-     */
-    String getMinusSignText() {
-        return minusSignText;
-    }
-
-    /**
-     * Sets the string used to represent minus sign. If no explicit
-     * negative format is specified, one is formed by prefixing
-     * minusSignText to the positive format.
-     *
-     * Setting the {@code minusSignText} affects the return value of
-     * {@link #getMinusSign()}, in which the first non-format character of
-     * {@code minusSignText} is returned.
-     *
-     * @param minusSignText the character representing minus sign
-     * @throws NullPointerException if {@code minusSignText} is null
-     * @throws IllegalArgumentException if {@code minusSignText} is an
-     *  empty string
-     * @see #getMinusSign()
-     * @see #getMinusSignText()
-     * @since 13
-     */
-    void setMinusSignText(String minusSignText) {
-        Objects.requireNonNull(minusSignText);
-        if (minusSignText.isEmpty()) {
-            throw new IllegalArgumentException("Empty argument string");
-        }
-
-        this.minusSignText = minusSignText;
-        this.minusSign = findNonFormatChar(minusSignText, '-');
-    }
-
-    /**
      * Returns the currency symbol for the currency of these
      * DecimalFormatSymbols in their locale.
      *
@@ -510,6 +413,7 @@
     public void setCurrencySymbol(String currency)
     {
         initializeCurrency(locale);
+        hashCode = 0;
         currencySymbol = currency;
     }
 
@@ -545,6 +449,7 @@
     public void setInternationalCurrencySymbol(String currencyCode)
     {
         initializeCurrency(locale);
+        hashCode = 0;
         intlCurrencySymbol = currencyCode;
         currency = null;
         if (currencyCode != null) {
@@ -586,6 +491,7 @@
             throw new NullPointerException();
         }
         initializeCurrency(locale);
+        hashCode = 0;
         this.currency = currency;
         intlCurrencySymbol = currency.getCurrencyCode();
         currencySymbol = currency.getSymbol(locale);
@@ -611,21 +517,10 @@
      */
     public void setMonetaryDecimalSeparator(char sep)
     {
+        hashCode = 0;
         monetarySeparator = sep;
     }
 
-    //------------------------------------------------------------
-    // BEGIN   Package Private methods ... to be made public later
-    //------------------------------------------------------------
-
-    /**
-     * Returns the character used to separate the mantissa from the exponent.
-     */
-    char getExponentialSymbol()
-    {
-        return exponential;
-    }
-
     /**
      * Returns the string used to separate the mantissa from the exponent.
      * Examples: "x10^" for 1.23x10^4, "E" for 1.23E4.
@@ -640,14 +535,6 @@
     }
 
     /**
-     * Sets the character used to separate the mantissa from the exponent.
-     */
-    void setExponentialSymbol(char exp)
-    {
-        exponential = exp;
-    }
-
-    /**
      * Sets the string used to separate the mantissa from the exponent.
      * Examples: "x10^" for 1.23x10^4, "E" for 1.23E4.
      *
@@ -661,9 +548,166 @@
         if (exp == null) {
             throw new NullPointerException();
         }
+        hashCode = 0;
         exponentialSeparator = exp;
     }
 
+    /**
+     * Gets the character used for grouping separator for currencies.
+     * May be different from {@code grouping separator} in some locales,
+     * e.g, German in Austria.
+     *
+     * @return the monetary grouping separator
+     * @since 15
+     */
+    public char getMonetaryGroupingSeparator() {
+        return monetaryGroupingSeparator;
+    }
+
+    /**
+     * Sets the character used for grouping separator for currencies.
+     * Invocation of this method will not affect the normal
+     * {@code grouping separator}.
+     *
+     * @param monetaryGroupingSeparator the monetary grouping separator
+     * @see #setGroupingSeparator(char)
+     * @since 15
+     */
+    public void setMonetaryGroupingSeparator(char monetaryGroupingSeparator)
+    {
+        hashCode = 0;
+        this.monetaryGroupingSeparator = monetaryGroupingSeparator;
+    }
+
+    //------------------------------------------------------------
+    // BEGIN   Package Private methods ... to be made public later
+    //------------------------------------------------------------
+
+    /**
+     * Returns the character used to separate the mantissa from the exponent.
+     */
+    char getExponentialSymbol()
+    {
+        return exponential;
+    }
+
+    /**
+     * Sets the character used to separate the mantissa from the exponent.
+     */
+    void setExponentialSymbol(char exp)
+    {
+        exponential = exp;
+    }
+
+    /**
+     * Gets the string used for per mille sign. Different for Arabic, etc.
+     *
+     * @return the string used for per mille sign
+     * @since 13
+     */
+    String getPerMillText() {
+        return perMillText;
+    }
+
+    /**
+     * Sets the string used for per mille sign. Different for Arabic, etc.
+     *
+     * Setting the {@code perMillText} affects the return value of
+     * {@link #getPerMill()}, in which the first non-format character of
+     * {@code perMillText} is returned.
+     *
+     * @param perMillText the string used for per mille sign
+     * @throws NullPointerException if {@code perMillText} is null
+     * @throws IllegalArgumentException if {@code perMillText} is an empty string
+     * @see #getPerMill()
+     * @see #getPerMillText()
+     * @since 13
+     */
+    void setPerMillText(String perMillText) {
+        Objects.requireNonNull(perMillText);
+        if (perMillText.isEmpty()) {
+            throw new IllegalArgumentException("Empty argument string");
+        }
+
+        hashCode = 0;
+        this.perMillText = perMillText;
+        this.perMill = findNonFormatChar(perMillText, '\u2030');
+    }
+
+    /**
+     * Gets the string used for percent sign. Different for Arabic, etc.
+     *
+     * @return the string used for percent sign
+     * @since 13
+     */
+    String getPercentText() {
+        return percentText;
+    }
+
+    /**
+     * Sets the string used for percent sign. Different for Arabic, etc.
+     *
+     * Setting the {@code percentText} affects the return value of
+     * {@link #getPercent()}, in which the first non-format character of
+     * {@code percentText} is returned.
+     *
+     * @param percentText the string used for percent sign
+     * @throws NullPointerException if {@code percentText} is null
+     * @throws IllegalArgumentException if {@code percentText} is an empty string
+     * @see #getPercent()
+     * @see #getPercentText()
+     * @since 13
+     */
+    void setPercentText(String percentText) {
+        Objects.requireNonNull(percentText);
+        if (percentText.isEmpty()) {
+            throw new IllegalArgumentException("Empty argument string");
+        }
+
+        hashCode = 0;
+        this.percentText = percentText;
+        this.percent = findNonFormatChar(percentText, '%');
+    }
+
+    /**
+     * Gets the string used to represent minus sign. If no explicit
+     * negative format is specified, one is formed by prefixing
+     * minusSignText to the positive format.
+     *
+     * @return the string representing minus sign
+     * @since 13
+     */
+    String getMinusSignText() {
+        return minusSignText;
+    }
+
+    /**
+     * Sets the string used to represent minus sign. If no explicit
+     * negative format is specified, one is formed by prefixing
+     * minusSignText to the positive format.
+     *
+     * Setting the {@code minusSignText} affects the return value of
+     * {@link #getMinusSign()}, in which the first non-format character of
+     * {@code minusSignText} is returned.
+     *
+     * @param minusSignText the character representing minus sign
+     * @throws NullPointerException if {@code minusSignText} is null
+     * @throws IllegalArgumentException if {@code minusSignText} is an
+     *  empty string
+     * @see #getMinusSign()
+     * @see #getMinusSignText()
+     * @since 13
+     */
+    void setMinusSignText(String minusSignText) {
+        Objects.requireNonNull(minusSignText);
+        if (minusSignText.isEmpty()) {
+            throw new IllegalArgumentException("Empty argument string");
+        }
+
+        hashCode = 0;
+        this.minusSignText = minusSignText;
+        this.minusSign = findNonFormatChar(minusSignText, '-');
+    }
 
     //------------------------------------------------------------
     // END     Package Private methods ... to be made public later
@@ -692,35 +736,57 @@
         if (getClass() != obj.getClass()) return false;
         DecimalFormatSymbols other = (DecimalFormatSymbols) obj;
         return (zeroDigit == other.zeroDigit &&
-        groupingSeparator == other.groupingSeparator &&
-        decimalSeparator == other.decimalSeparator &&
-        percent == other.percent &&
-        percentText.equals(other.percentText) &&
-        perMill == other.perMill &&
-        perMillText.equals(other.perMillText) &&
-        digit == other.digit &&
-        minusSign == other.minusSign &&
-        minusSignText.equals(other.minusSignText) &&
-        patternSeparator == other.patternSeparator &&
-        infinity.equals(other.infinity) &&
-        NaN.equals(other.NaN) &&
-        getCurrencySymbol().equals(other.getCurrencySymbol()) && // possible currency init occurs here
-        intlCurrencySymbol.equals(other.intlCurrencySymbol) &&
-        currency == other.currency &&
-        monetarySeparator == other.monetarySeparator &&
-        exponentialSeparator.equals(other.exponentialSeparator) &&
-        locale.equals(other.locale));
+            groupingSeparator == other.groupingSeparator &&
+            decimalSeparator == other.decimalSeparator &&
+            percent == other.percent &&
+            percentText.equals(other.percentText) &&
+            perMill == other.perMill &&
+            perMillText.equals(other.perMillText) &&
+            digit == other.digit &&
+            minusSign == other.minusSign &&
+            minusSignText.equals(other.minusSignText) &&
+            patternSeparator == other.patternSeparator &&
+            infinity.equals(other.infinity) &&
+            NaN.equals(other.NaN) &&
+            getCurrencySymbol().equals(other.getCurrencySymbol()) && // possible currency init occurs here
+            intlCurrencySymbol.equals(other.intlCurrencySymbol) &&
+            currency == other.currency &&
+            monetarySeparator == other.monetarySeparator &&
+            monetaryGroupingSeparator == other.monetaryGroupingSeparator &&
+            exponentialSeparator.equals(other.exponentialSeparator) &&
+            locale.equals(other.locale));
     }
 
     /**
      * Override hashCode.
      */
+    private volatile int hashCode;
     @Override
     public int hashCode() {
-            int result = zeroDigit;
-            result = result * 37 + groupingSeparator;
-            result = result * 37 + decimalSeparator;
-            return result;
+        if (hashCode == 0) {
+            hashCode = Objects.hash(
+                zeroDigit,
+                groupingSeparator,
+                decimalSeparator,
+                percent,
+                percentText,
+                perMill,
+                perMillText,
+                digit,
+                minusSign,
+                minusSignText,
+                patternSeparator,
+                infinity,
+                NaN,
+                getCurrencySymbol(), // possible currency init occurs here
+                intlCurrencySymbol,
+                currency,
+                monetarySeparator,
+                monetaryGroupingSeparator,
+                exponentialSeparator,
+                locale);
+        }
+        return hashCode;
     }
 
     /**
@@ -759,14 +825,15 @@
         infinity  = numberElements[9];
         NaN = numberElements[10];
 
+        // monetary decimal/grouping separators may be missing in resource bundles
+        monetarySeparator = numberElements.length < 12 || numberElements[11].isEmpty() ?
+            decimalSeparator : numberElements[11].charAt(0);
+        monetaryGroupingSeparator = numberElements.length < 13 || numberElements[12].isEmpty() ?
+            groupingSeparator : numberElements[12].charAt(0);
+
         // maybe filled with previously cached values, or null.
         intlCurrencySymbol = (String) data[1];
         currencySymbol = (String) data[2];
-
-        // Currently the monetary decimal separator is the same as the
-        // standard decimal separator for all locales that we support.
-        // If that changes, add a new entry to NumberElements.
-        monetarySeparator = decimalSeparator;
     }
 
     /**
@@ -844,6 +911,8 @@
      * {@code perMillText}, {@code percentText}, and
      * {@code minusSignText} using {@code perMill}, {@code percent}, and
      * {@code minusSign} respectively.
+     * If {@code serialVersionOnStream} is less than 5, it initializes
+     * {@code monetaryGroupingSeparator} using {@code groupingSeparator}.
      * Sets {@code serialVersionOnStream} back to the maximum allowed value so that
      * default serialization will work properly if this object is streamed out again.
      * Initializes the currency from the intlCurrencySymbol field.
@@ -886,6 +955,10 @@
                     "per mille, and/or minus sign disagree.");
             }
         }
+        if (serialVersionOnStream < 5) {
+            // didn't have monetaryGroupingSeparator. Create one using groupingSeparator
+            monetaryGroupingSeparator = groupingSeparator;
+        }
 
         serialVersionOnStream = currentSerialVersion;
 
@@ -907,7 +980,7 @@
     private  char    zeroDigit;
 
     /**
-     * Character used for thousands separator.
+     * Character used for grouping separator.
      *
      * @serial
      * @see #getGroupingSeparator
@@ -1063,6 +1136,14 @@
      */
     private  String minusSignText;
 
+    /**
+     * The grouping separator used when formatting currency values.
+     *
+     * @serial
+     * @since 15
+     */
+    private  char    monetaryGroupingSeparator;
+
     // currency; only the ISO code is serialized.
     private transient Currency currency;
     private transient volatile boolean currencyInitialized;
@@ -1079,7 +1160,8 @@
     // - 3 for version from J2SE 1.6, which includes exponentialSeparator field.
     // - 4 for version from Java SE 13, which includes perMillText, percentText,
     //      and minusSignText field.
-    private static final int currentSerialVersion = 4;
+    // - 5 for version from Java SE 15, which includes monetaryGroupingSeparator.
+    private static final int currentSerialVersion = 5;
 
     /**
      * Describes the version of {@code DecimalFormatSymbols} present on the stream.
@@ -1096,7 +1178,9 @@
      * <li><b>4</b>: Versions written by Java SE 13 or later, which include
      *      new {@code perMillText}, {@code percentText}, and
      *      {@code minusSignText} field.
-     * </ul>
+     * <li><b>5</b>: Versions written by Java SE 15 or later, which include
+     *      new {@code monetaryGroupingSeparator} field.
+     * * </ul>
      * When streaming out a {@code DecimalFormatSymbols}, the most recent format
      * (corresponding to the highest allowable {@code serialVersionOnStream})
      * is always written.
--- a/test/jdk/java/text/Format/NumberFormat/CurrencyFormat.java	Sun Jan 05 21:04:39 2020 -0800
+++ b/test/jdk/java/text/Format/NumberFormat/CurrencyFormat.java	Mon Jan 06 10:31:20 2020 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2001, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2001, 2020, 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
@@ -23,10 +23,11 @@
 
 /*
  * @test
- * @bug 4290801 4942982 5102005 8008577 8021121 8210153
+ * @bug 4290801 4942982 5102005 8008577 8021121 8210153 8227313
  * @summary Basic tests for currency formatting.
  * @modules jdk.localedata
- * @run main/othervm -Djava.locale.providers=JRE,SPI CurrencyFormat
+ * @run main/othervm -Djava.locale.providers=COMPAT CurrencyFormat COMPAT
+ * @run main/othervm -Djava.locale.providers=CLDR CurrencyFormat CLDR
  */
 
 import java.io.File;
@@ -42,7 +43,10 @@
 
 public class CurrencyFormat {
 
+    private static boolean isCompat;
+
     public static void main(String[] args) throws Exception {
+        isCompat = "COMPAT".equals(args[0]);
         testFormatting();
         testSymbols();
     }
@@ -54,7 +58,10 @@
             Locale.JAPAN,
             Locale.GERMANY,
             Locale.ITALY,
-            new Locale("it", "IT", "EURO") };
+            new Locale("it", "IT", "EURO"),
+            Locale.forLanguageTag("de-AT"),
+            Locale.forLanguageTag("fr-CH"),
+        };
         Currency[] currencies = {
             null,
             Currency.getInstance("USD"),
@@ -68,6 +75,17 @@
             {"1.234,56 \u20AC", "1.234,56 USD", "1.235 JPY", "1.234,56 DM", "1.234,56 \u20AC"},
             {"\u20AC 1.234,56", "USD 1.234,56", "JPY 1.235", "DEM 1.234,56", "\u20AC 1.234,56"},
             {"\u20AC 1.234,56", "USD 1.234,56", "JPY 1.235", "DEM 1.234,56", "\u20AC 1.234,56"},
+            {"\u20AC 1.234,56", "USD 1.234,56", "JPY 1.235", "DEM 1.234,56", "\u20AC 1.234,56"},
+            {"SFr. 1'234.56", "USD 1'234.56", "JPY 1'235", "DEM 1'234.56", "EUR 1'234.56"},
+        };
+        String[][] expecteds_cldr = {
+            {"$1,234.56", "$1,234.56", "\u00a51,235", "DEM1,234.56", "\u20ac1,234.56"},
+            {"\uFFE51,235", "$1,234.56", "\uFFE51,235", "DEM1,234.56", "\u20ac1,234.56"},
+            {"1.234,56\u00a0\u20ac", "1.234,56\u00a0$", "1.235\u00a0\u00a5", "1.234,56\u00a0DM", "1.234,56\u00a0\u20ac"},
+            {"1.234,56\u00a0\u20ac", "1.234,56\u00a0USD", "1.235\u00a0JPY", "1.234,56\u00a0DEM", "1.234,56\u00a0\u20ac"},
+            {"1.234,56\u00a0\u20ac", "1.234,56\u00a0USD", "1.235\u00a0JPY", "1.234,56\u00a0DEM", "1.234,56\u00a0\u20ac"},
+            {"\u20ac\u00a01.234,56", "$\u00a01.234,56", "\u00a5\u00a01.235", "DM\u00a01.234,56", "\u20ac\u00a01.234,56"},
+            {"1\u202f234.56\u00a0CHF", "1\u202f234.56\u00a0$US", "1\u202f235\u00a0JPY", "1\u202f234.56\u00a0DEM", "1\u202f234.56\u00a0\u20ac"},
         };
 
         for (int i = 0; i < locales.length; i++) {
@@ -75,7 +93,7 @@
             NumberFormat format = NumberFormat.getCurrencyInstance(locale);
             for (int j = 0; j < currencies.length; j++) {
                 Currency currency = currencies[j];
-                String expected = expecteds[i][j];
+                String expected = isCompat ? expecteds[i][j] : expecteds_cldr[i][j];
                 if (currency != null) {
                     format.setCurrency(currency);
                     int digits = currency.getDefaultFractionDigits();
@@ -99,6 +117,11 @@
     }
 
     static void testSymbols() throws Exception {
+        if (!isCompat) {
+            // For COMPAT only.
+            return;
+        }
+
         FileInputStream stream = new FileInputStream(new File(System.getProperty("test.src", "."), "CurrencySymbols.properties"));
         Properties props = new Properties();
         props.load(stream);
--- a/test/jdk/java/text/Format/NumberFormat/NumberRegression.java	Sun Jan 05 21:04:39 2020 -0800
+++ b/test/jdk/java/text/Format/NumberFormat/NumberRegression.java	Mon Jan 06 10:31:20 2020 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1997, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2020, 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
@@ -29,7 +29,7 @@
  * 4098741 4099404 4101481 4106658 4106662 4106664 4108738 4110936 4122840
  * 4125885 4134034 4134300 4140009 4141750 4145457 4147295 4147706 4162198
  * 4162852 4167494 4170798 4176114 4179818 4185761 4212072 4212073 4216742
- * 4217661 4243011 4243108 4330377 4233840 4241880 4833877 8008577
+ * 4217661 4243011 4243108 4330377 4233840 4241880 4833877 8008577 8227313
  * @summary Regression tests for NumberFormat and associated classes
  * @library /java/text/testlib
  * @build IntlTest HexDumpReader TestUtils
@@ -1802,6 +1802,66 @@
         }
         Locale.setDefault(savedLocale);
     }
+
+    /**
+     * Test for get/setMonetaryGroupingSeparator() methods.
+     * @since 15
+     */
+    public void test8227313() throws ParseException {
+        var nrmSep = 'n';
+        var monSep = 'm';
+        var curSym = "Cur";
+        var inputNum = 10;
+        var nrmPattern = ",#";
+        var monPattern = "\u00a4 ,#";
+        var expectedNrmFmt = "1n0";
+        var expectedMonFmt = "Cur 1m0";
+
+        var ndfs = DecimalFormatSymbols.getInstance();
+        ndfs.setGroupingSeparator(nrmSep);
+        var nf = new DecimalFormat(nrmPattern, ndfs);
+        var mdfs = DecimalFormatSymbols.getInstance();
+        mdfs.setMonetaryGroupingSeparator(monSep);
+        mdfs.setCurrencySymbol(curSym);
+        var mf = new DecimalFormat(monPattern, mdfs);
+
+        // get test
+        char gotNrmSep = mdfs.getGroupingSeparator();
+        char gotMonSep = mdfs.getMonetaryGroupingSeparator();
+        if (gotMonSep != monSep) {
+            errln("FAIL: getMonetaryGroupingSeparator() returned incorrect value. expected: "
+                    + monSep + ", got: " + gotMonSep);
+        }
+        if (gotMonSep == gotNrmSep) {
+            errln("FAIL: getMonetaryGroupingSeparator() returned the same value with " +
+                    "getGroupingSeparator(): monetary sep: " + gotMonSep +
+                    ", normal sep: " + gotNrmSep);
+        }
+
+        // format test
+        var formatted = mf.format(inputNum);
+        if (!formatted.equals(expectedMonFmt)) {
+            errln("FAIL: format failed. expected: " + expectedMonFmt +
+                    ", got: " + formatted);
+        }
+        formatted = nf.format(inputNum);
+        if (!formatted.equals(expectedNrmFmt)) {
+            errln("FAIL: normal format failed. expected: " + expectedNrmFmt +
+                    ", got: " + formatted);
+        }
+
+        // parse test
+        Number parsed = mf.parse(expectedMonFmt);
+        if (parsed.intValue() != inputNum) {
+            errln("FAIL: parse failed. expected: " + inputNum +
+                    ", got: " + parsed);
+        }
+        parsed = nf.parse(expectedNrmFmt);
+        if (parsed.intValue() != inputNum) {
+            errln("FAIL: normal parse failed. expected: " + inputNum +
+                    ", got: " + parsed);
+        }
+    }
 }
 
 @SuppressWarnings("serial")
--- a/test/jdk/sun/text/resources/LocaleData.cldr	Sun Jan 05 21:04:39 2020 -0800
+++ b/test/jdk/sun/text/resources/LocaleData.cldr	Mon Jan 06 10:31:20 2020 -0800
@@ -8388,3 +8388,9 @@
 FormatData/es_CL/standalone.MonthAbbreviations/8=sept.
 FormatData/es_CO/MonthAbbreviations/8=sep.
 FormatData/es_CO/standalone.MonthAbbreviations/8=sept.
+
+# bug # 8227313
+FormatData/de/latn.NumberElements/12=
+FormatData/de_AT/latn.NumberElements/12=.
+FormatData/fr/latn.NumberElements/11=
+FormatData/fr_CH/latn.NumberElements/11=.