]> granicus.if.org Git - icu/commitdiff
ICU-7467 Merging branch to trunk 4
authorShane Carr <shane@unicode.org>
Fri, 17 Mar 2017 23:54:23 +0000 (23:54 +0000)
committerShane Carr <shane@unicode.org>
Fri, 17 Mar 2017 23:54:23 +0000 (23:54 +0000)
X-SVN-Rev: 39866

83 files changed:
icu4j/main/classes/core/src/com/ibm/icu/impl/TextTrieMap.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/AffixPatternUtils.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/Endpoint.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/Exportable.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/Format.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity1.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity2.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity3.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity4.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantityBCD.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantitySelector.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/ModifierHolder.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/NumberStringBuilder.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/PNAffixGenerator.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/Parse.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternString.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/Properties.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/Rounder.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/RoundingUtils.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/demo.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/BigDecimalMultiplier.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CompactDecimalFormat.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CurrencyFormat.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/MagnitudeMultiplier.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/MeasureFormat.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PaddingFormat.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PositiveDecimalFormat.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PositiveNegativeAffixFormat.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/RangeFormat.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/RoundingFormat.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/ScientificFormat.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/StrongAffixFormat.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/ConstantAffixModifier.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/ConstantMultiFieldModifier.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/GeneralPluralModifier.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/PositiveNegativeAffixModifier.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/SimpleModifier.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/IncrementRounder.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/MagnitudeRounder.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/NoRounder.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/SignificantDigitsRounder.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalDataCache.java [deleted file]
icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalFormat.java
icu4j/main/classes/core/src/com/ibm/icu/text/CurrencyPluralInfo.java
icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormatSymbols.java
icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat_ICU58.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java
icu4j/main/classes/core/src/com/ibm/icu/text/MessageFormat.java
icu4j/main/classes/core/src/com/ibm/icu/text/NumberFormat.java
icu4j/main/classes/core/src/com/ibm/icu/text/PluralFormat.java
icu4j/main/classes/core/src/com/ibm/icu/text/PluralRules.java
icu4j/main/classes/core/src/com/ibm/icu/text/RuleBasedNumberFormat.java
icu4j/main/classes/core/src/com/ibm/icu/text/ScientificNumberFormatter.java
icu4j/main/classes/core/src/com/ibm/icu/util/Currency.java
icu4j/main/classes/core/src/com/ibm/icu/util/Measure.java
icu4j/main/tests/core/src/com/ibm/icu/dev/data/numberformattestspecification.txt
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/BigNumberFormatTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/CompactDecimalFormatTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DataDrivenNumberFormatTestData.java [moved from icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTestData.java with 96% similarity]
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DataDrivenNumberFormatTestUtility.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/IntlTestDecimalFormatAPI.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/IntlTestDecimalFormatAPIC.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/IntlTestDecimalFormatSymbolsC.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MeasureUnitTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatDataDrivenTest.java [new file with mode: 0644]
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatRegressionTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatSerialTestData.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTestCases.txt
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberRegressionTests.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/ShanesDataDrivenTester.java [new file with mode: 0644]
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestMessageFormat.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/AffixPatternUtilsTest.java [new file with mode: 0644]
icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/FormatQuantityTest.java [new file with mode: 0644]
icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/NumberStringBuilderTest.java [new file with mode: 0644]
icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/PatternStringTest.java [new file with mode: 0644]
icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/PropertiesTest.java [new file with mode: 0644]
icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/RounderTest.java [new file with mode: 0644]
icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/FormatHandler.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/util/TextTrieMapTest.java

index 6688d4719f1e55dfd9c084fb44bc98c28475cfbd..2576ff18d1c3fa4ffd5ca657d64f0448fd99a038 100644 (file)
@@ -105,6 +105,85 @@ public class TextTrieMap<V> {
         }
     }
 
+    /**
+     * Creates an object that consumes code points one at a time and returns intermediate prefix
+     * matches.  Returns null if no match exists.
+     *
+     * @return An instance of {@link ParseState}, or null if the starting code point is not a
+     * prefix for any entry in the trie.
+     */
+    public ParseState openParseState(int startingCp) {
+      // Check to see whether this is a valid starting character.  If not, return null.
+      if (_ignoreCase) {
+        startingCp = UCharacter.foldCase(startingCp, true);
+      }
+      int count = Character.charCount(startingCp);
+      char ch1 = (count == 1) ? (char) startingCp : Character.highSurrogate(startingCp);
+      if (!_root.hasChildFor(ch1)) {
+        return null;
+      }
+
+      return new ParseState(_root);
+    }
+
+    /**
+     * ParseState is mutable, not thread-safe, and intended to be used internally by parsers for
+     * consuming values from this trie.
+     */
+    public class ParseState {
+      private Node node;
+      private int offset;
+      private Node.StepResult result;
+
+      ParseState(Node start) {
+        node = start;
+        offset = 0;
+        result = start.new StepResult();
+      }
+
+      /**
+       * Consumes a code point and walk to the next node in the trie.
+       *
+       * @param cp The code point to consume.
+       */
+      public void accept(int cp) {
+        assert node != null;
+        if (_ignoreCase) {
+          cp = UCharacter.foldCase(cp, true);
+        }
+        int count = Character.charCount(cp);
+        char ch1 = (count == 1) ? (char) cp : Character.highSurrogate(cp);
+        node.takeStep(ch1, offset, result);
+        if (count == 2 && result.node != null) {
+          char ch2 = Character.lowSurrogate(cp);
+          result.node.takeStep(ch2, result.offset, result);
+        }
+        node = result.node;
+        offset = result.offset;
+      }
+
+      /**
+       * Gets the exact prefix matches for all code points that have been consumed so far.
+       *
+       * @return The matches.
+       */
+      public Iterator<V> getCurrentMatches() {
+        if (node != null && offset == node.charCount()) {
+          return node.values();
+        }
+        return null;
+      }
+
+      /**
+       * Checks whether any more code points can be consumed.
+       *
+       * @return true if no more code points can be consumed; false otherwise.
+       */
+      public boolean atEnd() {
+        return node == null || (node.charCount() == offset && node._children == null);
+      }
+    }
+
     public static class CharIterator implements Iterator<Character> {
         private boolean _ignoreCase;
         private CharSequence _text;
@@ -234,6 +313,21 @@ public class TextTrieMap<V> {
             _children = children;
         }
 
+        public int charCount() {
+          return _text == null ? 0 : _text.length;
+        }
+
+        public boolean hasChildFor(char ch) {
+          for (int i=0; _children != null && i < _children.size(); i++) {
+            Node child = _children.get(i);
+            if (ch < child._text[0]) break;
+            if (ch == child._text[0]) {
+              return true;
+            }
+          }
+          return false;
+        }
+
         public Iterator<V> values() {
             if (_values == null) {
                 return null;
@@ -272,6 +366,37 @@ public class TextTrieMap<V> {
             return match;
         }
 
+        public class StepResult {
+          public Node node;
+          public int offset;
+        }
+        public void takeStep(char ch, int offset, StepResult result) {
+          assert offset <= charCount();
+          if (offset == charCount()) {
+            // Go to a child node
+            for (int i=0; _children != null && i < _children.size(); i++) {
+              Node child = _children.get(i);
+              if (ch < child._text[0]) break;
+              if (ch == child._text[0]) {
+                // Found a matching child node
+                result.node = child;
+                result.offset = 1;
+                return;
+              }
+            }
+            // No matching children; fall through
+          } else if (_text[offset] == ch) {
+            // Return to this node; increase offset
+            result.node = this;
+            result.offset = offset + 1;
+            return;
+          }
+          // No matches
+          result.node = null;
+          result.offset = -1;
+          return;
+        }
+
         private void add(char[] text, int offset, V value) {
             if (text.length == offset) {
                 _values = addValue(_values, value);
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/AffixPatternUtils.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/AffixPatternUtils.java
new file mode 100644 (file)
index 0000000..992028d
--- /dev/null
@@ -0,0 +1,524 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.NumberFormat.Field;
+
+/**
+ * Performs manipulations on affix patterns: the prefix and suffix strings associated with a decimal
+ * format pattern. For example:
+ *
+ * <table>
+ * <tr><th>Affix Pattern</th><th>Example Unescaped (Formatted) String</th></tr>
+ * <tr><td>abc</td><td>abc</td></tr>
+ * <tr><td>ab-</td><td>ab−</td></tr>
+ * <tr><td>ab'-'</td><td>ab-</td></tr>
+ * <tr><td>ab''</td><td>ab'</td></tr>
+ * </table>
+ *
+ * To manually iterate over tokens in a literal string, use the following pattern, which is designed
+ * to be efficient.
+ *
+ * <pre>
+ * long tag = 0L;
+ * while (AffixPatternUtils.hasNext(tag, patternString)) {
+ *   tag = AffixPatternUtils.nextToken(tag, patternString);
+ *   int typeOrCp = AffixPatternUtils.getTypeOrCp(tag);
+ *   switch (typeOrCp) {
+ *     case AffixPatternUtils.TYPE_MINUS_SIGN:
+ *       // Current token is a minus sign.
+ *       break;
+ *     case AffixPatternUtils.TYPE_PLUS_SIGN:
+ *       // Current token is a plus sign.
+ *       break;
+ *     case AffixPatternUtils.TYPE_PERCENT:
+ *       // Current token is a percent sign.
+ *       break;
+ *     case AffixPatternUtils.TYPE_PERMILLE:
+ *       // Current token is a permille sign.
+ *       break;
+ *     case AffixPatternUtils.TYPE_CURRENCY_SINGLE:
+ *       // Current token is a single currency sign.
+ *       break;
+ *     case AffixPatternUtils.TYPE_CURRENCY_DOUBLE:
+ *       // Current token is a double currency sign.
+ *       break;
+ *     case AffixPatternUtils.TYPE_CURRENCY_TRIPLE:
+ *       // Current token is a triple currency sign.
+ *       break;
+ *     case AffixPatternUtils.TYPE_CURRENCY_OVERFLOW:
+ *       // Current token has four or more currency signs.
+ *       break;
+ *     default:
+ *       // Current token is an arbitrary code point.
+ *       // The variable typeOrCp is the code point.
+ *       break;
+ *   }
+ * }
+ * </pre>
+ */
+public class AffixPatternUtils {
+
+  private static final int STATE_BASE = 0;
+  private static final int STATE_FIRST_QUOTE = 1;
+  private static final int STATE_INSIDE_QUOTE = 2;
+  private static final int STATE_AFTER_QUOTE = 3;
+  private static final int STATE_FIRST_CURR = 4;
+  private static final int STATE_SECOND_CURR = 5;
+  private static final int STATE_THIRD_CURR = 6;
+  private static final int STATE_OVERFLOW_CURR = 7;
+
+  private static final int TYPE_CODEPOINT = 0;
+
+  /** Represents a minus sign symbol '-'. */
+  public static final int TYPE_MINUS_SIGN = -1;
+
+  /** Represents a plus sign symbol '+'. */
+  public static final int TYPE_PLUS_SIGN = -2;
+
+  /** Represents a percent sign symbol '%'. */
+  public static final int TYPE_PERCENT = -3;
+
+  /** Represents a permille sign symbol '‰'. */
+  public static final int TYPE_PERMILLE = -4;
+
+  /** Represents a single currency symbol '¤'. */
+  public static final int TYPE_CURRENCY_SINGLE = -5;
+
+  /** Represents a double currency symbol '¤¤'. */
+  public static final int TYPE_CURRENCY_DOUBLE = -6;
+
+  /** Represents a triple currency symbol '¤¤¤'. */
+  public static final int TYPE_CURRENCY_TRIPLE = -7;
+
+  /** Represents a sequence of four or more currency symbols. */
+  public static final int TYPE_CURRENCY_OVERFLOW = -15;
+
+  /**
+   * Checks whether the specified affix pattern has any unquoted currency symbols ("¤").
+   *
+   * @param patternString The string to check for currency symbols.
+   * @return true if the literal has at least one unquoted currency symbol; false otherwise.
+   */
+  public static boolean hasCurrencySymbols(CharSequence patternString) {
+    if (patternString == null) return false;
+    int offset = 0;
+    int state = STATE_BASE;
+    boolean result = false;
+    for (; offset < patternString.length(); ) {
+      int cp = Character.codePointAt(patternString, offset);
+      switch (state) {
+        case STATE_BASE:
+          if (cp == '¤') {
+            result = true;
+          } else if (cp == '\'') {
+            state = STATE_INSIDE_QUOTE;
+          }
+          break;
+        case STATE_INSIDE_QUOTE:
+          if (cp == '\'') {
+            state = STATE_BASE;
+          }
+          break;
+        default:
+          throw new AssertionError();
+      }
+      offset += Character.charCount(cp);
+    }
+
+    if (state == STATE_INSIDE_QUOTE) {
+      throw new IllegalArgumentException("Unterminated quote: \"" + patternString + "\"");
+    } else {
+      return result;
+    }
+  }
+
+  /**
+   * Estimates the number of code points present in an unescaped version of the affix pattern string
+   * (one that would be returned by {@link #unescape}), assuming that all interpolated symbols
+   * consume one code point and that currencies consume as many code points as their symbol width.
+   * Used for computing padding width.
+   *
+   * @param patternString The original string whose width will be estimated.
+   * @return The length of the unescaped string.
+   */
+  public static int unescapedLength(CharSequence patternString) {
+    if (patternString == null) return 0;
+    int state = STATE_BASE;
+    int offset = 0;
+    int length = 0;
+    for (; offset < patternString.length(); ) {
+      int cp = Character.codePointAt(patternString, offset);
+
+      switch (state) {
+        case STATE_BASE:
+          if (cp == '\'') {
+            // First quote
+            state = STATE_FIRST_QUOTE;
+          } else {
+            // Unquoted symbol
+            length++;
+          }
+          break;
+        case STATE_FIRST_QUOTE:
+          if (cp == '\'') {
+            // Repeated quote
+            length++;
+            state = STATE_BASE;
+          } else {
+            // Quoted code point
+            length++;
+            state = STATE_INSIDE_QUOTE;
+          }
+          break;
+        case STATE_INSIDE_QUOTE:
+          if (cp == '\'') {
+            // End of quoted sequence
+            state = STATE_AFTER_QUOTE;
+          } else {
+            // Quoted code point
+            length++;
+          }
+          break;
+        case STATE_AFTER_QUOTE:
+          if (cp == '\'') {
+            // Double quote inside of quoted sequence
+            length++;
+            state = STATE_INSIDE_QUOTE;
+          } else {
+            // Unquoted symbol
+            length++;
+          }
+          break;
+        default:
+          throw new AssertionError();
+      }
+
+      offset += Character.charCount(cp);
+    }
+
+    switch (state) {
+      case STATE_FIRST_QUOTE:
+      case STATE_INSIDE_QUOTE:
+        throw new IllegalArgumentException("Unterminated quote: \"" + patternString + "\"");
+      default:
+        break;
+    }
+
+    return length;
+  }
+
+  /**
+   * Takes a string and escapes (quotes) characters that have special meaning in the affix pattern
+   * syntax. This function does not reverse-lookup symbols.
+   *
+   * <p>Example input: "-$x"; example output: "'-'$x"
+   *
+   * @param input The string to be escaped.
+   * @param output The string builder to which to append the escaped string.
+   * @return The number of chars (UTF-16 code units) appended to the output.
+   */
+  public static int escape(CharSequence input, StringBuilder output) {
+    if (input == null) return 0;
+    int state = STATE_BASE;
+    int offset = 0;
+    int startLength = output.length();
+    for (; offset < input.length(); ) {
+      int cp = Character.codePointAt(input, offset);
+
+      switch (cp) {
+        case '\'':
+          output.append("''");
+          break;
+
+        case '-':
+        case '+':
+        case '%':
+        case '‰':
+        case '¤':
+          if (state == STATE_BASE) {
+            output.append('\'');
+            output.appendCodePoint(cp);
+            state = STATE_INSIDE_QUOTE;
+          } else {
+            output.appendCodePoint(cp);
+          }
+          break;
+
+        default:
+          if (state == STATE_INSIDE_QUOTE) {
+            output.append('\'');
+            output.appendCodePoint(cp);
+            state = STATE_BASE;
+          } else {
+            output.appendCodePoint(cp);
+          }
+          break;
+      }
+      offset += Character.charCount(cp);
+    }
+
+    if (state == STATE_INSIDE_QUOTE) {
+      output.append('\'');
+    }
+
+    return output.length() - startLength;
+  }
+
+  /**
+   * Executes the unescape state machine. Replaces the unquoted characters "-", "+", "%", and "‰"
+   * with their localized equivalents. Replaces "¤", "¤¤", and "¤¤¤" with the three argument
+   * strings.
+   *
+   * <p>Example input: "'-'¤x"; example output: "-$x"
+   *
+   * @param affixPattern The original string to be unescaped.
+   * @param symbols An instance of {@link DecimalFormatSymbols} for the locale of interest.
+   * @param currency1 The string to replace "¤".
+   * @param currency2 The string to replace "¤¤".
+   * @param currency3 The string to replace "¤¤¤".
+   * @param minusSign The string to replace "-". If null, symbols.getMinusSignString() is used.
+   * @param output The {@link NumberStringBuilder} to which the result will be appended.
+   */
+  public static void unescape(
+      CharSequence affixPattern,
+      DecimalFormatSymbols symbols,
+      String currency1,
+      String currency2,
+      String currency3,
+      String minusSign,
+      NumberStringBuilder output) {
+    if (affixPattern == null || affixPattern.length() == 0) return;
+    if (minusSign == null) minusSign = symbols.getMinusSignString();
+    long tag = 0L;
+    while (hasNext(tag, affixPattern)) {
+      tag = nextToken(tag, affixPattern);
+      int typeOrCp = getTypeOrCp(tag);
+      switch (typeOrCp) {
+        case TYPE_MINUS_SIGN:
+          output.append(minusSign, Field.SIGN);
+          break;
+        case TYPE_PLUS_SIGN:
+          output.append(symbols.getPlusSignString(), Field.SIGN);
+          break;
+        case TYPE_PERCENT:
+          output.append(symbols.getPercentString(), Field.PERCENT);
+          break;
+        case TYPE_PERMILLE:
+          output.append(symbols.getPerMillString(), Field.PERMILLE);
+          break;
+        case TYPE_CURRENCY_SINGLE:
+          output.append(currency1, Field.CURRENCY);
+          break;
+        case TYPE_CURRENCY_DOUBLE:
+          output.append(currency2, Field.CURRENCY);
+          break;
+        case TYPE_CURRENCY_TRIPLE:
+          output.append(currency3, Field.CURRENCY);
+          break;
+        case TYPE_CURRENCY_OVERFLOW:
+          output.append("\uFFFD", Field.CURRENCY);
+          break;
+        default:
+          output.appendCodePoint(typeOrCp, null);
+          break;
+      }
+    }
+  }
+
+  /**
+   * Returns the next token from the affix pattern.
+   *
+   * @param tag A bitmask used for keeping track of state from token to token. The initial value
+   *     should be 0L.
+   * @param patternString The affix pattern.
+   * @return The bitmask tag to pass to the next call of this method to retrieve the following
+   *     token.
+   * @throws IllegalArgumentException If there are no tokens left or if there is a syntax error in
+   *     the pattern string.
+   * @see #hasNext
+   */
+  public static long nextToken(long tag, CharSequence patternString) {
+    int offset = getOffset(tag);
+    int state = getState(tag);
+    for (; offset < patternString.length(); ) {
+      int cp = Character.codePointAt(patternString, offset);
+      int count = Character.charCount(cp);
+
+      switch (state) {
+        case STATE_BASE:
+          switch (cp) {
+            case '\'':
+              state = STATE_FIRST_QUOTE;
+              offset += count;
+              // continue to the next code point
+              break;
+            case '-':
+              return makeTag(offset + count, TYPE_MINUS_SIGN, STATE_BASE, 0);
+            case '+':
+              return makeTag(offset + count, TYPE_PLUS_SIGN, STATE_BASE, 0);
+            case '%':
+              return makeTag(offset + count, TYPE_PERCENT, STATE_BASE, 0);
+            case '‰':
+              return makeTag(offset + count, TYPE_PERMILLE, STATE_BASE, 0);
+            case '¤':
+              state = STATE_FIRST_CURR;
+              offset += count;
+              // continue to the next code point
+              break;
+            default:
+              return makeTag(offset + count, TYPE_CODEPOINT, STATE_BASE, cp);
+          }
+          break;
+        case STATE_FIRST_QUOTE:
+          if (cp == '\'') {
+            return makeTag(offset + count, TYPE_CODEPOINT, STATE_BASE, cp);
+          } else {
+            return makeTag(offset + count, TYPE_CODEPOINT, STATE_INSIDE_QUOTE, cp);
+          }
+        case STATE_INSIDE_QUOTE:
+          if (cp == '\'') {
+            state = STATE_AFTER_QUOTE;
+            offset += count;
+            // continue to the next code point
+            break;
+          } else {
+            return makeTag(offset + count, TYPE_CODEPOINT, STATE_INSIDE_QUOTE, cp);
+          }
+        case STATE_AFTER_QUOTE:
+          if (cp == '\'') {
+            return makeTag(offset + count, TYPE_CODEPOINT, STATE_INSIDE_QUOTE, cp);
+          } else {
+            state = STATE_BASE;
+            // re-evaluate this code point
+            break;
+          }
+        case STATE_FIRST_CURR:
+          if (cp == '¤') {
+            state = STATE_SECOND_CURR;
+            offset += count;
+            // continue to the next code point
+            break;
+          } else {
+            return makeTag(offset, TYPE_CURRENCY_SINGLE, STATE_BASE, 0);
+          }
+        case STATE_SECOND_CURR:
+          if (cp == '¤') {
+            state = STATE_THIRD_CURR;
+            offset += count;
+            // continue to the next code point
+            break;
+          } else {
+            return makeTag(offset, TYPE_CURRENCY_DOUBLE, STATE_BASE, 0);
+          }
+        case STATE_THIRD_CURR:
+          if (cp == '¤') {
+            state = STATE_OVERFLOW_CURR;
+            offset += count;
+            // continue to the next code point
+            break;
+          } else {
+            return makeTag(offset, TYPE_CURRENCY_TRIPLE, STATE_BASE, 0);
+          }
+        case STATE_OVERFLOW_CURR:
+          if (cp == '¤') {
+            offset += count;
+            // continue to the next code point and loop back to this state
+            break;
+          } else {
+            return makeTag(offset, TYPE_CURRENCY_OVERFLOW, STATE_BASE, 0);
+          }
+        default:
+          throw new AssertionError();
+      }
+    }
+    // End of string
+    switch (state) {
+      case STATE_BASE:
+        // We shouldn't get here if hasNext() was followed.
+        throw new IllegalArgumentException();
+      case STATE_FIRST_QUOTE:
+      case STATE_INSIDE_QUOTE:
+        // For consistent behavior with the JDK and ICU 58, throw an exception here.
+        throw new IllegalArgumentException(
+            "Unterminated quote in pattern affix: \"" + patternString + "\"");
+      case STATE_AFTER_QUOTE:
+        // We shouldn't get here if hasNext() was followed.
+        throw new IllegalArgumentException();
+      case STATE_FIRST_CURR:
+        return makeTag(offset, TYPE_CURRENCY_SINGLE, STATE_BASE, 0);
+      case STATE_SECOND_CURR:
+        return makeTag(offset, TYPE_CURRENCY_DOUBLE, STATE_BASE, 0);
+      case STATE_THIRD_CURR:
+        return makeTag(offset, TYPE_CURRENCY_TRIPLE, STATE_BASE, 0);
+      case STATE_OVERFLOW_CURR:
+        return makeTag(offset, TYPE_CURRENCY_OVERFLOW, STATE_BASE, 0);
+      default:
+        throw new AssertionError();
+    }
+  }
+
+  /**
+   * Returns whether the affix pattern string has any more tokens to be retrieved from a call to
+   * {@link #nextToken}.
+   *
+   * @param tag The bitmask tag of the previous token, as returned by {@link #nextToken}.
+   * @param string The affix pattern.
+   * @return true if there are more tokens to consume; false otherwise.
+   */
+  public static boolean hasNext(long tag, CharSequence string) {
+    int state = getState(tag);
+    int offset = getOffset(tag);
+    // Special case: the last character in string is an end quote.
+    if (state == STATE_INSIDE_QUOTE
+        && offset == string.length() - 1
+        && string.charAt(offset) == '\'') {
+      return false;
+    } else if (state != STATE_BASE) {
+      return true;
+    } else {
+      return offset < string.length();
+    }
+  }
+
+  /**
+   * This function helps determine the identity of the token consumed by {@link #nextToken}.
+   * Converts from a bitmask tag, based on a call to {@link #nextToken}, to its corresponding symbol
+   * type or code point.
+   *
+   * @param tag The bitmask tag of the current token, as returned by {@link #nextToken}.
+   * @return If less than zero, a symbol type corresponding to one of the <code>TYPE_</code>
+   *     constants, such as {@link #TYPE_MINUS_SIGN}. If greater than or equal to zero, a literal
+   *     code point.
+   */
+  public static int getTypeOrCp(long tag) {
+    int type = getType(tag);
+    return (type == 0) ? getCodePoint(tag) : -type;
+  }
+
+  private static long makeTag(int offset, int type, int state, int cp) {
+    long tag = 0L;
+    tag |= offset;
+    tag |= (-(long) type) << 32;
+    tag |= ((long) state) << 36;
+    tag |= ((long) cp) << 40;
+    return tag;
+  }
+
+  static int getOffset(long tag) {
+    return (int) (tag & 0xffffffff);
+  }
+
+  static int getType(long tag) {
+    return (int) ((tag >>> 32) & 0xf);
+  }
+
+  static int getState(long tag) {
+    return (int) ((tag >>> 36) & 0xf);
+  }
+
+  static int getCodePoint(long tag) {
+    return (int) (tag >>> 40);
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Endpoint.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Endpoint.java
new file mode 100644 (file)
index 0000000..ef603bc
--- /dev/null
@@ -0,0 +1,286 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import com.ibm.icu.impl.number.Format.BeforeTargetAfterFormat;
+import com.ibm.icu.impl.number.Format.SingularFormat;
+import com.ibm.icu.impl.number.Format.TargetFormat;
+import com.ibm.icu.impl.number.formatters.BigDecimalMultiplier;
+import com.ibm.icu.impl.number.formatters.CompactDecimalFormat;
+import com.ibm.icu.impl.number.formatters.CurrencyFormat;
+import com.ibm.icu.impl.number.formatters.MagnitudeMultiplier;
+import com.ibm.icu.impl.number.formatters.MeasureFormat;
+import com.ibm.icu.impl.number.formatters.PaddingFormat;
+import com.ibm.icu.impl.number.formatters.PositiveDecimalFormat;
+import com.ibm.icu.impl.number.formatters.PositiveNegativeAffixFormat;
+import com.ibm.icu.impl.number.formatters.RoundingFormat;
+import com.ibm.icu.impl.number.formatters.ScientificFormat;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.PluralRules;
+import com.ibm.icu.util.ULocale;
+
+public class Endpoint {
+  //  public static Format from(DecimalFormatSymbols symbols, Properties properties)
+  //      throws ParseException {
+  //    Format format = new PositiveIntegerFormat(symbols, properties);
+  //    // TODO: integer-only format
+  //    format = new PositiveDecimalFormat((SelfContainedFormat) format, symbols, properties);
+  //    if (properties.useCompactDecimalFormat()) {
+  //      format = CompactDecimalFormat.getInstance((SingularFormat) format, symbols, properties);
+  //    } else {
+  //      format =
+  //          PositiveNegativeAffixFormat.getInstance((SingularFormat) format, symbols, properties);
+  //    }
+  //    if (properties.useRoundingInterval()) {
+  //      format = new IntervalRoundingFormat((SingularFormat) format, properties);
+  //    } else if (properties.useSignificantDigits()) {
+  //      format = new SignificantDigitsFormat((SingularFormat) format, properties);
+  //    } else if (properties.useFractionFormat()) {
+  //      format = new RoundingFormat((SingularFormat) format, properties);
+  //    }
+  //    return format;
+  //  }
+
+  public static Format fromBTA(Properties properties) {
+    return fromBTA(properties, getSymbols());
+  }
+
+  public static SingularFormat fromBTA(Properties properties, Locale locale) {
+    return fromBTA(properties, getSymbols(locale));
+  }
+
+  public static SingularFormat fromBTA(Properties properties, ULocale uLocale) {
+    return fromBTA(properties, getSymbols(uLocale));
+  }
+
+  public static SingularFormat fromBTA(String pattern) {
+    return fromBTA(getProperties(pattern), getSymbols());
+  }
+
+  public static SingularFormat fromBTA(String pattern, Locale locale) {
+    return fromBTA(getProperties(pattern), getSymbols(locale));
+  }
+
+  public static SingularFormat fromBTA(String pattern, ULocale uLocale) {
+    return fromBTA(getProperties(pattern), getSymbols(uLocale));
+  }
+
+  public static SingularFormat fromBTA(String pattern, DecimalFormatSymbols symbols) {
+    return fromBTA(getProperties(pattern), symbols);
+  }
+
+  public static SingularFormat fromBTA(Properties properties, DecimalFormatSymbols symbols) {
+
+    if (symbols == null) throw new IllegalArgumentException("symbols must not be null");
+
+    // TODO: This fast track results in an improvement of about 10ns during formatting.  See if
+    // there is a way to implement it more elegantly.
+    boolean canUseFastTrack = true;
+    PluralRules rules = getPluralRules(symbols.getULocale(), properties);
+    BeforeTargetAfterFormat format = new Format.BeforeTargetAfterFormat(rules);
+    TargetFormat target = new PositiveDecimalFormat(symbols, properties);
+    format.setTargetFormat(target);
+    // TODO: integer-only format?
+    if (MagnitudeMultiplier.useMagnitudeMultiplier(properties)) {
+      canUseFastTrack = false;
+      format.addBeforeFormat(MagnitudeMultiplier.getInstance(properties));
+    }
+    if (BigDecimalMultiplier.useMultiplier(properties)) {
+      canUseFastTrack = false;
+      format.addBeforeFormat(BigDecimalMultiplier.getInstance(properties));
+    }
+    if (MeasureFormat.useMeasureFormat(properties)) {
+      canUseFastTrack = false;
+      format.addBeforeFormat(MeasureFormat.getInstance(symbols, properties));
+    }
+    if (CurrencyFormat.useCurrency(properties)) {
+      canUseFastTrack = false;
+      if (CompactDecimalFormat.useCompactDecimalFormat(properties)) {
+        format.addBeforeFormat(CompactDecimalFormat.getInstance(symbols, properties));
+      } else if (ScientificFormat.useScientificNotation(properties)) {
+          // TODO: Should the currency rounder or scientific rounder be used in this case?
+          // For now, default to using the scientific rounder.
+        format.addBeforeFormat(PositiveNegativeAffixFormat.getInstance(symbols, properties));
+        format.addBeforeFormat(ScientificFormat.getInstance(symbols, properties));
+      } else {
+        format.addBeforeFormat(CurrencyFormat.getCurrencyRounder(symbols, properties));
+        format.addBeforeFormat(CurrencyFormat.getCurrencyModifier(symbols, properties));
+      }
+    } else {
+      if (CompactDecimalFormat.useCompactDecimalFormat(properties)) {
+        canUseFastTrack = false;
+        format.addBeforeFormat(CompactDecimalFormat.getInstance(symbols, properties));
+      } else if (ScientificFormat.useScientificNotation(properties)) {
+        canUseFastTrack = false;
+        format.addBeforeFormat(PositiveNegativeAffixFormat.getInstance(symbols, properties));
+        format.addBeforeFormat(ScientificFormat.getInstance(symbols, properties));
+      } else {
+        format.addBeforeFormat(PositiveNegativeAffixFormat.getInstance(symbols, properties));
+        format.addBeforeFormat(RoundingFormat.getDefaultOrNoRounder(properties));
+      }
+    }
+    if (PaddingFormat.usePadding(properties)) {
+      canUseFastTrack = false;
+      format.addAfterFormat(PaddingFormat.getInstance(properties));
+    }
+    if (canUseFastTrack) {
+      return new Format.PositiveNegativeRounderTargetFormat(
+          PositiveNegativeAffixFormat.getInstance(symbols, properties),
+          RoundingFormat.getDefaultOrNoRounder(properties),
+          target);
+    } else {
+      return format;
+    }
+  }
+
+  public static String staticFormat(FormatQuantity input, Properties properties) {
+    return staticFormat(input, properties, getSymbols());
+  }
+
+  public static String staticFormat(FormatQuantity input, Properties properties, Locale locale) {
+    return staticFormat(input, properties, getSymbols(locale));
+  }
+
+  public static String staticFormat(FormatQuantity input, Properties properties, ULocale uLocale) {
+    return staticFormat(input, properties, getSymbols(uLocale));
+  }
+
+  public static String staticFormat(FormatQuantity input, String pattern) {
+    return staticFormat(input, getProperties(pattern), getSymbols());
+  }
+
+  public static String staticFormat(FormatQuantity input, String pattern, Locale locale) {
+    return staticFormat(input, getProperties(pattern), getSymbols(locale));
+  }
+
+  public static String staticFormat(FormatQuantity input, String pattern, ULocale uLocale) {
+    return staticFormat(input, getProperties(pattern), getSymbols(uLocale));
+  }
+
+  public static String staticFormat(
+      FormatQuantity input, String pattern, DecimalFormatSymbols symbols) {
+    return staticFormat(input, getProperties(pattern), symbols);
+  }
+
+  public static String staticFormat(
+      FormatQuantity input, Properties properties, DecimalFormatSymbols symbols) {
+    PluralRules rules = null;
+    ModifierHolder mods = Format.threadLocalModifierHolder.get().clear();
+    NumberStringBuilder sb = Format.threadLocalStringBuilder.get().clear();
+    int length = 0;
+
+    // Pre-processing
+    if (!input.isNaN()) {
+      if (MagnitudeMultiplier.useMagnitudeMultiplier(properties)) {
+        MagnitudeMultiplier.getInstance(properties).before(input, mods, rules);
+      }
+      if (BigDecimalMultiplier.useMultiplier(properties)) {
+        BigDecimalMultiplier.getInstance(properties).before(input, mods, rules);
+      }
+      if (MeasureFormat.useMeasureFormat(properties)) {
+        rules = (rules != null) ? rules : getPluralRules(symbols.getULocale(), properties);
+        MeasureFormat.getInstance(symbols, properties).before(input, mods, rules);
+      }
+      if (CompactDecimalFormat.useCompactDecimalFormat(properties)) {
+        rules = (rules != null) ? rules : getPluralRules(symbols.getULocale(), properties);
+        CompactDecimalFormat.apply(input, mods, rules, symbols, properties);
+      } else if (CurrencyFormat.useCurrency(properties)) {
+        rules = (rules != null) ? rules : getPluralRules(symbols.getULocale(), properties);
+        CurrencyFormat.getCurrencyRounder(symbols, properties).before(input, mods, rules);
+        CurrencyFormat.getCurrencyModifier(symbols, properties).before(input, mods, rules);
+      } else if (ScientificFormat.useScientificNotation(properties)) {
+        // TODO: Is it possible to combine significant digits with currency?
+        PositiveNegativeAffixFormat.getInstance(symbols, properties).before(input, mods, rules);
+        ScientificFormat.getInstance(symbols, properties).before(input, mods, rules);
+      } else {
+        PositiveNegativeAffixFormat.apply(input, mods, symbols, properties);
+        RoundingFormat.getDefaultOrNoRounder(properties).before(input, mods, rules);
+      }
+    }
+
+    // Primary format step
+    length += new PositiveDecimalFormat(symbols, properties).target(input, sb, 0);
+    length += mods.applyStrong(sb, 0, length);
+
+    // Post-processing
+    if (PaddingFormat.usePadding(properties)) {
+      length += PaddingFormat.getInstance(properties).after(mods, sb, 0, length);
+    }
+    length += mods.applyAll(sb, 0, length);
+
+    return sb.toString();
+  }
+
+  private static final ThreadLocal<Map<ULocale, DecimalFormatSymbols>> threadLocalSymbolsCache =
+      new ThreadLocal<Map<ULocale, DecimalFormatSymbols>>() {
+        @Override
+        protected Map<ULocale, DecimalFormatSymbols> initialValue() {
+          return new HashMap<ULocale, DecimalFormatSymbols>();
+        }
+      };
+
+  private static DecimalFormatSymbols getSymbols() {
+    ULocale uLocale = ULocale.getDefault();
+    return getSymbols(uLocale);
+  }
+
+  private static DecimalFormatSymbols getSymbols(Locale locale) {
+    ULocale uLocale = ULocale.forLocale(locale);
+    return getSymbols(uLocale);
+  }
+
+  private static DecimalFormatSymbols getSymbols(ULocale uLocale) {
+    if (uLocale == null) uLocale = ULocale.getDefault();
+    DecimalFormatSymbols symbols = threadLocalSymbolsCache.get().get(uLocale);
+    if (symbols == null) {
+      symbols = DecimalFormatSymbols.getInstance(uLocale);
+      threadLocalSymbolsCache.get().put(uLocale, symbols);
+    }
+    return symbols;
+  }
+
+  private static final ThreadLocal<Map<String, Properties>> threadLocalPropertiesCache =
+      new ThreadLocal<Map<String, Properties>>() {
+        @Override
+        protected Map<String, Properties> initialValue() {
+          return new HashMap<String, Properties>();
+        }
+      };
+
+  private static Properties getProperties(String pattern) {
+    if (pattern == null) pattern = "#";
+    Properties properties = threadLocalPropertiesCache.get().get(pattern);
+    if (properties == null) {
+      properties = PatternString.parseToProperties(pattern);
+      threadLocalPropertiesCache.get().put(pattern.intern(), properties);
+    }
+    return properties;
+  }
+
+  private static final ThreadLocal<Map<ULocale, PluralRules>> threadLocalRulesCache =
+      new ThreadLocal<Map<ULocale, PluralRules>>() {
+        @Override
+        protected Map<ULocale, PluralRules> initialValue() {
+          return new HashMap<ULocale, PluralRules>();
+        }
+      };
+
+  private static PluralRules getPluralRules(ULocale uLocale, Properties properties) {
+    // Backwards compatibility: CurrencyPluralInfo wraps its own copy of PluralRules
+    if (properties.getCurrencyPluralInfo() != null) {
+      return properties.getCurrencyPluralInfo().getPluralRules();
+    }
+
+    if (uLocale == null) uLocale = ULocale.getDefault();
+    PluralRules rules = threadLocalRulesCache.get().get(uLocale);
+    if (rules == null) {
+      rules = PluralRules.forLocale(uLocale);
+      threadLocalRulesCache.get().put(uLocale, rules);
+    }
+    return rules;
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Exportable.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Exportable.java
new file mode 100644 (file)
index 0000000..515667d
--- /dev/null
@@ -0,0 +1,15 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+/**
+ * This is a small interface I made to assist with converting from a formatter pipeline object to a
+ * pattern string. It allows classes to "export" themselves to a property bag, which in turn can be
+ * passed to {@link PatternString#propertiesToString(Properties)} to generate the pattern string.
+ *
+ * <p>Depending on the new API we expose, this process might not be necessary if we persist the
+ * property bag in the current DecimalFormat shim.
+ */
+public interface Exportable {
+  public void export(Properties properties);
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Format.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Format.java
new file mode 100644 (file)
index 0000000..681b663
--- /dev/null
@@ -0,0 +1,277 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.text.AttributedCharacterIterator;
+import java.text.FieldPosition;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Deque;
+
+import com.ibm.icu.text.PluralRules;
+
+// TODO: Get a better name for this base class.
+public abstract class Format {
+
+  protected static final ThreadLocal<NumberStringBuilder> threadLocalStringBuilder =
+      new ThreadLocal<NumberStringBuilder>() {
+        @Override
+        protected NumberStringBuilder initialValue() {
+          return new NumberStringBuilder();
+        }
+      };
+
+  protected static final ThreadLocal<ModifierHolder> threadLocalModifierHolder =
+      new ThreadLocal<ModifierHolder>() {
+        @Override
+        protected ModifierHolder initialValue() {
+          return new ModifierHolder();
+        }
+      };
+
+  public String format(FormatQuantity... inputs) {
+    // Setup
+    Deque<FormatQuantity> inputDeque = new ArrayDeque<FormatQuantity>();
+    inputDeque.addAll(Arrays.asList(inputs));
+    ModifierHolder modDeque = threadLocalModifierHolder.get().clear();
+    NumberStringBuilder sb = threadLocalStringBuilder.get().clear();
+
+    // Primary "recursion" step, calling the implementation's process method
+    int length = process(inputDeque, modDeque, sb, 0);
+
+    // Resolve remaining affixes
+    length += modDeque.applyAll(sb, 0, length);
+    return sb.toString();
+  }
+
+  /** A Format that works on only one number. */
+  public abstract static class SingularFormat extends Format implements Exportable {
+
+    public String format(FormatQuantity input) {
+      NumberStringBuilder sb = formatToStringBuilder(input);
+      return sb.toString();
+    }
+
+    public void format(FormatQuantity input, StringBuffer output) {
+      NumberStringBuilder sb = formatToStringBuilder(input);
+      output.append(sb);
+    }
+
+    public String format(FormatQuantity input, FieldPosition fp) {
+      NumberStringBuilder sb = formatToStringBuilder(input);
+      sb.populateFieldPosition(fp, 0);
+      return sb.toString();
+    }
+
+    public void format(FormatQuantity input, StringBuffer output, FieldPosition fp) {
+      NumberStringBuilder sb = formatToStringBuilder(input);
+      sb.populateFieldPosition(fp, output.length());
+      output.append(sb);
+    }
+
+    public AttributedCharacterIterator formatToCharacterIterator(FormatQuantity input) {
+      NumberStringBuilder sb = formatToStringBuilder(input);
+      return sb.getIterator();
+    }
+
+    private NumberStringBuilder formatToStringBuilder(FormatQuantity input) {
+      // Setup
+      ModifierHolder modDeque = threadLocalModifierHolder.get().clear();
+      NumberStringBuilder sb = threadLocalStringBuilder.get().clear();
+
+      // Primary "recursion" step, calling the implementation's process method
+      int length = process(input, modDeque, sb, 0);
+
+      // Resolve remaining affixes
+      length += modDeque.applyAll(sb, 0, length);
+      return sb;
+    }
+
+    @Override
+    public int process(
+        Deque<FormatQuantity> input,
+        ModifierHolder mods,
+        NumberStringBuilder string,
+        int startIndex) {
+      return process(input.removeFirst(), mods, string, startIndex);
+    }
+
+    public abstract int process(
+        FormatQuantity input, ModifierHolder mods, NumberStringBuilder string, int startIndex);
+  }
+
+  public static class BeforeTargetAfterFormat extends SingularFormat {
+    // The formatters are kept as individual fields to avoid extra object creation overhead.
+    private BeforeFormat before1 = null;
+    private BeforeFormat before2 = null;
+    private BeforeFormat before3 = null;
+    private TargetFormat target = null;
+    private AfterFormat after1 = null;
+    private AfterFormat after2 = null;
+    private AfterFormat after3 = null;
+    private final PluralRules rules;
+
+    public BeforeTargetAfterFormat(PluralRules rules) {
+      this.rules = rules;
+    }
+
+    public void addBeforeFormat(BeforeFormat before) {
+      if (before1 == null) {
+        before1 = before;
+      } else if (before2 == null) {
+        before2 = before;
+      } else if (before3 == null) {
+        before3 = before;
+      } else {
+        throw new IllegalArgumentException("Only three BeforeFormats are allowed at a time");
+      }
+    }
+
+    public void setTargetFormat(TargetFormat target) {
+      this.target = target;
+    }
+
+    public void addAfterFormat(AfterFormat after) {
+      if (after1 == null) {
+        after1 = after;
+      } else if (after2 == null) {
+        after2 = after;
+      } else if (after3 == null) {
+        after3 = after;
+      } else {
+        throw new IllegalArgumentException("Only three AfterFormats are allowed at a time");
+      }
+    }
+
+    @Override
+    public String format(FormatQuantity input) {
+      ModifierHolder mods = threadLocalModifierHolder.get().clear();
+      NumberStringBuilder sb = threadLocalStringBuilder.get().clear();
+      int length = process(input, mods, sb, 0);
+      length += mods.applyAll(sb, 0, length);
+      return sb.toString();
+    }
+
+    @Override
+    public int process(
+        FormatQuantity input, ModifierHolder mods, NumberStringBuilder string, int startIndex) {
+      // Special case: modifiers are skipped for NaN
+      int length = 0;
+      if (!input.isNaN()) {
+        if (before1 != null) {
+          before1.before(input, mods, rules);
+        }
+        if (before2 != null) {
+          before2.before(input, mods, rules);
+        }
+        if (before3 != null) {
+          before3.before(input, mods, rules);
+        }
+      }
+      length = target.target(input, string, startIndex);
+      length += mods.applyStrong(string, startIndex, startIndex + length);
+      if (after1 != null) {
+        length += after1.after(mods, string, startIndex, startIndex + length);
+      }
+      if (after2 != null) {
+        length += after2.after(mods, string, startIndex, startIndex + length);
+      }
+      if (after3 != null) {
+        length += after3.after(mods, string, startIndex, startIndex + length);
+      }
+      return length;
+    }
+
+    @Override
+    public void export(Properties properties) {
+      if (before1 != null) {
+        before1.export(properties);
+      }
+      if (before2 != null) {
+        before2.export(properties);
+      }
+      if (before3 != null) {
+        before3.export(properties);
+      }
+      target.export(properties);
+      if (after1 != null) {
+        after1.export(properties);
+      }
+      if (after2 != null) {
+        after2.export(properties);
+      }
+      if (after3 != null) {
+        after3.export(properties);
+      }
+    }
+  }
+
+  public static class PositiveNegativeRounderTargetFormat extends SingularFormat {
+    private final Modifier.PositiveNegativeModifier positiveNegative;
+    private final Rounder rounder;
+    private final TargetFormat target;
+
+    public PositiveNegativeRounderTargetFormat(
+        Modifier.PositiveNegativeModifier positiveNegative, Rounder rounder, TargetFormat target) {
+      this.positiveNegative = positiveNegative;
+      this.rounder = rounder;
+      this.target = target;
+    }
+
+    @Override
+    public String format(FormatQuantity input) {
+      NumberStringBuilder sb = threadLocalStringBuilder.get().clear();
+      process(input, null, sb, 0);
+      return sb.toString();
+    }
+
+    @Override
+    public int process(
+        FormatQuantity input, ModifierHolder mods, NumberStringBuilder string, int startIndex) {
+      // Special case: modifiers are skipped for NaN
+      Modifier mod = null;
+      rounder.apply(input);
+      if (!input.isNaN() && positiveNegative != null) {
+        mod = positiveNegative.getModifier(input.isNegative());
+      }
+      int length = target.target(input, string, startIndex);
+      if (mod != null) {
+        length += mod.apply(string, 0, length);
+      }
+      return length;
+    }
+
+    @Override
+    public void export(Properties properties) {
+      rounder.export(properties);
+      positiveNegative.export(properties);
+      target.export(properties);
+    }
+  }
+
+  public abstract static class BeforeFormat implements Exportable {
+    protected abstract void before(FormatQuantity input, ModifierHolder mods);
+
+    @SuppressWarnings("unused")
+    public void before(FormatQuantity input, ModifierHolder mods, PluralRules rules) {
+      before(input, mods);
+    }
+  }
+
+  public static interface TargetFormat extends Exportable {
+    public abstract int target(FormatQuantity input, NumberStringBuilder string, int startIndex);
+  }
+
+  public static interface AfterFormat extends Exportable {
+    public abstract int after(
+        ModifierHolder mods, NumberStringBuilder string, int leftIndex, int rightIndex);
+  }
+
+  // Instead of Dequeue<BigDecimal>, it could be Deque<Quantity> where
+  // we control the API of Quantity
+  public abstract int process(
+      Deque<FormatQuantity> inputs,
+      ModifierHolder outputMods,
+      NumberStringBuilder outputString,
+      int startIndex);
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity.java
new file mode 100644 (file)
index 0000000..a5d3765
--- /dev/null
@@ -0,0 +1,180 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.text.PluralRules;
+
+/**
+ * An interface representing a number to be processed by the decimal formatting pipeline. Includes
+ * methods for rounding, plural rules, and decimal digit extraction.
+ *
+ * <p>By design, this is NOT IMMUTABLE and NOT THREAD SAFE. It is intended to be an intermediate
+ * object holding state during a pass through the decimal formatting pipeline.
+ *
+ * <p>Implementations of this interface are free to use any internal storage mechanism.
+ *
+ * <p>TODO: Should I change this to an abstract class so that logic for min/max digits doesn't need
+ * to be copied to every implementation?
+ */
+public interface FormatQuantity extends PluralRules.IFixedDecimal {
+
+  /**
+   * Sets the minimum and maximum digits that this {@link FormatQuantity} should generate. This
+   * method does not perform rounding.
+   *
+   * @param minInt The minimum number of integer digits.
+   * @param maxInt The maximum number of integer digits.
+   * @param minFrac The minimum number of fraction digits.
+   * @param maxFrac The maximum number of fraction digits.
+   */
+  public void setIntegerFractionLength(int minInt, int maxInt, int minFrac, int maxFrac);
+
+  /**
+   * Rounds the number to a specified interval, such as 0.05.
+   *
+   * <p>If rounding to a power of ten, use the more efficient {@link #roundToMagnitude} instead.
+   *
+   * @param roundingInterval The increment to which to round.
+   * @param mathContext The {@link MathContext} to use if rounding is necessary. Undefined behavior
+   *     if null.
+   */
+  public void roundToIncrement(BigDecimal roundingInterval, MathContext mathContext);
+
+  /**
+   * Rounds the number to a specified magnitude (power of ten).
+   *
+   * @param roundingMagnitude The power of ten to which to round. For example, a value of -2 will
+   *     round to 2 decimal places.
+   * @param mathContext The {@link MathContext} to use if rounding is necessary. Undefined behavior
+   *     if null.
+   */
+  public void roundToMagnitude(int roundingMagnitude, MathContext mathContext);
+
+  /**
+   * Rounds the number to an infinite number of decimal points. This has no effect except for
+   * forcing the double in {@link FormatQuantityBCD} to adopt its exact representation.
+   */
+  public void roundToInfinity();
+
+  /**
+   * Multiply the internal value.
+   *
+   * @param multiplicand The value by which to multiply.
+   */
+  public void multiplyBy(BigDecimal multiplicand);
+
+  /**
+   * Scales the number by a power of ten. For example, if the value is currently "1234.56", calling
+   * this method with delta=-3 will change the value to "1.23456".
+   *
+   * @param delta The number of magnitudes of ten to change by.
+   */
+  public void adjustMagnitude(int delta);
+
+  /**
+   * @return The power of ten corresponding to the most significant nonzero digit.
+   * @throws ArithmeticException If the value represented is zero.
+   */
+  public int getMagnitude() throws ArithmeticException;
+
+  /** @return Whether the value represented by this {@link FormatQuantity} is zero. */
+  public boolean isZero();
+
+  /** @return Whether the value represented by this {@link FormatQuantity} is less than zero. */
+  public boolean isNegative();
+
+  /** @return Whether the value represented by this {@link FormatQuantity} is infinite. */
+  @Override
+  public boolean isInfinite();
+
+  /** @return Whether the value represented by this {@link FormatQuantity} is not a number. */
+  @Override
+  public boolean isNaN();
+
+  /** @return The value contained in this {@link FormatQuantity} approximated as a double. */
+  public double toDouble();
+
+  public BigDecimal toBigDecimal();
+
+  public int maxRepresentableDigits();
+
+  // TODO: Should this method be removed, since FormatQuantity implements IFixedDecimal now?
+  /**
+   * Computes the plural form for this number based on the specified set of rules.
+   *
+   * @param rules A {@link PluralRules} object representing the set of rules.
+   * @return The {@link StandardPlural} according to the PluralRules. If the plural form is not in
+   *     the set of standard plurals, {@link StandardPlural#OTHER} is returned instead.
+   */
+  public StandardPlural getStandardPlural(PluralRules rules);
+
+  //  /**
+  //   * @return The number of fraction digits, always in the closed interval [minFrac, maxFrac].
+  //   * @see #setIntegerFractionLength(int, int, int, int)
+  //   */
+  //  public int fractionCount();
+  //
+  //  /**
+  //   * @return The number of integer digits, always in the closed interval [minInt, maxInt].
+  //   * @see #setIntegerFractionLength(int, int, int, int)
+  //   */
+  //  public int integerCount();
+  //
+  //  /**
+  //   * @param index The index of the fraction digit relative to the decimal place, or 1 minus the
+  //   *     digit's power of ten.
+  //   * @return The digit at the specified index. Undefined if index is greater than maxInt or less
+  //   *     than 0.
+  //   * @see #fractionCount()
+  //   */
+  //  public byte getFractionDigit(int index);
+  //
+  //  /**
+  //   * @param index The index of the integer digit relative to the decimal place, or the digit's power
+  //   *     of ten.
+  //   * @return The digit at the specified index. Undefined if index is greater than maxInt or less
+  //   *     than 0.
+  //   * @see #integerCount()
+  //   */
+  //  public byte getIntegerDigit(int index);
+
+  /**
+   * Gets the digit at the specified magnitude. For example, if the represented number is 12.3,
+   * getDigit(-1) returns 3, since 3 is the digit corresponding to 10^-1.
+   *
+   * @param magnitude The magnitude of the digit.
+   * @return The digit at the specified magnitude.
+   */
+  public byte getDigit(int magnitude);
+
+  /**
+   * Gets the largest power of ten that needs to be displayed. The value returned by this function
+   * will be bounded between minInt and maxInt.
+   *
+   * @return The highest-magnitude digit to be displayed.
+   */
+  public int getUpperDisplayMagnitude();
+
+  /**
+   * Gets the smallest power of ten that needs to be displayed. The value returned by this function
+   * will be bounded between -minFrac and -maxFrac.
+   *
+   * @return The lowest-magnitude digit to be displayed.
+   */
+  public int getLowerDisplayMagnitude();
+
+  public FormatQuantity clone();
+
+  public void copyFrom(FormatQuantity other);
+
+  /**
+   * This method is for internal testing only and should be removed before release.
+   *
+   * @internal
+   */
+  public long getPositionFingerprint();
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity1.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity1.java
new file mode 100644 (file)
index 0000000..f9cc8e7
--- /dev/null
@@ -0,0 +1,860 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.text.PluralRules;
+import com.ibm.icu.text.PluralRules.Operand;
+
+/**
+ * This is an older implementation of FormatQuantity. A newer, faster implementation is
+ * FormatQuantity2. I kept this implementation around because it was useful for testing purposes
+ * (being able to compare the output of one implementation with the other).
+ *
+ * <p>This class is NOT IMMUTABLE and NOT THREAD SAFE and is intended to be used by a single thread
+ * to format a number through a formatter, which is thread-safe.
+ */
+public class FormatQuantity1 implements FormatQuantity {
+  // Four positions: left optional '(', left required '[', right required ']', right optional ')'.
+  // These four positions determine which digits are displayed in the output string.  They do NOT
+  // affect rounding.  These positions are internal-only and can be specified only by the public
+  // endpoints like setFractionLength, setIntegerLength, and setSignificantDigits, among others.
+  //
+  //   * Digits between lReqPos and rReqPos are in the "required zone" and are always displayed.
+  //   * Digits between lOptPos and rOptPos but outside the required zone are in the "optional zone"
+  //     and are displayed unless they are trailing off the left or right edge of the number and
+  //     have a numerical value of zero.  In order to be "trailing", the digits need to be beyond
+  //     the decimal point in their respective directions.
+  //   * Digits outside of the "optional zone" are never displayed.
+  //
+  // See the table below for illustrative examples.
+  //
+  // +---------+---------+---------+---------+------------+------------------------+--------------+
+  // | lOptPos | lReqPos | rReqPos | rOptPos |   number   |        positions       | en-US string |
+  // +---------+---------+---------+---------+------------+------------------------+--------------+
+  // |    5    |    2    |   -1    |   -5    |   1234.567 |     ( 12[34.5]67  )    |   1,234.567  |
+  // |    3    |    2    |   -1    |   -5    |   1234.567 |      1(2[34.5]67  )    |     234.567  |
+  // |    3    |    2    |   -1    |   -2    |   1234.567 |      1(2[34.5]6)7      |     234.56   |
+  // |    6    |    4    |    2    |   -5    | 123456789. |  123(45[67]89.     )   | 456,789.     |
+  // |    6    |    4    |    2    |    1    | 123456789. |     123(45[67]8)9.     | 456,780.     |
+  // |   -1    |   -1    |   -3    |   -4    | 0.123456   |     0.1([23]4)56       |        .0234 |
+  // |    6    |    4    |   -2    |   -2    |     12.3   |     (  [  12.3 ])      |    0012.30   |
+  // +---------+---------+---------+---------+------------+------------------------+--------------+
+  //
+  private int lOptPos = Integer.MAX_VALUE;
+  private int lReqPos = 0;
+  private int rReqPos = 0;
+  private int rOptPos = Integer.MIN_VALUE;
+
+  // Internally, attempt to use a long to store the number. A long can hold numbers between 18 and
+  // 19 digits, covering the vast majority of use cases. We store three values: the long itself,
+  // the "scale" of the long (the power of 10 represented by the rightmost digit in the long), and
+  // the "precision" (the number of digits in the long). "primary" and "primaryScale" are the only
+  // two variables that are required for representing the number in memory. "primaryPrecision" is
+  // saved only for the sake of performance enhancements when performing certain operations. It can
+  // always be re-computed from "primary" and "primaryScale".
+  private long primary;
+  private int primaryScale;
+  private int primaryPrecision;
+
+  // If the decimal can't fit into the long, fall back to a BigDecimal.
+  private BigDecimal fallback;
+
+  // Other properties
+  private int flags;
+  private static final int NEGATIVE_FLAG = 1;
+  private static final int INFINITY_FLAG = 2;
+  private static final int NAN_FLAG = 4;
+  private static final long[] POWERS_OF_TEN = {
+    1L,
+    10L,
+    100L,
+    1000L,
+    10000L,
+    100000L,
+    1000000L,
+    10000000L,
+    100000000L,
+    1000000000L,
+    10000000000L,
+    100000000000L,
+    1000000000000L,
+    10000000000000L,
+    100000000000000L,
+    1000000000000000L,
+    10000000000000000L,
+    100000000000000000L,
+    1000000000000000000L
+  };
+
+  @Override
+  public int maxRepresentableDigits() {
+    return Integer.MAX_VALUE;
+  }
+
+  public FormatQuantity1(long input) {
+    if (input < 0) {
+      setNegative(true);
+      input *= -1;
+    }
+
+    primary = input;
+    primaryScale = 0;
+    primaryPrecision = computePrecision(primary);
+    fallback = null;
+  }
+
+  /**
+   * Creates a FormatQuantity from the given double value. Internally attempts several strategies
+   * for converting the double to an exact representation, falling back on a BigDecimal if it fails
+   * to do so.
+   *
+   * @param input The double to represent by this FormatQuantity.
+   */
+  public FormatQuantity1(double input) {
+    if (input < 0) {
+      setNegative(true);
+      input *= -1;
+    }
+
+    // First try reading from IEEE bits. This is trivial only for doubles in [2^52, 2^64). If it
+    // fails, we wasted only a few CPU cycles.
+    long ieeeBits = Double.doubleToLongBits(input);
+    int exponent = (int) ((ieeeBits & 0x7ff0000000000000L) >> 52) - 0x3ff;
+    if (exponent >= 52 && exponent <= 63) {
+      // We can convert this double directly to a long.
+      long mantissa = (ieeeBits & 0x000fffffffffffffL) + 0x0010000000000000L;
+      primary = (mantissa << (exponent - 52));
+      primaryScale = 0;
+      primaryPrecision = computePrecision(primary);
+      return;
+    }
+
+    // Now try parsing the string produced by Double.toString().
+    String temp = Double.toString(input);
+    try {
+      if (temp.length() == 3 && temp.equals("0.0")) {
+        // Case 1: Zero.
+        primary = 0L;
+        primaryScale = 0;
+        primaryPrecision = 0;
+      } else if (temp.indexOf('E') != -1) {
+        // Case 2: Exponential notation.
+        assert temp.indexOf('.') == 1;
+        int expPos = temp.indexOf('E');
+        primary = Long.parseLong(temp.charAt(0) + temp.substring(2, expPos));
+        primaryScale = Integer.parseInt(temp.substring(expPos + 1)) - (expPos - 1) + 1;
+        primaryPrecision = expPos - 1;
+      } else if (temp.charAt(0) == '0') {
+        // Case 3: Fraction-only number.
+        assert temp.indexOf('.') == 1;
+        primary = Long.parseLong(temp.substring(2)); // ignores leading zeros
+        primaryScale = 2 - temp.length();
+        primaryPrecision = computePrecision(primary);
+      } else if (temp.charAt(temp.length() - 1) == '0') {
+        // Case 4: Integer-only number.
+        assert temp.indexOf('.') == temp.length() - 2;
+        int rightmostNonzeroDigitIndex = temp.length() - 3;
+        while (temp.charAt(rightmostNonzeroDigitIndex) == '0') {
+          rightmostNonzeroDigitIndex -= 1;
+        }
+        primary = Long.parseLong(temp.substring(0, rightmostNonzeroDigitIndex + 1));
+        primaryScale = temp.length() - rightmostNonzeroDigitIndex - 3;
+        primaryPrecision = rightmostNonzeroDigitIndex + 1;
+      } else if (temp.equals("Infinity")) {
+        // Case 5: Infinity.
+        primary = 0;
+        setInfinity(true);
+      } else if (temp.equals("NaN")) {
+        // Case 6: NaN.
+        primary = 0;
+        setNaN(true);
+      } else {
+        // Case 7: Number with both a fraction and an integer.
+        int decimalPos = temp.indexOf('.');
+        primary = Long.parseLong(temp.substring(0, decimalPos) + temp.substring(decimalPos + 1));
+        primaryScale = decimalPos - temp.length() + 1;
+        primaryPrecision = temp.length() - 1;
+      }
+    } catch (NumberFormatException e) {
+      // The digits of the double can't fit into the long.
+      primary = -1;
+      fallback = new BigDecimal(temp);
+    }
+  }
+
+  static final double LOG_2_OF_TEN = 3.32192809489;
+
+  public FormatQuantity1(double input, boolean fast) {
+    if (input < 0) {
+      setNegative(true);
+      input *= -1;
+    }
+
+    // Our strategy is to read all digits that are *guaranteed* to be valid without delving into
+    // the IEEE rounding rules.  This strategy might not end up with a perfect representation of
+    // the fractional part of the double.
+    long ieeeBits = Double.doubleToLongBits(input);
+    int exponent = (int) ((ieeeBits & 0x7ff0000000000000L) >> 52) - 0x3ff;
+    long mantissa = (ieeeBits & 0x000fffffffffffffL) + 0x0010000000000000L;
+    if (exponent > 63) {
+      throw new IllegalArgumentException(); // FIXME
+    } else if (exponent >= 52) {
+      primary = (mantissa << (exponent - 52));
+      primaryScale = 0;
+      primaryPrecision = computePrecision(primary);
+      return;
+    } else if (exponent >= 0) {
+      int shift = 52 - exponent;
+      primary = (mantissa >> shift); // integer part
+      int fractionCount = (int) (shift / LOG_2_OF_TEN);
+      long fraction = (mantissa - (primary << shift)) + 1L; // TODO: Explain the +1L
+      primary *= POWERS_OF_TEN[fractionCount];
+      for (int i = 0; i < fractionCount; i++) {
+        long times10 = (fraction * 10L);
+        long digit = times10 >> shift;
+        assert digit >= 0 && digit < 10;
+        primary += digit * POWERS_OF_TEN[fractionCount - i - 1];
+        fraction = times10 & ((1L << shift) - 1);
+      }
+      primaryScale = -fractionCount;
+      primaryPrecision = computePrecision(primary);
+    } else {
+      throw new IllegalArgumentException(); // FIXME
+    }
+  }
+
+  public FormatQuantity1(BigDecimal decimal) {
+    if (decimal.compareTo(BigDecimal.ZERO) < 0) {
+      setNegative(true);
+      decimal = decimal.negate();
+    }
+
+    primary = -1;
+    if (decimal.compareTo(BigDecimal.ZERO) == 0) {
+      fallback = BigDecimal.ZERO;
+    } else {
+      fallback = decimal;
+    }
+  }
+
+  public FormatQuantity1(FormatQuantity1 other) {
+    copyFrom(other);
+  }
+
+  @Override
+  public FormatQuantity clone() {
+    return new FormatQuantity1(this);
+  }
+
+  /**
+   * Make the internal state of this FormatQuantity equal to another FormatQuantity.
+   *
+   * @param other The template FormatQuantity. All properties from this FormatQuantity will be
+   *     copied into this FormatQuantity.
+   */
+  @Override
+  public void copyFrom(FormatQuantity other) {
+    // TODO: Check before casting
+    FormatQuantity1 _other = (FormatQuantity1) other;
+    lOptPos = _other.lOptPos;
+    lReqPos = _other.lReqPos;
+    rReqPos = _other.rReqPos;
+    rOptPos = _other.rOptPos;
+    primary = _other.primary;
+    primaryScale = _other.primaryScale;
+    primaryPrecision = _other.primaryPrecision;
+    fallback = _other.fallback;
+    flags = _other.flags;
+  }
+
+  @Override
+  public long getPositionFingerprint() {
+    long fingerprint = 0;
+    fingerprint ^= lOptPos;
+    fingerprint ^= (lReqPos << 16);
+    fingerprint ^= ((long) rReqPos << 32);
+    fingerprint ^= ((long) rOptPos << 48);
+    return fingerprint;
+  }
+
+  /**
+   * Utility method to compute the number of digits ("precision") in a long.
+   *
+   * @param input The long (which can't contain more than 19 digits).
+   * @return The precision of the long.
+   */
+  private static int computePrecision(long input) {
+    int precision = 0;
+    while (input > 0) {
+      input /= 10;
+      precision++;
+    }
+    return precision;
+  }
+
+  /**
+   * Changes the internal representation from a long to a BigDecimal. Used only for operations that
+   * don't support longs.
+   */
+  private void convertToBigDecimal() {
+    if (primary == -1) {
+      return;
+    }
+
+    fallback = new BigDecimal(primary).scaleByPowerOfTen(primaryScale);
+    primary = -1;
+  }
+
+  @Override
+  public void setIntegerFractionLength(int minInt, int maxInt, int minFrac, int maxFrac) {
+    // Graceful failures for bogus input
+    minInt = Math.max(0, minInt);
+    maxInt = Math.max(0, maxInt);
+    minFrac = Math.max(0, minFrac);
+    maxFrac = Math.max(0, maxFrac);
+
+    // The minima must be less than or equal to the maxima
+    if (maxInt < minInt) {
+      minInt = maxInt;
+    }
+    if (maxFrac < minFrac) {
+      minFrac = maxFrac;
+    }
+
+    // Displaying neither integer nor fraction digits is not allowed
+    if (maxInt == 0 && maxFrac == 0) {
+      maxInt = Integer.MAX_VALUE;
+      maxFrac = Integer.MAX_VALUE;
+    }
+
+    // Save values into internal state
+    // Negation is safe for minFrac/maxFrac because -Integer.MAX_VALUE > Integer.MIN_VALUE
+    lOptPos = maxInt;
+    lReqPos = minInt;
+    rReqPos = -minFrac;
+    rOptPos = -maxFrac;
+  }
+
+  @Override
+  public void roundToIncrement(BigDecimal roundingInterval, MathContext mathContext) {
+    BigDecimal d =
+        (primary == -1) ? fallback : new BigDecimal(primary).scaleByPowerOfTen(primaryScale);
+    if (isNegative()) d = d.negate();
+    d = d.divide(roundingInterval, 0, mathContext.getRoundingMode()).multiply(roundingInterval);
+    if (isNegative()) d = d.negate();
+    fallback = d;
+    primary = -1;
+  }
+
+  @Override
+  public void roundToMagnitude(int roundingMagnitude, MathContext mathContext) {
+    if (roundingMagnitude < -1000) {
+      roundToInfinity();
+      return;
+    }
+    if (primary == -1) {
+      if (isNegative()) fallback = fallback.negate();
+      fallback = fallback.setScale(-roundingMagnitude, mathContext.getRoundingMode());
+      if (isNegative()) fallback = fallback.negate();
+      // Enforce the math context.
+      fallback = fallback.round(mathContext);
+    } else {
+      int relativeScale = primaryScale - roundingMagnitude;
+      if (relativeScale < -18) {
+        // No digits will remain after rounding the number.
+        primary = 0L;
+        primaryScale = roundingMagnitude;
+        primaryPrecision = 0;
+      } else if (relativeScale < 0) {
+        // This is the harder case, when we need to perform the rounding logic.
+        // First check if the rightmost digits are already zero, where we can skip rounding.
+        if ((primary % POWERS_OF_TEN[0 - relativeScale]) == 0) {
+          // No rounding is necessary.
+        } else {
+          // TODO: Make this more efficient. Temporarily, convert to a BigDecimal and back again.
+          BigDecimal temp = new BigDecimal(primary).scaleByPowerOfTen(primaryScale);
+          if (isNegative()) temp = temp.negate();
+          temp = temp.setScale(-roundingMagnitude, mathContext.getRoundingMode());
+          if (isNegative()) temp = temp.negate();
+          temp = temp.scaleByPowerOfTen(-roundingMagnitude);
+          primary = temp.longValueExact(); // should never throw
+          primaryScale = roundingMagnitude;
+          primaryPrecision = computePrecision(primary);
+        }
+      } else {
+        // No rounding is necessary. All digits are to the left of the rounding magnitude.
+      }
+      // Enforce the math context.
+      primary = new BigDecimal(primary).round(mathContext).longValueExact();
+      primaryPrecision = computePrecision(primary);
+    }
+  }
+
+  @Override
+  public void roundToInfinity() {
+    // noop
+  }
+
+  /**
+   * Multiply the internal number by the specified multiplicand. This method forces the internal
+   * representation into a BigDecimal. If you are multiplying by a power of 10, use {@link
+   * #adjustMagnitude} instead.
+   *
+   * @param multiplicand The number to be passed to {@link BigDecimal#multiply}.
+   */
+  @Override
+  public void multiplyBy(BigDecimal multiplicand) {
+    convertToBigDecimal();
+    fallback = fallback.multiply(multiplicand);
+    if (fallback.compareTo(BigDecimal.ZERO) < 0) {
+      setNegative(!isNegative());
+      fallback = fallback.negate();
+    }
+  }
+
+  /**
+   * Divide the internal number by the specified quotient. This method forces the internal
+   * representation into a BigDecimal. If you are dividing by a power of 10, use {@link
+   * #adjustMagnitude} instead.
+   *
+   * @param divisor The number to be passed to {@link BigDecimal#divide}.
+   * @param scale The scale of the final rounded number. More negative means more decimal places.
+   * @param mathContext The math context to use if rounding is necessary.
+   */
+  private void divideBy(BigDecimal divisor, int scale, MathContext mathContext) {
+    convertToBigDecimal();
+    // Negate the scale because BigDecimal's scale is defined as the inverse of our scale
+    fallback = fallback.divide(divisor, -scale, mathContext.getRoundingMode());
+    if (fallback.compareTo(BigDecimal.ZERO) < 0) {
+      setNegative(!isNegative());
+      fallback = fallback.negate();
+    }
+  }
+
+  @Override
+  public boolean isZero() {
+    if (primary == -1) {
+      return fallback.compareTo(BigDecimal.ZERO) == 0;
+    } else {
+      return primary == 0;
+    }
+  }
+
+  /** @return The power of ten of the highest digit represented by this FormatQuantity */
+  @Override
+  public int getMagnitude() throws ArithmeticException {
+    int scale = (primary == -1) ? scaleBigDecimal(fallback) : primaryScale;
+    int precision = (primary == -1) ? precisionBigDecimal(fallback) : primaryPrecision;
+    if (precision == 0) {
+      throw new ArithmeticException("Magnitude is not well-defined for zero");
+    } else {
+      return scale + precision - 1;
+    }
+  }
+
+  /**
+   * Changes the magnitude of this FormatQuantity. If the indices of the represented digits had been
+   * previously specified, those indices are moved relative to the FormatQuantity.
+   *
+   * <p>This method does NOT perform rounding.
+   *
+   * @param delta The number of powers of ten to shift (positive shifts to the left).
+   */
+  @Override
+  public void adjustMagnitude(int delta) {
+    if (primary == -1) {
+      fallback = fallback.scaleByPowerOfTen(delta);
+    } else {
+      primaryScale = addOrMaxValue(primaryScale, delta);
+    }
+  }
+
+  private static int addOrMaxValue(int a, int b) {
+    // Check for overflow, and return min/max value if overflow occurs.
+    if (b < 0 && a + b > a) {
+      return Integer.MIN_VALUE;
+    } else if (b > 0 && a + b < a) {
+      return Integer.MAX_VALUE;
+    }
+    return a + b;
+  }
+
+  /** @return If the number represented by this FormatQuantity is less than zero */
+  @Override
+  public boolean isNegative() {
+    return (flags & NEGATIVE_FLAG) != 0;
+  }
+
+  private void setNegative(boolean isNegative) {
+    flags = (flags & (~NEGATIVE_FLAG)) | (isNegative ? NEGATIVE_FLAG : 0);
+  }
+
+  @Override
+  public boolean isInfinite() {
+    return (flags & INFINITY_FLAG) != 0;
+  }
+
+  private void setInfinity(boolean isInfinity) {
+    flags = (flags & (~INFINITY_FLAG)) | (isInfinity ? INFINITY_FLAG : 0);
+  }
+
+  @Override
+  public boolean isNaN() {
+    return (flags & NAN_FLAG) != 0;
+  }
+
+  private void setNaN(boolean isNaN) {
+    flags = (flags & (~NAN_FLAG)) | (isNaN ? NAN_FLAG : 0);
+  }
+
+  /**
+   * Returns a representation of this FormatQuantity as a double, with possible loss of information.
+   */
+  @Override
+  public double toDouble() {
+    double result;
+    if (primary == -1) {
+      result = fallback.doubleValue();
+    } else {
+      // TODO: Make this more efficient
+      result = primary;
+      for (int i = 0; i < primaryScale; i++) {
+        result *= 10.;
+      }
+      for (int i = 0; i > primaryScale; i--) {
+        result /= 10.;
+      }
+    }
+    return isNegative() ? -result : result;
+  }
+
+  @Override
+  public BigDecimal toBigDecimal() {
+    BigDecimal result;
+    if (primary != -1) {
+      result = new BigDecimal(primary).scaleByPowerOfTen(primaryScale);
+    } else {
+      result = fallback;
+    }
+    return isNegative() ? result.negate() : result;
+  }
+
+  /** @return */
+  @Override
+  public StandardPlural getStandardPlural(PluralRules rules) {
+    if (rules == null) {
+      // Fail gracefully if the user didn't provide a PluralRules
+      return StandardPlural.OTHER;
+    } else {
+      // TODO: Avoid converting to a double for the sake of PluralRules
+      String ruleString = rules.select(toDouble());
+      return StandardPlural.orOtherFromString(ruleString);
+    }
+  }
+
+  @Override
+  public double getPluralOperand(Operand operand) {
+    // TODO: This is a temporary hack.
+    return new PluralRules.FixedDecimal(toDouble()).getPluralOperand(operand);
+  }
+
+  /** @return */
+  public boolean hasNextFraction() {
+    if (rReqPos < 0) {
+      // We are in the required zone.
+      return true;
+    } else if (rOptPos >= 0) {
+      // We are in the forbidden zone.
+      return false;
+    } else {
+      // We are in the optional zone.
+      if (primary == -1) {
+        return fallback.remainder(BigDecimal.ONE).compareTo(BigDecimal.ZERO) > 0;
+      } else {
+        if (primaryScale <= -19) {
+          // The number is a fraction so small that it consists of only fraction digits.
+          return primary > 0;
+        } else if (primaryScale < 0) {
+          // Check if we have a fraction part.
+          long factor = POWERS_OF_TEN[0 - primaryScale];
+          return ((primary % factor) != 0);
+        } else {
+          // The lowest digit in the long has magnitude greater than -1.
+          return false;
+        }
+      }
+    }
+  }
+
+  /** @return */
+  public byte nextFraction() {
+    byte returnValue;
+    if (primary == -1) {
+      BigDecimal temp = fallback.multiply(BigDecimal.TEN);
+      returnValue = temp.setScale(0, RoundingMode.FLOOR).remainder(BigDecimal.TEN).byteValue();
+      fallback = fallback.setScale(0, RoundingMode.FLOOR).add(temp.remainder(BigDecimal.ONE));
+    } else {
+      if (primaryScale <= -20) {
+        // The number is a fraction so small that it has no first fraction digit.
+        primaryScale += 1;
+        returnValue = 0;
+      } else if (primaryScale < 0) {
+        // Extract the fraction digit out of the middle of the long.
+        long factor = POWERS_OF_TEN[0 - primaryScale - 1];
+        long temp1 = primary / factor;
+        long temp2 = primary % factor;
+        returnValue = (byte) (temp1 % 10); // not necessarily nonzero
+        primary = ((temp1 / 10) * factor) + temp2;
+        primaryScale += 1;
+        if (temp1 != 0) {
+          primaryPrecision -= 1;
+        }
+      } else {
+        // The lowest digit in the long has magnitude greater than -1.
+        returnValue = 0;
+      }
+    }
+
+    // Update digit brackets
+    if (lOptPos < 0) {
+      lOptPos += 1;
+    }
+    if (lReqPos < 0) {
+      lReqPos += 1;
+    }
+    if (rReqPos < 0) {
+      rReqPos += 1;
+    }
+    if (rOptPos < 0) {
+      rOptPos += 1;
+    }
+
+    assert returnValue >= 0;
+    return returnValue;
+  }
+
+  /** @return */
+  public boolean hasNextInteger() {
+    if (lReqPos > 0) {
+      // We are in the required zone.
+      return true;
+    } else if (lOptPos <= 0) {
+      // We are in the forbidden zone.
+      return false;
+    } else {
+      // We are in the optional zone.
+      if (primary == -1) {
+        return fallback.setScale(0, RoundingMode.FLOOR).compareTo(BigDecimal.ZERO) > 0;
+      } else {
+        if (primaryScale < -18) {
+          // The number is a fraction so small that it has no integer part.
+          return false;
+        } else if (primaryScale < 0) {
+          // Check if we have an integer part.
+          long factor = POWERS_OF_TEN[0 - primaryScale];
+          return ((primary % factor) != primary); // equivalent: ((primary / 10) != 0)
+        } else {
+          // The lowest digit in the long has magnitude of at least 0.
+          return primary != 0;
+        }
+      }
+    }
+  }
+
+  private int integerCount() {
+    int digitsRemaining;
+    if (primary == -1) {
+      digitsRemaining = precisionBigDecimal(fallback) + scaleBigDecimal(fallback);
+    } else {
+      digitsRemaining = primaryPrecision + primaryScale;
+    }
+    return Math.min(Math.max(digitsRemaining, lReqPos), lOptPos);
+  }
+
+  private int fractionCount() {
+    // TODO: This is temporary.
+    FormatQuantity1 copy = (FormatQuantity1) this.clone();
+    int fractionCount = 0;
+    while (copy.hasNextFraction()) {
+      copy.nextFraction();
+      fractionCount++;
+    }
+    return fractionCount;
+  }
+
+  @Override
+  public int getUpperDisplayMagnitude() {
+    return integerCount() - 1;
+  }
+
+  @Override
+  public int getLowerDisplayMagnitude() {
+    return -fractionCount();
+  }
+
+  //  @Override
+  //  public byte getIntegerDigit(int index) {
+  //    return getDigitPos(index);
+  //  }
+  //
+  //  @Override
+  //  public byte getFractionDigit(int index) {
+  //    return getDigitPos(-index - 1);
+  //  }
+
+  @Override
+  public byte getDigit(int magnitude) {
+    // TODO: This is temporary.
+    FormatQuantity1 copy = (FormatQuantity1) this.clone();
+    if (magnitude < 0) {
+      for (int p = -1; p > magnitude; p--) {
+        copy.nextFraction();
+      }
+      return copy.nextFraction();
+    } else {
+      for (int p = 0; p < magnitude; p++) {
+        copy.nextInteger();
+      }
+      return copy.nextInteger();
+    }
+  }
+
+  /** @return */
+  public byte nextInteger() {
+    byte returnValue;
+    if (primary == -1) {
+      returnValue = fallback.setScale(0, RoundingMode.FLOOR).remainder(BigDecimal.TEN).byteValue();
+      BigDecimal temp = fallback.divide(BigDecimal.TEN).setScale(0, RoundingMode.FLOOR);
+      fallback = fallback.remainder(BigDecimal.ONE).add(temp);
+    } else {
+      if (primaryScale < -18) {
+        // The number is a fraction so small that it has no integer part.
+        returnValue = 0;
+      } else if (primaryScale < 0) {
+        // Extract the integer digit out of the middle of the long. In many ways, this is the heart
+        // of the digit iterator algorithm.
+        long factor = POWERS_OF_TEN[0 - primaryScale];
+        if ((primary % factor) != primary) { // equivalent: ((primary / 10) != 0)
+          returnValue = (byte) ((primary / factor) % 10);
+          long temp = (primary / 10);
+          primary = temp - (temp % factor) + (primary % factor);
+          primaryPrecision -= 1;
+        } else {
+          returnValue = 0;
+        }
+      } else if (primaryScale == 0) {
+        // Fast-path for primaryScale == 0 (otherwise equivalent to previous step).
+        if (primary != 0) {
+          returnValue = (byte) (primary % 10);
+          primary /= 10;
+          primaryPrecision -= 1;
+        } else {
+          returnValue = 0;
+        }
+      } else {
+        // The lowest digit in the long has magnitude greater than 0.
+        primaryScale -= 1;
+        returnValue = 0;
+      }
+    }
+
+    // Update digit brackets
+    if (lOptPos > 0) {
+      lOptPos -= 1;
+    }
+    if (lReqPos > 0) {
+      lReqPos -= 1;
+    }
+    if (rReqPos > 0) {
+      rReqPos -= 1;
+    }
+    if (rOptPos > 0) {
+      rOptPos -= 1;
+    }
+
+    assert returnValue >= 0;
+    return returnValue;
+  }
+
+  /**
+   * Helper method to compute the precision of a BigDecimal by our definition of precision, which is
+   * that the number zero gets precision zero.
+   *
+   * @param decimal The BigDecimal whose precision to compute.
+   * @return The precision by our definition.
+   */
+  private static int precisionBigDecimal(BigDecimal decimal) {
+    if (decimal.compareTo(BigDecimal.ZERO) == 0) {
+      return 0;
+    } else {
+      return decimal.precision();
+    }
+  }
+
+  /**
+   * Helper method to compute the scale of a BigDecimal by our definition of scale, which is that
+   * deeper fractions result in negative scales as opposed to positive scales.
+   *
+   * @param decimal The BigDecimal whose scale to compute.
+   * @return The scale by our definition.
+   */
+  private static int scaleBigDecimal(BigDecimal decimal) {
+    return -decimal.scale();
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append("<FormatQuantity1 ");
+    if (primary == -1) {
+      sb.append(lOptPos > 1000 ? "max" : lOptPos);
+      sb.append(":");
+      sb.append(lReqPos);
+      sb.append(":");
+      sb.append(rReqPos);
+      sb.append(":");
+      sb.append(rOptPos < -1000 ? "min" : rOptPos);
+      sb.append(" ");
+      sb.append(fallback.toString());
+    } else {
+      String digits = Long.toString(primary);
+      int iDec = digits.length() + primaryScale;
+      int iLP = iDec - toRange(lOptPos, -1000, 1000);
+      int iLB = iDec - toRange(lReqPos, -1000, 1000);
+      int iRB = iDec - toRange(rReqPos, -1000, 1000);
+      int iRP = iDec - toRange(rOptPos, -1000, 1000);
+      iDec = Math.max(Math.min(iDec, digits.length() + 1), -1);
+      iLP = Math.max(Math.min(iLP, digits.length() + 1), -1);
+      iLB = Math.max(Math.min(iLB, digits.length() + 1), -1);
+      iRB = Math.max(Math.min(iRB, digits.length() + 1), -1);
+      iRP = Math.max(Math.min(iRP, digits.length() + 1), -1);
+
+      for (int i = -1; i <= digits.length() + 1; i++) {
+        if (i == iLP) sb.append('(');
+        if (i == iLB) sb.append('[');
+        if (i == iDec) sb.append('.');
+        if (i == iRB) sb.append(']');
+        if (i == iRP) sb.append(')');
+        if (i >= 0 && i < digits.length()) sb.append(digits.charAt(i));
+        else sb.append('\u00A0');
+      }
+    }
+    sb.append(">");
+    return sb.toString();
+  }
+
+  private static int toRange(int i, int lo, int hi) {
+    if (i < lo) {
+      return lo;
+    } else if (i > hi) {
+      return hi;
+    } else {
+      return i;
+    }
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity2.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity2.java
new file mode 100644 (file)
index 0000000..3093169
--- /dev/null
@@ -0,0 +1,173 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+public final class FormatQuantity2 extends FormatQuantityBCD {
+
+  /**
+   * The BCD of the 16 digits of the number represented by this object. Every 4 bits of the long map
+   * to one digit. For example, the number "12345" in BCD is "0x12345".
+   *
+   * <p>Whenever bcd changes internally, {@link #compact()} must be called, except in special cases
+   * like setting the digit to zero.
+   */
+  private long bcd;
+
+  @Override
+  public int maxRepresentableDigits() {
+    return 16;
+  }
+
+  public FormatQuantity2(long input) {
+    setToLong(input);
+  }
+
+  public FormatQuantity2(int input) {
+    setToInt(input);
+  }
+
+  public FormatQuantity2(double input) {
+    setToDouble(input);
+  }
+
+  public FormatQuantity2(BigInteger input) {
+    setToBigInteger(input);
+  }
+
+  public FormatQuantity2(BigDecimal input) {
+    setToBigDecimal(input);
+  }
+
+  public FormatQuantity2(FormatQuantity2 other) {
+    copyFrom(other);
+  }
+
+  @Override
+  protected byte getDigitPos(int position) {
+    if (position < 0 || position >= 16) return 0;
+    return (byte) ((bcd >>> (position * 4)) & 0xf);
+  }
+
+  @Override
+  protected void setDigitPos(int position, byte value) {
+    assert position >= 0 && position < 16;
+    int shift = position * 4;
+    bcd = bcd & ~(0xfL << shift) | ((long) value << shift);
+  }
+
+  @Override
+  protected void shiftLeft(int numDigits) {
+    assert precision + numDigits <= 16;
+    bcd <<= (numDigits * 4);
+    scale -= numDigits;
+    precision += numDigits;
+  }
+
+  @Override
+  protected void shiftRight(int numDigits) {
+    bcd >>>= (numDigits * 4);
+    scale += numDigits;
+    precision -= numDigits;
+  }
+
+  @Override
+  protected void setBcdToZero() {
+    bcd = 0L;
+    scale = 0;
+    precision = 0;
+    isApproximate = false;
+    origDouble = 0;
+    origDelta = 0;
+  }
+
+  @Override
+  protected void readIntToBcd(int n) {
+    long result = 0L;
+    int i = 16;
+    for (; n != 0; n /= 10, i--) {
+      result = (result >>> 4) + (((long) n % 10) << 60);
+    }
+    // ints can't overflow the 16 digits in the BCD, so scale is always zero
+    bcd = result >>> (i * 4);
+    scale = 0;
+    precision = 16 - i;
+  }
+
+  @Override
+  protected void readLongToBcd(long n) {
+    long result = 0L;
+    int i = 16;
+    for (; n != 0L; n /= 10L, i--) {
+      result = (result >>> 4) + ((n % 10) << 60);
+    }
+    int adjustment = (i > 0) ? i : 0;
+    bcd = result >>> (adjustment * 4);
+    scale = (i < 0) ? -i : 0;
+    precision = 16 - i;
+  }
+
+  @Override
+  protected void readBigIntegerToBcd(BigInteger n) {
+    long result = 0L;
+    int i = 16;
+    for (; n.signum() != 0; i--) {
+      BigInteger[] temp = n.divideAndRemainder(BigInteger.TEN);
+      result = (result >>> 4) + (temp[1].longValue() << 60);
+      n = temp[0];
+    }
+    int adjustment = (i > 0) ? i : 0;
+    bcd = result >>> (adjustment * 4);
+    scale = (i < 0) ? -i : 0;
+  }
+
+  @Override
+  protected BigDecimal bcdToBigDecimal() {
+    long tempLong = 0L;
+    for (int shift = (precision - 1); shift >= 0; shift--) {
+      tempLong = tempLong * 10 + getDigitPos(shift);
+    }
+    BigDecimal result = BigDecimal.valueOf(tempLong);
+    result = result.scaleByPowerOfTen(scale);
+    if (isNegative()) result = result.negate();
+    return result;
+  }
+
+  @Override
+  protected void compact() {
+    // Special handling for 0
+    if (bcd == 0L) {
+      scale = 0;
+      precision = 0;
+      return;
+    }
+
+    // Compact the number (remove trailing zeros)
+    int delta = Long.numberOfTrailingZeros(bcd) / 4;
+    bcd >>>= delta * 4;
+    scale += delta;
+
+    // Compute precision
+    precision = 16 - (Long.numberOfLeadingZeros(bcd) / 4);
+  }
+
+  @Override
+  protected void copyBcdFrom(FormatQuantity _other) {
+    FormatQuantity2 other = (FormatQuantity2) _other;
+    bcd = other.bcd;
+  }
+
+  @Override
+  public String toString() {
+    return String.format(
+        "<FormatQuantity2 %s:%d:%d:%s %016XE%d>",
+        (lOptPos > 1000 ? "max" : String.valueOf(lOptPos)),
+        lReqPos,
+        rReqPos,
+        (rOptPos < -1000 ? "min" : String.valueOf(rOptPos)),
+        bcd,
+        scale);
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity3.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity3.java
new file mode 100644 (file)
index 0000000..9e25a45
--- /dev/null
@@ -0,0 +1,222 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+public final class FormatQuantity3 extends FormatQuantityBCD {
+
+  /**
+   * The BCD of the 16 digits of the number represented by this object. Every 4 bits of the long map
+   * to one digit. For example, the number "12345" in BCD is "0x12345".
+   *
+   * <p>Whenever bcd changes internally, {@link #compact()} must be called, except in special cases
+   * like setting the digit to zero.
+   */
+  private byte[] bcd = new byte[100];
+
+  @Override
+  public int maxRepresentableDigits() {
+    return Integer.MAX_VALUE;
+  }
+
+  public FormatQuantity3(long input) {
+    setToLong(input);
+  }
+
+  public FormatQuantity3(int input) {
+    setToInt(input);
+  }
+
+  public FormatQuantity3(double input) {
+    setToDouble(input);
+  }
+
+  public FormatQuantity3(BigInteger input) {
+    setToBigInteger(input);
+  }
+
+  public FormatQuantity3(BigDecimal input) {
+    setToBigDecimal(input);
+  }
+
+  public FormatQuantity3(FormatQuantity3 other) {
+    copyFrom(other);
+  }
+
+  @Override
+  protected byte getDigitPos(int position) {
+    if (position < 0 || position > precision) return 0;
+    return bcd[position];
+  }
+
+  @Override
+  protected void setDigitPos(int position, byte value) {
+    assert position >= 0;
+    ensureCapacity(position + 1);
+    bcd[position] = value;
+  }
+
+  @Override
+  protected void shiftLeft(int numDigits) {
+    ensureCapacity(precision + numDigits);
+    int i = precision + numDigits - 1;
+    for (; i >= numDigits; i--) {
+      bcd[i] = bcd[i - numDigits];
+    }
+    for (; i >= 0; i--) {
+      bcd[i] = 0;
+    }
+    scale -= numDigits;
+    precision += numDigits;
+  }
+
+  @Override
+  protected void shiftRight(int numDigits) {
+    int i = 0;
+    for (; i < precision - numDigits; i++) {
+      bcd[i] = bcd[i + numDigits];
+    }
+    for (; i < precision; i++) {
+      bcd[i] = 0;
+    }
+    scale += numDigits;
+    precision -= numDigits;
+  }
+
+  @Override
+  protected void setBcdToZero() {
+    for (int i = 0; i < precision; i++) {
+      bcd[i] = (byte) 0;
+    }
+    scale = 0;
+    precision = 0;
+    isApproximate = false;
+    origDouble = 0;
+    origDelta = 0;
+  }
+
+  @Override
+  protected void readIntToBcd(int n) {
+    int i = 0;
+    for (; n != 0L; n /= 10L, i++) {
+      bcd[i] = (byte) (n % 10);
+    }
+    scale = 0;
+    precision = i;
+  }
+
+  private static final byte[] LONG_MIN_VALUE =
+      new byte[] {8, 0, 8, 5, 7, 7, 4, 5, 8, 6, 3, 0, 2, 7, 3, 3, 2, 2, 9};
+
+  @Override
+  protected void readLongToBcd(long n) {
+    if (n == Long.MIN_VALUE) {
+      // Can't consume via the normal path.
+      System.arraycopy(LONG_MIN_VALUE, 0, bcd, 0, LONG_MIN_VALUE.length);
+      scale = 0;
+      precision = LONG_MIN_VALUE.length;
+      return;
+    }
+    int i = 0;
+    for (; n != 0L; n /= 10L, i++) {
+      bcd[i] = (byte) (n % 10);
+    }
+    scale = 0;
+    precision = i;
+  }
+
+  @Override
+  protected void readBigIntegerToBcd(BigInteger n) {
+    int i = 0;
+    for (; n.signum() != 0; i++) {
+      BigInteger[] temp = n.divideAndRemainder(BigInteger.TEN);
+      ensureCapacity(i + 1);
+      bcd[i] = temp[1].byteValue();
+      n = temp[0];
+    }
+    scale = 0;
+    precision = i;
+  }
+
+  @Override
+  protected BigDecimal bcdToBigDecimal() {
+    // Converting to a string here is faster than doing BigInteger/BigDecimal arithmetic.
+    return new BigDecimal(toDumbString());
+  }
+
+  private String toDumbString() {
+    StringBuilder sb = new StringBuilder();
+    if (isNegative()) sb.append('-');
+    if (precision == 0) {
+      sb.append('0');
+      return sb.toString();
+    }
+    for (int i = precision - 1; i >= 0; i--) {
+      sb.append(getDigitPos(i));
+    }
+    if (scale != 0) {
+      sb.append('E');
+      sb.append(scale);
+    }
+    return sb.toString();
+  }
+
+  @Override
+  protected void compact() {
+    // Special handling for 0
+    boolean isZero = true;
+    for (int i = 0; i < precision; i++) {
+      if (bcd[i] != 0) {
+        isZero = false;
+        break;
+      }
+    }
+    if (isZero) {
+      scale = 0;
+      precision = 0;
+      return;
+    }
+
+    // Compact the number (remove trailing zeros)
+    int delta = 0;
+    for (; bcd[delta] == 0; delta++) ;
+    shiftRight(delta);
+
+    // Compute precision
+    int leading = precision - 1;
+    for (; leading >= 0 && bcd[leading] == 0; leading--) ;
+    precision = leading + 1;
+  }
+
+  private void ensureCapacity(int capacity) {
+    if (bcd.length >= capacity) return;
+    byte[] bcd1 = new byte[capacity * 2];
+    System.arraycopy(bcd, 0, bcd1, 0, bcd.length);
+    bcd = bcd1;
+  }
+
+  @Override
+  protected void copyBcdFrom(FormatQuantity _other) {
+    FormatQuantity3 other = (FormatQuantity3) _other;
+    System.arraycopy(other.bcd, 0, bcd, 0, bcd.length);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    for (int i = 30; i >= 0; i--) {
+      sb.append(bcd[i]);
+    }
+    return String.format(
+        "<FormatQuantity3 %s:%d:%d:%s %s%s%d>",
+        (lOptPos > 1000 ? "max" : String.valueOf(lOptPos)),
+        lReqPos,
+        rReqPos,
+        (rOptPos < -1000 ? "min" : String.valueOf(rOptPos)),
+        sb,
+        "E",
+        scale);
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity4.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity4.java
new file mode 100644 (file)
index 0000000..335b78f
--- /dev/null
@@ -0,0 +1,407 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+public final class FormatQuantity4 extends FormatQuantityBCD {
+
+  /**
+   * The BCD of the 16 digits of the number represented by this object. Every 4 bits of the long map
+   * to one digit. For example, the number "12345" in BCD is "0x12345".
+   *
+   * <p>Whenever bcd changes internally, {@link #compact()} must be called, except in special cases
+   * like setting the digit to zero.
+   */
+  private byte[] bcdBytes;
+
+  private long bcdLong = 0L;
+
+  private boolean usingBytes = false;;
+
+  @Override
+  public int maxRepresentableDigits() {
+    return Integer.MAX_VALUE;
+  }
+
+  public FormatQuantity4() {
+    setBcdToZero();
+  }
+
+  public FormatQuantity4(long input) {
+    setToLong(input);
+  }
+
+  public FormatQuantity4(int input) {
+    setToInt(input);
+  }
+
+  public FormatQuantity4(double input) {
+    setToDouble(input);
+  }
+
+  public FormatQuantity4(BigInteger input) {
+    setToBigInteger(input);
+  }
+
+  public FormatQuantity4(BigDecimal input) {
+    setToBigDecimal(input);
+  }
+
+  public FormatQuantity4(FormatQuantity4 other) {
+    copyFrom(other);
+  }
+
+  public FormatQuantity4(Number number) {
+    if (number instanceof Long) {
+      setToLong(number.longValue());
+    } else if (number instanceof Integer) {
+      setToInt(number.intValue());
+    } else if (number instanceof Double) {
+      setToDouble(number.doubleValue());
+    } else if (number instanceof BigInteger) {
+      setToBigInteger((BigInteger) number);
+    } else if (number instanceof BigDecimal) {
+      setToBigDecimal((BigDecimal) number);
+    } else if (number instanceof com.ibm.icu.math.BigDecimal) {
+      setToBigDecimal(((com.ibm.icu.math.BigDecimal) number).toBigDecimal());
+    } else {
+      throw new IllegalArgumentException(
+          "Number is of an unsupported type: " + number.getClass().getName());
+    }
+  }
+
+  @Override
+  protected byte getDigitPos(int position) {
+    if (usingBytes) {
+      if (position < 0 || position > precision) return 0;
+      return bcdBytes[position];
+    } else {
+      if (position < 0 || position >= 16) return 0;
+      return (byte) ((bcdLong >>> (position * 4)) & 0xf);
+    }
+  }
+
+  @Override
+  protected void setDigitPos(int position, byte value) {
+    assert position >= 0;
+    if (usingBytes) {
+      ensureCapacity(position + 1);
+      bcdBytes[position] = value;
+    } else if (position >= 16) {
+      switchStorage();
+      ensureCapacity(position + 1);
+      bcdBytes[position] = value;
+    } else {
+      int shift = position * 4;
+      bcdLong = bcdLong & ~(0xfL << shift) | ((long) value << shift);
+    }
+  }
+
+  @Override
+  protected void shiftLeft(int numDigits) {
+    if (!usingBytes && precision + numDigits > 16) {
+      switchStorage();
+    }
+    if (usingBytes) {
+      ensureCapacity(precision + numDigits);
+      int i = precision + numDigits - 1;
+      for (; i >= numDigits; i--) {
+        bcdBytes[i] = bcdBytes[i - numDigits];
+      }
+      for (; i >= 0; i--) {
+        bcdBytes[i] = 0;
+      }
+    } else {
+      bcdLong <<= (numDigits * 4);
+    }
+    scale -= numDigits;
+    precision += numDigits;
+  }
+
+  @Override
+  protected void shiftRight(int numDigits) {
+    if (usingBytes) {
+      int i = 0;
+      for (; i < precision - numDigits; i++) {
+        bcdBytes[i] = bcdBytes[i + numDigits];
+      }
+      for (; i < precision; i++) {
+        bcdBytes[i] = 0;
+      }
+    } else {
+      bcdLong >>>= (numDigits * 4);
+    }
+    scale += numDigits;
+    precision -= numDigits;
+  }
+
+  @Override
+  protected void setBcdToZero() {
+    if (usingBytes) {
+      for (int i = 0; i < precision; i++) {
+        bcdBytes[i] = (byte) 0;
+      }
+    }
+    usingBytes = false;
+    bcdLong = 0L;
+    scale = 0;
+    precision = 0;
+    isApproximate = false;
+    origDouble = 0;
+    origDelta = 0;
+  }
+
+  @Override
+  protected void readIntToBcd(int n) {
+    // ints always fit inside the long implementation.
+    long result = 0L;
+    int i = 16;
+    for (; n != 0; n /= 10, i--) {
+      result = (result >>> 4) + (((long) n % 10) << 60);
+    }
+    usingBytes = false;
+    bcdLong = result >>> (i * 4);
+    scale = 0;
+    precision = 16 - i;
+  }
+
+  @Override
+  protected void readLongToBcd(long n) {
+    if (n >= 10000000000000000L) {
+      ensureCapacity();
+      int i = 0;
+      for (; n != 0L; n /= 10L, i++) {
+        bcdBytes[i] = (byte) (n % 10);
+      }
+      usingBytes = true;
+      scale = 0;
+      precision = i;
+    } else {
+      long result = 0L;
+      int i = 16;
+      for (; n != 0L; n /= 10L, i--) {
+        result = (result >>> 4) + ((n % 10) << 60);
+      }
+      assert i >= 0;
+      usingBytes = false;
+      bcdLong = result >>> (i * 4);
+      scale = 0;
+      precision = 16 - i;
+    }
+  }
+
+  @Override
+  protected void readBigIntegerToBcd(BigInteger n) {
+    ensureCapacity(); // allocate initial byte array
+    int i = 0;
+    for (; n.signum() != 0; i++) {
+      BigInteger[] temp = n.divideAndRemainder(BigInteger.TEN);
+      ensureCapacity(i + 1);
+      bcdBytes[i] = temp[1].byteValue();
+      n = temp[0];
+    }
+    usingBytes = true;
+    scale = 0;
+    precision = i;
+  }
+
+  @Override
+  protected BigDecimal bcdToBigDecimal() {
+    if (usingBytes) {
+      // Converting to a string here is faster than doing BigInteger/BigDecimal arithmetic.
+      StringBuilder sb = new StringBuilder();
+      if (isNegative()) sb.append('-');
+      assert precision > 0;
+      for (int i = precision - 1; i >= 0; i--) {
+        sb.append(getDigitPos(i));
+      }
+      if (scale != 0) {
+        sb.append('E');
+        sb.append(scale);
+      }
+      return new BigDecimal(sb.toString());
+    } else {
+      long tempLong = 0L;
+      for (int shift = (precision - 1); shift >= 0; shift--) {
+        tempLong = tempLong * 10 + getDigitPos(shift);
+      }
+      BigDecimal result = BigDecimal.valueOf(tempLong);
+      result = result.scaleByPowerOfTen(scale);
+      if (isNegative()) result = result.negate();
+      return result;
+    }
+  }
+
+  @Override
+  protected void compact() {
+    if (usingBytes) {
+      int delta = 0;
+      for (; delta < precision && bcdBytes[delta] == 0; delta++) ;
+      if (delta == precision) {
+        // Number is zero
+        setBcdToZero();
+        return;
+      } else {
+        // Remove trailing zeros
+        shiftRight(delta);
+      }
+
+      // Compute precision
+      int leading = precision - 1;
+      for (; leading >= 0 && bcdBytes[leading] == 0; leading--) ;
+      precision = leading + 1;
+
+      // Switch storage mechanism if possible
+      if (precision <= 16) {
+        switchStorage();
+      }
+
+    } else {
+      if (bcdLong == 0L) {
+        // Number is zero
+        setBcdToZero();
+        return;
+      }
+
+      // Compact the number (remove trailing zeros)
+      int delta = Long.numberOfTrailingZeros(bcdLong) / 4;
+      bcdLong >>>= delta * 4;
+      scale += delta;
+
+      // Compute precision
+      precision = 16 - (Long.numberOfLeadingZeros(bcdLong) / 4);
+    }
+  }
+
+  /** Ensure that a byte array of at least 40 digits is allocated. */
+  private void ensureCapacity() {
+    ensureCapacity(40);
+  }
+
+  private void ensureCapacity(int capacity) {
+    if (bcdBytes == null && capacity > 0) {
+      bcdBytes = new byte[capacity];
+    } else if (bcdBytes.length < capacity) {
+      byte[] bcd1 = new byte[capacity * 2];
+      System.arraycopy(bcdBytes, 0, bcd1, 0, bcdBytes.length);
+      bcdBytes = bcd1;
+    }
+  }
+
+  /** Switches the internal storage mechanism between the 64-bit long and the byte array. */
+  private void switchStorage() {
+    if (usingBytes) {
+      // Change from bytes to long
+      bcdLong = 0L;
+      for (int i = precision - 1; i >= 0; i--) {
+        bcdLong <<= 4;
+        bcdLong |= bcdBytes[i];
+        bcdBytes[i] = 0;
+      }
+      usingBytes = false;
+    } else {
+      // Change from long to bytes
+      ensureCapacity();
+      for (int i = 0; i < precision; i++) {
+        bcdBytes[i] = (byte) (bcdLong & 0xf);
+        bcdLong >>>= 4;
+      }
+      usingBytes = true;
+    }
+  }
+
+  @Override
+  protected void copyBcdFrom(FormatQuantity _other) {
+    FormatQuantity4 other = (FormatQuantity4) _other;
+    if (other.usingBytes) {
+      usingBytes = true;
+      ensureCapacity(other.precision);
+      System.arraycopy(other.bcdBytes, 0, bcdBytes, 0, other.precision);
+    } else {
+      usingBytes = false;
+      bcdLong = other.bcdLong;
+    }
+  }
+
+  /**
+   * Checks whether the bytes stored in this instance are all valid. For internal unit testing only.
+   *
+   * @return An error message if this instance is invalid, or null if this instance is healthy.
+   * @internal
+   * @deprecated This API is for ICU internal use only.
+   */
+  @Deprecated
+  public String checkHealth() {
+    if (usingBytes) {
+      if (bcdLong != 0) return "Value in bcdLong but we are in byte mode";
+      if (precision == 0) return "Zero precision but we are in byte mode";
+      if (precision > bcdBytes.length) return "Precision exceeds length of byte array";
+      if (getDigitPos(precision - 1) == 0) return "Most significant digit is zero in byte mode";
+      if (getDigitPos(0) == 0) return "Least significant digit is zero in long mode";
+      for (int i = 0; i < precision; i++) {
+        if (getDigitPos(i) >= 10) return "Digit exceeding 10 in byte array";
+        if (getDigitPos(i) < 0) return "Digit below 0 in byte array";
+      }
+      for (int i = precision; i < bcdBytes.length; i++) {
+        if (getDigitPos(i) != 0) return "Nonzero digits outside of range in byte array";
+      }
+    } else {
+      if (bcdBytes != null) {
+        for (int i = 0; i < bcdBytes.length; i++) {
+          if (bcdBytes[i] != 0) return "Nonzero digits in byte array but we are in long mode";
+        }
+      }
+      if (precision == 0 && bcdLong != 0) return "Value in bcdLong even though precision is zero";
+      if (precision > 16) return "Precision exceeds length of long";
+      if (precision != 0 && getDigitPos(precision - 1) == 0)
+        return "Most significant digit is zero in long mode";
+      if (precision != 0 && getDigitPos(0) == 0)
+        return "Least significant digit is zero in long mode";
+      for (int i = 0; i < precision; i++) {
+        if (getDigitPos(i) >= 10) return "Digit exceeding 10 in long";
+        if (getDigitPos(i) < 0) return "Digit below 0 in long (?!)";
+      }
+      for (int i = precision; i < 16; i++) {
+        if (getDigitPos(i) != 0) return "Nonzero digits outside of range in long";
+      }
+    }
+
+    return null;
+  }
+
+  /**
+   * Checks whether this {@link FormatQuantity4} is using its internal byte array storage mechanism.
+   *
+   * @return true if an internal byte array is being used; false if a long is being used.
+   * @internal
+   * @deprecated This API is ICU internal only.
+   */
+  @Deprecated
+  public boolean usingBytes() {
+    return usingBytes;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    if (usingBytes) {
+      for (int i = precision - 1; i >= 0; i--) {
+        sb.append(bcdBytes[i]);
+      }
+    } else {
+      sb.append(Long.toHexString(bcdLong));
+    }
+    return String.format(
+        "<FormatQuantity4 %s:%d:%d:%s %s %s%s%d>",
+        (lOptPos > 1000 ? "max" : String.valueOf(lOptPos)),
+        lReqPos,
+        rReqPos,
+        (rOptPos < -1000 ? "min" : String.valueOf(rOptPos)),
+        (usingBytes ? "bytes" : "long"),
+        sb,
+        "E",
+        scale);
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantityBCD.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantityBCD.java
new file mode 100644 (file)
index 0000000..6d58319
--- /dev/null
@@ -0,0 +1,900 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.MathContext;
+import java.text.FieldPosition;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.text.PluralRules;
+import com.ibm.icu.text.PluralRules.Operand;
+import com.ibm.icu.text.UFieldPosition;
+
+/**
+ * Represents numbers and digit display properties using Binary Coded Decimal (BCD).
+ *
+ * @implements {@link FormatQuantity}
+ */
+public abstract class FormatQuantityBCD implements FormatQuantity {
+
+  /**
+   * The power of ten corresponding to the least significant digit in the BCD. For example, if this
+   * object represents the number "3.14", the BCD will be "0x314" and the scale will be -2.
+   *
+   * <p>Note that in {@link java.math.BigDecimal}, the scale is defined differently: the number of
+   * digits after the decimal place, which is the negative of our definition of scale.
+   */
+  protected int scale;
+
+  /**
+   * The number of digits in the BCD. For example, "1007" has BCD "0x1007" and precision 4. The
+   * maximum precision is 16 since a long can hold only 16 digits.
+   *
+   * <p>This value must be re-calculated whenever the value in bcd changes by using {@link
+   * #computePrecisionAndCompact()}.
+   */
+  protected int precision;
+
+  /**
+   * A bitmask of properties relating to the number represented by this object.
+   *
+   * @see #NEGATIVE_FLAG
+   * @see #INFINITY_FLAG
+   * @see #NAN_FLAG
+   */
+  protected int flags;
+
+  protected static final int NEGATIVE_FLAG = 1;
+  protected static final int INFINITY_FLAG = 2;
+  protected static final int NAN_FLAG = 4;
+
+  /**
+   * The original number provided by the user and which is represented in BCD. Used when we need to
+   * re-compute the BCD for an exact double representation.
+   */
+  protected double origDouble;
+
+  protected int origDelta;
+  protected boolean isApproximate;
+
+  // Four positions: left optional '(', left required '[', right required ']', right optional ')'.
+  // These four positions determine which digits are displayed in the output string.  They do NOT
+  // affect rounding.  These positions are internal-only and can be specified only by the public
+  // endpoints like setFractionLength, setIntegerLength, and setSignificantDigits, among others.
+  //
+  //   * Digits between lReqPos and rReqPos are in the "required zone" and are always displayed.
+  //   * Digits between lOptPos and rOptPos but outside the required zone are in the "optional zone"
+  //     and are displayed unless they are trailing off the left or right edge of the number and
+  //     have a numerical value of zero.  In order to be "trailing", the digits need to be beyond
+  //     the decimal point in their respective directions.
+  //   * Digits outside of the "optional zone" are never displayed.
+  //
+  // See the table below for illustrative examples.
+  //
+  // +---------+---------+---------+---------+------------+------------------------+--------------+
+  // | lOptPos | lReqPos | rReqPos | rOptPos |   number   |        positions       | en-US string |
+  // +---------+---------+---------+---------+------------+------------------------+--------------+
+  // |    5    |    2    |   -1    |   -5    |   1234.567 |     ( 12[34.5]67  )    |   1,234.567  |
+  // |    3    |    2    |   -1    |   -5    |   1234.567 |      1(2[34.5]67  )    |     234.567  |
+  // |    3    |    2    |   -1    |   -2    |   1234.567 |      1(2[34.5]6)7      |     234.56   |
+  // |    6    |    4    |    2    |   -5    | 123456789. |  123(45[67]89.     )   | 456,789.     |
+  // |    6    |    4    |    2    |    1    | 123456789. |     123(45[67]8)9.     | 456,780.     |
+  // |   -1    |   -1    |   -3    |   -4    | 0.123456   |     0.1([23]4)56       |        .0234 |
+  // |    6    |    4    |   -2    |   -2    |     12.3   |     (  [  12.3 ])      |    0012.30   |
+  // +---------+---------+---------+---------+------------+------------------------+--------------+
+  //
+  protected int lOptPos = Integer.MAX_VALUE;
+  protected int lReqPos = 0;
+  protected int rReqPos = 0;
+  protected int rOptPos = Integer.MIN_VALUE;
+
+  @Override
+  public void copyFrom(FormatQuantity _other) {
+    copyBcdFrom(_other);
+    FormatQuantityBCD other = (FormatQuantityBCD) _other;
+    lOptPos = other.lOptPos;
+    lReqPos = other.lReqPos;
+    rReqPos = other.rReqPos;
+    rOptPos = other.rOptPos;
+    scale = other.scale;
+    precision = other.precision;
+    flags = other.flags;
+    origDouble = other.origDouble;
+    origDelta = other.origDelta;
+    isApproximate = other.isApproximate;
+  }
+
+  public FormatQuantityBCD clear() {
+    lOptPos = Integer.MAX_VALUE;
+    lReqPos = 0;
+    rReqPos = 0;
+    rOptPos = Integer.MIN_VALUE;
+    flags = 0;
+    setBcdToZero(); // sets scale, precision, hasDouble, origDouble, origDelta, and BCD data
+    return this;
+  }
+
+  @Override
+  public void setIntegerFractionLength(int minInt, int maxInt, int minFrac, int maxFrac) {
+    // Graceful failures for bogus input
+    minInt = Math.max(0, minInt);
+    maxInt = Math.max(0, maxInt);
+    minFrac = Math.max(0, minFrac);
+    maxFrac = Math.max(0, maxFrac);
+
+    // The minima must be less than or equal to the maxima
+    if (maxInt < minInt) {
+      minInt = maxInt;
+    }
+    if (maxFrac < minFrac) {
+      minFrac = maxFrac;
+    }
+
+    // Displaying neither integer nor fraction digits is not allowed
+    if (maxInt == 0 && maxFrac == 0) {
+      maxInt = Integer.MAX_VALUE;
+      maxFrac = Integer.MAX_VALUE;
+    }
+
+    // Save values into internal state
+    // Negation is safe for minFrac/maxFrac because -Integer.MAX_VALUE > Integer.MIN_VALUE
+    lOptPos = maxInt;
+    lReqPos = minInt;
+    rReqPos = -minFrac;
+    rOptPos = -maxFrac;
+  }
+
+  @Override
+  public long getPositionFingerprint() {
+    long fingerprint = 0;
+    fingerprint ^= lOptPos;
+    fingerprint ^= (lReqPos << 16);
+    fingerprint ^= ((long) rReqPos << 32);
+    fingerprint ^= ((long) rOptPos << 48);
+    return fingerprint;
+  }
+
+  @Override
+  public void roundToIncrement(BigDecimal roundingInterval, MathContext mathContext) {
+    // TODO: Avoid converting back and forth to BigDecimal.
+    BigDecimal temp = toBigDecimal();
+    temp =
+        temp.divide(roundingInterval, 0, mathContext.getRoundingMode())
+            .multiply(roundingInterval)
+            .round(mathContext);
+    if (temp.signum() == 0) {
+      setBcdToZero(); // keeps negative flag for -0.0
+    } else {
+      setToBigDecimal(temp);
+    }
+  }
+
+  @Override
+  public void multiplyBy(BigDecimal multiplicand) {
+    BigDecimal temp = toBigDecimal();
+    temp = temp.multiply(multiplicand);
+    setToBigDecimal(temp);
+  }
+
+  @Override
+  public int getMagnitude() throws ArithmeticException {
+    if (precision == 0) {
+      throw new ArithmeticException("Magnitude is not well-defined for zero");
+    } else {
+      return scale + precision - 1;
+    }
+  }
+
+  @Override
+  public void adjustMagnitude(int delta) {
+    if (precision != 0) {
+      scale += delta;
+      origDelta += delta;
+    }
+  }
+
+  @Override
+  public StandardPlural getStandardPlural(PluralRules rules) {
+    if (rules == null) {
+      // Fail gracefully if the user didn't provide a PluralRules
+      return StandardPlural.OTHER;
+    } else {
+      @SuppressWarnings("deprecation")
+      String ruleString = rules.select(this);
+      return StandardPlural.orOtherFromString(ruleString);
+    }
+  }
+
+  @Override
+  public double getPluralOperand(Operand operand) {
+    switch (operand) {
+      case i:
+        return toLong();
+      case f:
+        return toFractionLong(true);
+      case t:
+        return toFractionLong(false);
+      case v:
+        return fractionCount();
+      case w:
+        return fractionCountWithoutTrailingZeros();
+      default:
+        return Math.abs(toDouble());
+    }
+  }
+
+  /**
+   * If the given {@link FieldPosition} is a {@link UFieldPosition}, populates it with the fraction
+   * length and fraction long value. If the argument is not a {@link UFieldPosition}, nothing
+   * happens.
+   *
+   * @param fp The {@link UFieldPosition} to populate.
+   */
+  public void populateUFieldPosition(FieldPosition fp) {
+    if (fp instanceof UFieldPosition) {
+      ((UFieldPosition) fp)
+          .setFractionDigits((int) getPluralOperand(Operand.v), (long) getPluralOperand(Operand.f));
+    }
+  }
+
+  @Override
+  public int getUpperDisplayMagnitude() {
+    int magnitude = scale + precision;
+    int result = (lReqPos > magnitude) ? lReqPos : (lOptPos < magnitude) ? lOptPos : magnitude;
+    return result - 1;
+  }
+
+  @Override
+  public int getLowerDisplayMagnitude() {
+    int magnitude = scale;
+    int result = (rReqPos < magnitude) ? rReqPos : (rOptPos > magnitude) ? rOptPos : magnitude;
+    return result;
+  }
+
+  @Override
+  public byte getDigit(int magnitude) {
+    return getDigitPos(magnitude - scale);
+  }
+
+  private int fractionCount() {
+    return -getLowerDisplayMagnitude();
+  }
+
+  private int fractionCountWithoutTrailingZeros() {
+    return Math.max(-scale, 0);
+  }
+
+  @Override
+  public boolean isNegative() {
+    return (flags & NEGATIVE_FLAG) != 0;
+  }
+
+  @Override
+  public boolean isInfinite() {
+    return (flags & INFINITY_FLAG) != 0;
+  }
+
+  @Override
+  public boolean isNaN() {
+    return (flags & NAN_FLAG) != 0;
+  }
+
+  @Override
+  public boolean isZero() {
+    return precision == 0;
+  }
+
+  @Override
+  public FormatQuantity clone() {
+    if (this instanceof FormatQuantity2) {
+      return new FormatQuantity2((FormatQuantity2) this);
+    } else if (this instanceof FormatQuantity3) {
+      return new FormatQuantity3((FormatQuantity3) this);
+    } else if (this instanceof FormatQuantity4) {
+      return new FormatQuantity4((FormatQuantity4) this);
+    } else {
+      throw new IllegalArgumentException("Don't know how to clone " + this.getClass());
+    }
+  }
+
+  public void setToInt(int n) {
+    setBcdToZero();
+    flags = 0;
+    if (n < 0) {
+      flags |= NEGATIVE_FLAG;
+      n = -n;
+    }
+    if (n != 0) {
+      _setToInt(n);
+      compact();
+    }
+  }
+
+  private void _setToInt(int n) {
+    if (n == Integer.MIN_VALUE) {
+      readLongToBcd(-(long) n);
+    } else {
+      readIntToBcd(n);
+    }
+  }
+
+  public void setToLong(long n) {
+    setBcdToZero();
+    flags = 0;
+    if (n < 0) {
+      flags |= NEGATIVE_FLAG;
+      n = -n;
+    }
+    if (n != 0) {
+      _setToLong(n);
+      compact();
+    }
+  }
+
+  private void _setToLong(long n) {
+    if (n == Long.MIN_VALUE) {
+      readBigIntegerToBcd(BigInteger.valueOf(n).negate());
+    } else if (n <= Integer.MAX_VALUE) {
+      readIntToBcd((int) n);
+    } else {
+      readLongToBcd(n);
+    }
+  }
+
+  public void setToBigInteger(BigInteger n) {
+    setBcdToZero();
+    flags = 0;
+    if (n.signum() == -1) {
+      flags |= NEGATIVE_FLAG;
+      n = n.negate();
+    }
+    if (n.signum() != 0) {
+      _setToBigInteger(n);
+      compact();
+    }
+  }
+
+  private void _setToBigInteger(BigInteger n) {
+    if (n.bitLength() < 32) {
+      readIntToBcd(n.intValue());
+    } else if (n.bitLength() < 64) {
+      readLongToBcd(n.longValue());
+    } else {
+      readBigIntegerToBcd(n);
+    }
+  }
+
+  /**
+   * Sets the internal BCD state to represent the value in the given double.
+   *
+   * @param n The value to consume.
+   */
+  public void setToDouble(double n) {
+    setBcdToZero();
+    flags = 0;
+    // Double.compare() handles +0.0 vs -0.0
+    if (Double.compare(n, 0.0) < 0) {
+      flags |= NEGATIVE_FLAG;
+      n = -n;
+    }
+    if (Double.isNaN(n)) {
+      flags |= NAN_FLAG;
+    } else if (Double.isInfinite(n)) {
+      flags |= INFINITY_FLAG;
+    } else if (n != 0) {
+      _setToDoubleFast(n);
+
+      // TODO: Remove this when finished testing.
+      //      isApproximate = true;
+      //      origDouble = n;
+      //      origDelta = 0;
+      //      convertToAccurateDouble();
+
+      compact();
+    }
+  }
+
+  private static final double[] DOUBLE_MULTIPLIERS = {
+    1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16,
+    1e17, 1e18, 1e19, 1e20, 1e21
+  };
+
+  /**
+   * Uses double multiplication and division to get the number into integer space before converting
+   * to digits. Since double arithmetic is inexact, the resulting digits may not be accurate.
+   */
+  private void _setToDoubleFast(double n) {
+    long ieeeBits = Double.doubleToLongBits(n);
+    int exponent = (int) ((ieeeBits & 0x7ff0000000000000L) >> 52) - 0x3ff;
+
+    // Not all integers can be represented exactly for exponent > 52
+    if (exponent <= 52 && (long) n == n) {
+      _setToLong((long) n);
+      return;
+    }
+
+    isApproximate = true;
+    origDouble = n;
+    origDelta = 0;
+
+    int fracLength = (int) ((52 - exponent) / 3.32192809489);
+    if (fracLength >= 0) {
+      int i = fracLength;
+      // 1e22 is the largest exact double.
+      for (; i >= 22; i -= 22) n *= 1e22;
+      n *= DOUBLE_MULTIPLIERS[i];
+    } else {
+      int i = fracLength;
+      // 1e22 is the largest exact double.
+      for (; i <= -22; i += 22) n /= 1e22;
+      n /= DOUBLE_MULTIPLIERS[-i];
+    }
+    _setToLong(Math.round(n));
+    scale -= fracLength;
+  }
+
+  /**
+   * Uses Double.toString() to obtain an exact accurate representation of the double, overwriting it
+   * into the BCD. This method can be called at any point after {@link #_setToDoubleFast} while
+   * {@link #isApproximate} is still true.
+   */
+  private void convertToAccurateDouble() {
+    double n = origDouble;
+    assert n != 0;
+    int delta = origDelta;
+    setBcdToZero();
+
+    // Call the slow oracle function
+    String temp = Double.toString(n);
+
+    if (temp.indexOf('E') != -1) {
+      // Case 1: Exponential notation.
+      assert temp.indexOf('.') == 1;
+      int expPos = temp.indexOf('E');
+      _setToLong(Long.parseLong(temp.charAt(0) + temp.substring(2, expPos)));
+      scale += Integer.parseInt(temp.substring(expPos + 1)) - (expPos - 1) + 1;
+    } else if (temp.charAt(0) == '0') {
+      // Case 2: Fraction-only number.
+      assert temp.indexOf('.') == 1;
+      _setToLong(Long.parseLong(temp.substring(2)));
+      scale += 2 - temp.length();
+    } else if (temp.charAt(temp.length() - 1) == '0') {
+      // Case 3: Integer-only number.
+      // Note: this path should not normally happen, because integer-only numbers are captured
+      // before the approximate double logic is performed.
+      assert temp.indexOf('.') == temp.length() - 2;
+      assert temp.length() - 2 <= 18;
+      _setToLong(Long.parseLong(temp.substring(0, temp.length() - 2)));
+      // no need to adjust scale
+    } else {
+      // Case 4: Number with both a fraction and an integer.
+      int decimalPos = temp.indexOf('.');
+      _setToLong(Long.parseLong(temp.substring(0, decimalPos) + temp.substring(decimalPos + 1)));
+      scale += decimalPos - temp.length() + 1;
+    }
+    scale += delta;
+    compact();
+    explicitExactDouble = true;
+  }
+
+  /**
+   * Whether this {@link FormatQuantity4} has been explicitly converted to an exact double. true if
+   * backed by a double that was explicitly converted via convertToAccurateDouble; false otherwise.
+   * Used for testing.
+   *
+   * @internal
+   * @deprecated This API is ICU internal only.
+   */
+  @Deprecated public boolean explicitExactDouble = false;
+
+  /**
+   * Sets the internal BCD state to represent the value in the given BigDecimal.
+   *
+   * @param n The value to consume.
+   */
+  public void setToBigDecimal(BigDecimal n) {
+    setBcdToZero();
+    flags = 0;
+    if (n.signum() == -1) {
+      flags |= NEGATIVE_FLAG;
+      n = n.negate();
+    }
+    if (n.signum() != 0) {
+      _setToBigDecimal(n);
+      compact();
+    }
+  }
+
+  private void _setToBigDecimal(BigDecimal n) {
+    int fracLength = n.scale();
+    n = n.scaleByPowerOfTen(fracLength);
+    BigInteger bi = n.toBigInteger();
+    _setToBigInteger(bi);
+    scale -= fracLength;
+  }
+
+  /**
+   * Returns a long approximating the internal BCD. A long can only represent the integral part of
+   * the number.
+   *
+   * @return A double representation of the internal BCD.
+   */
+  protected long toLong() {
+    long result = 0L;
+    for (int magnitude = scale + precision - 1; magnitude >= 0; magnitude--) {
+      result = result * 10 + getDigitPos(magnitude - scale);
+    }
+    return result;
+  }
+
+  /**
+   * This returns a long representing the fraction digits of the number, as required by PluralRules.
+   * For example, if we represent the number "1.20" (including optional and required digits), then
+   * this function returns "20" if includeTrailingZeros is true or "2" if false.
+   */
+  protected long toFractionLong(boolean includeTrailingZeros) {
+    long result = 0L;
+    int magnitude = -1;
+    for (;
+        (magnitude >= scale || (includeTrailingZeros && magnitude >= rReqPos))
+            && magnitude >= rOptPos;
+        magnitude--) {
+      result = result * 10 + getDigitPos(magnitude - scale);
+    }
+    return result;
+  }
+
+  /**
+   * Returns a double approximating the internal BCD. The double may not retain all of the
+   * information encoded in the BCD if the BCD represents a number out of range of a double.
+   *
+   * @return A double representation of the internal BCD.
+   */
+  @Override
+  public double toDouble() {
+    if (isApproximate) {
+      return toDoubleFromOriginal();
+    }
+
+    if (isNaN()) {
+      return Double.NaN;
+    } else if (isInfinite()) {
+      return isNegative() ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY;
+    }
+
+    long tempLong = 0L;
+    int lostDigits = precision - Math.min(precision, 17);
+    for (int shift = precision - 1; shift >= lostDigits; shift--) {
+      tempLong = tempLong * 10 + getDigitPos(shift);
+    }
+    double result = tempLong;
+    int _scale = scale + lostDigits;
+    if (_scale >= 0) {
+      int i = _scale;
+      for (; i >= 9; i -= 9) result *= 1000000000;
+      for (; i >= 3; i -= 3) result *= 1000;
+      for (; i >= 1; i -= 1) result *= 10;
+    } else {
+      int i = _scale;
+      for (; i <= -9; i += 9) result /= 1000000000;
+      for (; i <= -3; i += 3) result /= 1000;
+      for (; i <= -1; i += 1) result /= 10;
+    }
+    if (isNegative()) result = -result;
+    return result;
+  }
+
+  @Override
+  public BigDecimal toBigDecimal() {
+    if (isApproximate) {
+      // Converting to a BigDecimal requires Double.toString().
+      convertToAccurateDouble();
+    }
+    return bcdToBigDecimal();
+  }
+
+  protected double toDoubleFromOriginal() {
+    double result = origDouble;
+    double delta = origDelta;
+    if (delta >= 0) {
+      for (; delta >= 9; delta -= 9) result *= 1000000000;
+      for (; delta >= 3; delta -= 3) result *= 1000;
+      for (; delta >= 1; delta -= 1) result *= 10;
+    } else {
+      for (; delta <= -9; delta += 9) result /= 1000000000;
+      for (; delta <= -3; delta += 3) result /= 1000;
+      for (; delta <= -1; delta += 1) result /= 10;
+    }
+    if (isNegative()) result *= -1;
+    return result;
+  }
+
+  private static int safeSubtract(int a, int b) {
+    if (b < 0 && a - b < a) return Integer.MAX_VALUE;
+    if (b > 0 && a - b > a) return Integer.MIN_VALUE;
+    return a - b;
+  }
+
+  @Override
+  public void roundToMagnitude(int magnitude, MathContext mathContext) {
+    // The position in the BCD at which rounding will be performed; digits to the right of position
+    // will be rounded away.
+    // TODO: Andy: There was a test failure because of integer overflow here. Should I do
+    // "safe subtraction" everywhere in the code?  What's the nicest way to do it?
+    int position = safeSubtract(magnitude, scale);
+
+    // Enforce the number of digits required by the MathContext.
+    int _mcPrecision = mathContext.getPrecision();
+    if (magnitude == Integer.MAX_VALUE
+        || (_mcPrecision > 0 && precision - position > _mcPrecision)) {
+      position = precision - _mcPrecision;
+    }
+
+    if (position <= 0 && !isApproximate) {
+      // All digits are to the left of the rounding magnitude.
+    } else if (precision == 0) {
+      // No rounding for zero.
+    } else {
+      // Perform rounding logic.
+      // "leading" = most significant digit to the right of rounding
+      // "trailing" = least significant digit to the left of rounding
+      byte leadingDigit = getDigitPos(safeSubtract(position, 1));
+      byte trailingDigit = getDigitPos(position);
+
+      // Compute which section of the number we are in.
+      // EDGE means we are at the bottom or top edge, like 1.000 or 1.999 (used by doubles)
+      // LOWER means we are between the bottom edge and the midpoint, like 1.391
+      // MIDPOINT means we are exactly in the middle, like 1.500
+      // UPPER means we are between the midpoint and the top edge, like 1.916
+      int section = RoundingUtils.SECTION_MIDPOINT;
+      if (!isApproximate) {
+        if (leadingDigit < 5) {
+          section = RoundingUtils.SECTION_LOWER;
+        } else if (leadingDigit > 5) {
+          section = RoundingUtils.SECTION_UPPER;
+        } else {
+          for (int p = safeSubtract(position, 2); p >= 0; p--) {
+            if (getDigitPos(p) != 0) {
+              section = RoundingUtils.SECTION_UPPER;
+              break;
+            }
+          }
+        }
+      } else {
+        int p = safeSubtract(position, 2);
+        int minP = Math.max(0, precision - 14);
+        if (leadingDigit == 0) {
+          section = -1;
+          for (; p >= minP; p--) {
+            if (getDigitPos(p) != 0) {
+              section = RoundingUtils.SECTION_LOWER;
+              break;
+            }
+          }
+        } else if (leadingDigit == 4) {
+          for (; p >= minP; p--) {
+            if (getDigitPos(p) != 9) {
+              section = RoundingUtils.SECTION_LOWER;
+              break;
+            }
+          }
+        } else if (leadingDigit == 5) {
+          for (; p >= minP; p--) {
+            if (getDigitPos(p) != 0) {
+              section = RoundingUtils.SECTION_UPPER;
+              break;
+            }
+          }
+        } else if (leadingDigit == 9) {
+          section = -2;
+          for (; p >= minP; p--) {
+            if (getDigitPos(p) != 9) {
+              section = RoundingUtils.SECTION_UPPER;
+              break;
+            }
+          }
+        } else if (leadingDigit < 5) {
+          section = RoundingUtils.SECTION_LOWER;
+        } else {
+          section = RoundingUtils.SECTION_UPPER;
+        }
+
+        boolean roundsAtMidpoint =
+            RoundingUtils.roundsAtMidpoint(mathContext.getRoundingMode().ordinal());
+        if (safeSubtract(position, 1) < precision - 14
+            || (roundsAtMidpoint && section == RoundingUtils.SECTION_MIDPOINT)
+            || (!roundsAtMidpoint && section < 0 /* i.e. at upper or lower edge */)) {
+          // Oops! This means that we have to get the exact representation of the double, because
+          // the zone of uncertainty is along the rounding boundary.
+          convertToAccurateDouble();
+          roundToMagnitude(magnitude, mathContext); // start over
+          return;
+        }
+
+        // Turn off the approximate double flag, since the value is now confirmed to be exact.
+        isApproximate = false;
+        origDouble = 0.0;
+        origDelta = 0;
+
+        if (position <= 0) {
+          // All digits are to the left of the rounding magnitude.
+          return;
+        }
+
+        // Good to continue rounding.
+        if (section == -1) section = RoundingUtils.SECTION_LOWER;
+        if (section == -2) section = RoundingUtils.SECTION_UPPER;
+      }
+
+      boolean roundDown =
+          RoundingUtils.getRoundingDirection(
+              (trailingDigit % 2) == 0,
+              isNegative(),
+              section,
+              mathContext.getRoundingMode().ordinal(),
+              this);
+
+      // Perform truncation
+      if (position >= precision) {
+        setBcdToZero();
+        scale = magnitude;
+      } else {
+        shiftRight(position);
+      }
+
+      // Bubble the result to the higher digits
+      if (!roundDown) {
+        if (trailingDigit == 9) {
+          int bubblePos = 0;
+          // Note: in the long implementation, the most digits BCD can have at this point is 15,
+          // so bubblePos <= 15 and getDigitPos(bubblePos) is safe.
+          for (; getDigitPos(bubblePos) == 9; bubblePos++) {}
+          shiftRight(bubblePos); // shift off the trailing 9s
+        }
+        byte digit0 = getDigitPos(0);
+        assert digit0 != 9;
+        setDigitPos(0, (byte) (digit0 + 1));
+        precision += 1; // in case an extra digit got added
+      }
+
+      compact();
+    }
+  }
+
+  @Override
+  public void roundToInfinity() {
+    if (isApproximate) {
+      convertToAccurateDouble();
+    }
+  }
+
+  /**
+   * Appends a digit, optionally with one or more leading zeros, to the end of the value represented
+   * by this FormatQuantity.
+   *
+   * <p>The primary use of this method is to construct numbers during a parsing loop. It allows
+   * parsing to take advantage of the digit list infrastructure primarily designed for formatting.
+   *
+   * @param value The digit to append.
+   * @param leadingZeros The number of zeros to append before the digit. For example, if the value
+   *     in this instance starts as 12.3, and you append a 4 with 1 leading zero, the value becomes
+   *     12.304.
+   * @param appendAsInteger If true, increase the magnitude of existing digits to make room for the
+   *     new digit. If false, append to the end like a fraction digit. If true, there must not be
+   *     any fraction digits already in the number.
+   * @internal
+   * @deprecated This API is ICU internal only.
+   */
+  @Deprecated
+  public void appendDigit(byte value, int leadingZeros, boolean appendAsInteger) {
+    assert leadingZeros >= 0;
+
+    // Zero requires special handling to maintain the invariant that the least-significant digit
+    // in the BCD is nonzero.
+    if (value == 0) {
+      if (appendAsInteger && precision != 0) {
+        scale += leadingZeros + 1;
+      }
+      return;
+    }
+
+    // Deal with trailing zeros
+    if (scale > 0) {
+      leadingZeros += scale;
+      if (appendAsInteger) {
+        scale = 0;
+      }
+    }
+
+    // Append digit
+    shiftLeft(leadingZeros + 1);
+    setDigitPos(0, value);
+
+    // Fix scale if in integer mode
+    if (appendAsInteger) {
+      scale += leadingZeros + 1;
+    }
+  }
+
+  /**
+   * Returns a single digit from the BCD list. No internal state is changed by calling this method.
+   *
+   * @param position The position of the digit to pop, counted in BCD units from the least
+   *     significant digit. If outside the range supported by the implementation, zero is returned.
+   * @return The digit at the specified location.
+   */
+  protected abstract byte getDigitPos(int position);
+
+  /**
+   * Sets the digit in the BCD list. This method only sets the digit; it is the caller's
+   * responsibility to call {@link #compact} after setting the digit.
+   *
+   * @param position The position of the digit to pop, counted in BCD units from the least
+   *     significant digit. If outside the range supported by the implementation, an AssertionError
+   *     is thrown.
+   * @param value The digit to set at the specified location.
+   */
+  protected abstract void setDigitPos(int position, byte value);
+
+  /**
+   * Adds zeros to the end of the BCD list. This will result in an invalid BCD representation; it is
+   * the caller's responsibility to do further manipulation and then call {@link #compact}.
+   *
+   * @param numDigits The number of zeros to add.
+   */
+  protected abstract void shiftLeft(int numDigits);
+
+  protected abstract void shiftRight(int numDigits);
+
+  /**
+   * Sets the internal representation to zero. Clears any values stored in scale, precision,
+   * hasDouble, origDouble, origDelta, and BCD data.
+   */
+  protected abstract void setBcdToZero();
+
+  /**
+   * Sets the internal BCD state to represent the value in the given int. The int is guaranteed to
+   * be either positive. The internal state is guaranteed to be empty when this method is called.
+   *
+   * @param n The value to consume.
+   */
+  protected abstract void readIntToBcd(int input);
+
+  /**
+   * Sets the internal BCD state to represent the value in the given long. The long is guaranteed to
+   * be either positive. The internal state is guaranteed to be empty when this method is called.
+   *
+   * @param n The value to consume.
+   */
+  protected abstract void readLongToBcd(long input);
+
+  /**
+   * Sets the internal BCD state to represent the value in the given BigInteger. The BigInteger is
+   * guaranteed to be positive, and it is guaranteed to be larger than Long.MAX_VALUE. The internal
+   * state is guaranteed to be empty when this method is called.
+   *
+   * @param n The value to consume.
+   */
+  protected abstract void readBigIntegerToBcd(BigInteger input);
+
+  /**
+   * Returns a BigDecimal encoding the internal BCD value.
+   *
+   * @return A BigDecimal representation of the internal BCD.
+   */
+  protected abstract BigDecimal bcdToBigDecimal();
+
+  protected abstract void copyBcdFrom(FormatQuantity _other);
+
+  /**
+   * Removes trailing zeros from the BCD (adjusting the scale as required) and then computes the
+   * precision. The precision is the number of digits in the number up through the greatest nonzero
+   * digit.
+   *
+   * <p>This method must always be called when bcd changes in order for assumptions to be correct in
+   * methods like {@link #fractionCount()}.
+   */
+  protected abstract void compact();
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantitySelector.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantitySelector.java
new file mode 100644 (file)
index 0000000..95a11ba
--- /dev/null
@@ -0,0 +1,52 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+/** @author sffc */
+public class FormatQuantitySelector {
+  public static FormatQuantityBCD from(int input) {
+    return new FormatQuantity4(input);
+  }
+
+  public static FormatQuantityBCD from(long input) {
+    return new FormatQuantity4(input);
+  }
+
+  public static FormatQuantityBCD from(double input) {
+    return new FormatQuantity4(input);
+  }
+
+  public static FormatQuantityBCD from(BigInteger input) {
+    return new FormatQuantity4(input);
+  }
+
+  public static FormatQuantityBCD from(BigDecimal input) {
+    return new FormatQuantity4(input);
+  }
+
+  public static FormatQuantityBCD from(com.ibm.icu.math.BigDecimal input) {
+    return from(input.toBigDecimal());
+  }
+
+  public static FormatQuantityBCD from(Number number) {
+    if (number instanceof Long) {
+      return from(number.longValue());
+    } else if (number instanceof Integer) {
+      return from(number.intValue());
+    } else if (number instanceof Double) {
+      return from(number.doubleValue());
+    } else if (number instanceof BigInteger) {
+      return from((BigInteger) number);
+    } else if (number instanceof BigDecimal) {
+      return from((BigDecimal) number);
+    } else if (number instanceof com.ibm.icu.math.BigDecimal) {
+      return from((com.ibm.icu.math.BigDecimal) number);
+    } else {
+      throw new IllegalArgumentException(
+          "Number is of an unsupported type: " + number.getClass().getName());
+    }
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java
new file mode 100644 (file)
index 0000000..00739fd
--- /dev/null
@@ -0,0 +1,128 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier;
+import com.ibm.icu.impl.number.modifiers.GeneralPluralModifier;
+import com.ibm.icu.impl.number.modifiers.PositiveNegativeAffixModifier;
+import com.ibm.icu.impl.number.modifiers.SimpleModifier;
+
+/**
+ * A Modifier is an immutable object that can be passed through the formatting pipeline until it is
+ * finally applied to the string builder. A Modifier usually contains a prefix and a suffix that are
+ * applied, but it could contain something else, like a {@link com.ibm.icu.text.SimpleFormatter}
+ * pattern.
+ *
+ * @see PositiveNegativeAffixModifier
+ * @see ConstantAffixModifier
+ * @see GeneralPluralModifier
+ * @see SimpleModifier
+ */
+public interface Modifier {
+
+  /**
+   * Apply this Modifier to the string builder.
+   *
+   * @param output The string builder to which to apply this modifier.
+   * @param leftIndex The left index of the string within the builder. Equal to 0 when only one
+   *     number is being formatted.
+   * @param rightIndex The right index of the string within the string builder. Equal to length-1
+   *     when only one number is being formatted.
+   * @return The number of characters (UTF-16 code units) that were added to the string builder.
+   */
+  public int apply(NumberStringBuilder output, int leftIndex, int rightIndex);
+
+  /**
+   * The number of characters that {@link #apply} would add to the string builder.
+   *
+   * @return The number of characters (UTF-16 code units) that would be added to a string builder.
+   */
+  public int length();
+
+  /**
+   * Whether this modifier is strong. If a modifier is strong, it should always be applied
+   * immediately and not allowed to bubble up. With regard to padding, strong modifiers are
+   * considered to be on the inside of the prefix and suffix.
+   *
+   * @return Whether the modifier is strong.
+   */
+  public boolean isStrong();
+
+  /**
+   * Gets the prefix string associated with this modifier, defined as the string that will be
+   * inserted at leftIndex when {@link #apply} is called.
+   *
+   * @return The prefix string. Will not be null.
+   */
+  public String getPrefix();
+
+  /**
+   * Gets the prefix string associated with this modifier, defined as the string that will be
+   * inserted at rightIndex when {@link #apply} is called.
+   *
+   * @return The suffix string. Will not be null.
+   */
+  public String getSuffix();
+
+  /**
+   * An interface for a modifier that contains both a positive and a negative form. Note that a
+   * class implementing {@link PositiveNegativeModifier} is not necessarily a {@link Modifier}
+   * itself. Rather, it returns a {@link Modifier} when {@link #getModifier} is called.
+   */
+  public static interface PositiveNegativeModifier extends Exportable {
+    /**
+     * Converts this {@link PositiveNegativeModifier} to a {@link Modifier} given the negative sign.
+     *
+     * @param isNegative true if the negative form of this modifier should be used; false if the
+     *     positive form should be used.
+     * @return A Modifier corresponding to the negative sign.
+     */
+    public Modifier getModifier(boolean isNegative);
+  }
+
+  /**
+   * An interface for a modifier that contains both a positive and a negative form for all six
+   * standard plurals. Note that a class implementing {@link PositiveNegativePluralModifier} is not
+   * necessarily a {@link Modifier} itself. Rather, it returns a {@link Modifier} when {@link
+   * #getModifier} is called.
+   */
+  public static interface PositiveNegativePluralModifier extends Exportable {
+    /**
+     * Converts this {@link PositiveNegativePluralModifier} to a {@link Modifier} given the negative
+     * sign and the standard plural.
+     *
+     * @param plural The StandardPlural to use.
+     * @param isNegative true if the negative form of this modifier should be used; false if the
+     *     positive form should be used.
+     * @return A Modifier corresponding to the negative sign.
+     */
+    public Modifier getModifier(StandardPlural plural, boolean isNegative);
+  }
+
+  /**
+   * An interface for a modifier that is represented internally by a prefix string and a suffix
+   * string.
+   */
+  public static interface AffixModifier extends Modifier {}
+
+  /**
+   * A starter implementation with defaults for some of the basic methods.
+   *
+   * <p>Implements {@link PositiveNegativeModifier} only so that instances of this class can be used when
+   * a {@link PositiveNegativeModifier} is required.
+   */
+  public abstract static class BaseModifier extends Format.BeforeFormat
+      implements Modifier, PositiveNegativeModifier {
+
+    @Override
+    public void before(FormatQuantity input, ModifierHolder mods) {
+      mods.add(this);
+    }
+
+    @Override
+    public Modifier getModifier(boolean isNegative) {
+      return this;
+    }
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ModifierHolder.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ModifierHolder.java
new file mode 100644 (file)
index 0000000..483d6fd
--- /dev/null
@@ -0,0 +1,106 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.util.ArrayDeque;
+
+public class ModifierHolder {
+  private ArrayDeque<Modifier> mods = new ArrayDeque<Modifier>();
+
+  // Using five separate fields instead of the ArrayDeque saves about 10ns at the expense of
+  // worse code.
+  // TODO: Decide which implementation to use.
+
+  //    private Modifier mod1 = null;
+  //    private Modifier mod2 = null;
+  //    private Modifier mod3 = null;
+  //    private Modifier mod4 = null;
+  //    private Modifier mod5 = null;
+
+  public ModifierHolder clear() {
+    //      mod1 = null;
+    //      mod2 = null;
+    //      mod3 = null;
+    //      mod4 = null;
+    //      mod5 = null;
+    mods.clear();
+    return this;
+  }
+
+  public void add(Modifier modifier) {
+    //      if (mod1 == null) {
+    //        mod1 = modifier;
+    //      } else if (mod2 == null) {
+    //        mod2 = modifier;
+    //      } else if (mod3 == null) {
+    //        mod3 = modifier;
+    //      } else if (mod4 == null) {
+    //        mod4 = modifier;
+    //      } else if (mod5 == null) {
+    //        mod5 = modifier;
+    //      } else {
+    //        throw new IndexOutOfBoundsException();
+    //      }
+    if (modifier != null) mods.addFirst(modifier);
+  }
+
+  public Modifier peekLast() {
+    return mods.peekLast();
+  }
+
+  public Modifier removeLast() {
+    return mods.removeLast();
+  }
+
+  public int applyAll(NumberStringBuilder string, int leftIndex, int rightIndex) {
+    int addedLength = 0;
+    //      if (mod5 != null) {
+    //        addedLength += mod5.apply(string, leftIndex, rightIndex + addedLength);
+    //        mod5 = null;
+    //      }
+    //      if (mod4 != null) {
+    //        addedLength += mod4.apply(string, leftIndex, rightIndex + addedLength);
+    //        mod4 = null;
+    //      }
+    //      if (mod3 != null) {
+    //        addedLength += mod3.apply(string, leftIndex, rightIndex + addedLength);
+    //        mod3 = null;
+    //      }
+    //      if (mod2 != null) {
+    //        addedLength += mod2.apply(string, leftIndex, rightIndex + addedLength);
+    //        mod2 = null;
+    //      }
+    //      if (mod1 != null) {
+    //        addedLength += mod1.apply(string, leftIndex, rightIndex + addedLength);
+    //        mod1 = null;
+    //      }
+    while (!mods.isEmpty()) {
+      Modifier mod = mods.removeFirst();
+      addedLength += mod.apply(string, leftIndex, rightIndex + addedLength);
+    }
+    return addedLength;
+  }
+
+  public int applyStrong(NumberStringBuilder string, int leftIndex, int rightIndex) {
+    int addedLength = 0;
+    while (!mods.isEmpty() && mods.peekFirst().isStrong()) {
+      Modifier mod = mods.removeFirst();
+      addedLength += mod.apply(string, leftIndex, rightIndex + addedLength);
+    }
+    return addedLength;
+  }
+
+  public int totalLength() {
+    int length = 0;
+    //      if (mod1 != null) length += mod1.length();
+    //      if (mod2 != null) length += mod2.length();
+    //      if (mod3 != null) length += mod3.length();
+    //      if (mod4 != null) length += mod4.length();
+    //      if (mod5 != null) length += mod5.length();
+    for (Modifier mod : mods) {
+      if (mod == null) continue;
+      length += mod.length();
+    }
+    return length;
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/NumberStringBuilder.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/NumberStringBuilder.java
new file mode 100644 (file)
index 0000000..4008307
--- /dev/null
@@ -0,0 +1,411 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.text.AttributedCharacterIterator;
+import java.text.AttributedString;
+import java.text.FieldPosition;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.ibm.icu.text.NumberFormat;
+import com.ibm.icu.text.NumberFormat.Field;
+
+public class NumberStringBuilder implements CharSequence {
+  private char[] chars;
+  private Field[] fields;
+  private int zero;
+  private int length;
+
+  public NumberStringBuilder() {
+    this(40);
+  }
+
+  public NumberStringBuilder(int capacity) {
+    chars = new char[capacity];
+    fields = new Field[capacity];
+    zero = capacity / 2;
+    length = 0;
+  }
+
+  @Override
+  public int length() {
+    return length;
+  }
+
+  @Override
+  public char charAt(int index) {
+    if (index < 0 || index > length) {
+      throw new IndexOutOfBoundsException();
+    }
+    return chars[zero + index];
+  }
+
+  /**
+   * Appends the specified codePoint to the end of the string.
+   *
+   * @return The number of chars added: 1 if the code point is in the BMP, or 2 otherwise.
+   */
+  public int appendCodePoint(int codePoint, Field field) {
+    return insertCodePoint(length, codePoint, field);
+  }
+
+  /**
+   * Inserts the specified codePoint at the specified index in the string.
+   *
+   * @return The number of chars added: 1 if the code point is in the BMP, or 2 otherwise.
+   */
+  public int insertCodePoint(int index, int codePoint, Field field) {
+    int count = Character.charCount(codePoint);
+    int position = prepareForInsert(index, count);
+    Character.toChars(codePoint, chars, position);
+    fields[position] = field;
+    if (count == 2) fields[position + 1] = field;
+    return count;
+  }
+
+  /**
+   * Appends the specified CharSequence to the end of the string.
+   *
+   * @return The number of chars added, which is the length of CharSequence.
+   */
+  public int append(CharSequence sequence, Field field) {
+    return insert(length, sequence, field);
+  }
+
+  /**
+   * Inserts the specified CharSequence at the specified index in the string.
+   *
+   * @return The number of chars added, which is the length of CharSequence.
+   */
+  public int insert(int index, CharSequence sequence, Field field) {
+    if (sequence.length() == 0) {
+      // Nothing to insert.
+      return 0;
+    } else if (sequence.length() == 1) {
+      // Fast path: on a single-char string, using insertCodePoint below is 70% faster than the
+      // CharSequence method: 12.2 ns versus 41.9 ns for five operations on my Linux x86-64.
+      return insertCodePoint(index, sequence.charAt(0), field);
+    } else {
+      return insert(index, sequence, 0, sequence.length(), field);
+    }
+  }
+
+  /**
+   * Inserts the specified CharSequence at the specified index in the string, reading from the
+   * CharSequence from start (inclusive) to end (exclusive).
+   *
+   * @return The number of chars added, which is the length of CharSequence.
+   */
+  public int insert(int index, CharSequence sequence, int start, int end, Field field) {
+    int count = end - start;
+    int position = prepareForInsert(index, count);
+    for (int i = 0; i < count; i++) {
+      chars[position + i] = sequence.charAt(start + i);
+      fields[position + i] = field;
+    }
+    return count;
+  }
+
+  /**
+   * Appends the chars in the specified char array to the end of the string, and associates them
+   * with the fields in the specified field array, which must have the same length as chars.
+   *
+   * @return The number of chars added, which is the length of the char array.
+   */
+  public int append(char[] chars, Field[] fields) {
+    return insert(length, chars, fields);
+  }
+
+  /**
+   * Inserts the chars in the specified char array at the specified index in the string, and
+   * associates them with the fields in the specified field array, which must have the same length
+   * as chars.
+   *
+   * @return The number of chars added, which is the length of the char array.
+   */
+  public int insert(int index, char[] chars, Field[] fields) {
+    assert fields == null || chars.length == fields.length;
+    int count = chars.length;
+    if (count == 0) return 0; // nothing to insert
+    int position = prepareForInsert(index, count);
+    for (int i = 0; i < count; i++) {
+      this.chars[position + i] = chars[i];
+      this.fields[position + i] = fields == null ? null : fields[i];
+    }
+    return count;
+  }
+
+  /**
+   * Appends the contents of another {@link NumberStringBuilder} to the end of this instance.
+   *
+   * @return The number of chars added, which is the length of the other {@link
+   *     NumberStringBuilder}.
+   */
+  public int append(NumberStringBuilder other) {
+    return insert(length, other);
+  }
+
+  /**
+   * Inserts the contents of another {@link NumberStringBuilder} into this instance at the given
+   * index.
+   *
+   * @return The number of chars added, which is the length of the other {@link
+   *     NumberStringBuilder}.
+   */
+  public int insert(int index, NumberStringBuilder other) {
+    assert this != other;
+    int count = other.length;
+    if (count == 0) return 0; // nothing to insert
+    int position = prepareForInsert(index, count);
+    for (int i = 0; i < count; i++) {
+      this.chars[position + i] = other.chars[other.zero + i];
+      this.fields[position + i] = other.fields[other.zero + i];
+    }
+    return count;
+  }
+
+  /**
+   * Shifts around existing data if necessary to make room for new characters.
+   *
+   * @param index The location in the string where the operation is to take place.
+   * @param count The number of chars (UTF-16 code units) to be inserted at that location.
+   * @return The position in the char array to insert the chars.
+   */
+  private int prepareForInsert(int index, int count) {
+    if (index == 0 && zero - count >= 0) {
+      // Append to start
+      zero -= count;
+      length += count;
+      return zero;
+    } else if (index == length && zero + length + count < chars.length) {
+      // Append to end
+      length += count;
+      return zero + length - count;
+    } else {
+      // Move chars around and/or allocate more space
+      return prepareForInsertHelper(index, count);
+    }
+  }
+
+  private int prepareForInsertHelper(int index, int count) {
+    // Keeping this code out of prepareForInsert() increases the speed of append operations.
+    if (length + count > chars.length) {
+      char[] newChars = new char[(length + count) * 2];
+      Field[] newFields = new Field[(length + count) * 2];
+      int newZero = newChars.length / 2 - (length + count) / 2;
+      System.arraycopy(chars, zero, newChars, newZero, index);
+      System.arraycopy(chars, zero + index, newChars, newZero + index + count, length - index);
+      System.arraycopy(fields, zero, newFields, newZero, index);
+      System.arraycopy(fields, zero + index, newFields, newZero + index + count, length - index);
+      chars = newChars;
+      fields = newFields;
+      zero = newZero;
+      length += count;
+    } else {
+      int newZero = chars.length / 2 - (length + count) / 2;
+      System.arraycopy(chars, zero, chars, newZero, length);
+      System.arraycopy(chars, newZero + index, chars, newZero + index + count, length - index);
+      System.arraycopy(fields, zero, fields, newZero, length);
+      System.arraycopy(fields, newZero + index, fields, newZero + index + count, length - index);
+      zero = newZero;
+      length += count;
+    }
+    return zero + index;
+  }
+
+  @Override
+  public CharSequence subSequence(int start, int end) {
+    if (start < 0 || end > length || end < start) {
+      throw new IndexOutOfBoundsException();
+    }
+    NumberStringBuilder other = this.clone();
+    other.zero = zero + start;
+    other.length = end - start;
+    return other;
+  }
+
+  /**
+   * Returns the string represented by the characters in this string builder.
+   *
+   * <p>For a string intended be used for debugging, use {@link #toDebugString}.
+   */
+  @Override
+  public String toString() {
+    return new String(chars, zero, length);
+  }
+
+  private static final Map<Field, Character> fieldToDebugChar = new HashMap<Field, Character>();
+
+  static {
+    fieldToDebugChar.put(NumberFormat.Field.SIGN, '-');
+    fieldToDebugChar.put(NumberFormat.Field.INTEGER, 'i');
+    fieldToDebugChar.put(NumberFormat.Field.FRACTION, 'f');
+    fieldToDebugChar.put(NumberFormat.Field.EXPONENT, 'e');
+    fieldToDebugChar.put(NumberFormat.Field.EXPONENT_SIGN, '+');
+    fieldToDebugChar.put(NumberFormat.Field.EXPONENT_SYMBOL, 'E');
+    fieldToDebugChar.put(NumberFormat.Field.DECIMAL_SEPARATOR, '.');
+    fieldToDebugChar.put(NumberFormat.Field.GROUPING_SEPARATOR, ',');
+    fieldToDebugChar.put(NumberFormat.Field.PERCENT, '%');
+    fieldToDebugChar.put(NumberFormat.Field.PERMILLE, '‰');
+    fieldToDebugChar.put(NumberFormat.Field.CURRENCY, '$');
+  }
+
+  /**
+   * Returns a string that includes field information, for debugging purposes.
+   *
+   * <p>For example, if the string is "-12.345", the debug string will be something like
+   * "&lt;NumberStringBuilder [-123.45] [-iii.ff]&gt;"
+   *
+   * @return A string for debugging purposes.
+   */
+  public String toDebugString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append("<NumberStringBuilder [");
+    sb.append(this.toString());
+    sb.append("] [");
+    for (int i = zero; i < zero + length; i++) {
+      if (fields[i] == null) {
+        sb.append('n');
+      } else {
+        sb.append(fieldToDebugChar.get(fields[i]));
+      }
+    }
+    sb.append("]>");
+    return sb.toString();
+  }
+
+  /** @return A new array containing the contents of this string builder. */
+  public char[] toCharArray() {
+    return Arrays.copyOfRange(chars, zero, zero + length);
+  }
+
+  /** @return A new array containing the field values of this string builder. */
+  public Field[] toFieldArray() {
+    return Arrays.copyOfRange(fields, zero, zero + length);
+  }
+
+  /**
+   * @return Whether the contents and field values of this string builder are equal to the given
+   *     chars and fields.
+   * @see #toCharArray
+   * @see #toFieldArray
+   */
+  public boolean contentEquals(char[] chars, Field[] fields) {
+    if (chars.length != length) return false;
+    if (fields.length != length) return false;
+    for (int i = 0; i < length; i++) {
+      if (this.chars[zero + i] != chars[i]) return false;
+      if (this.fields[zero + i] != fields[i]) return false;
+    }
+    return true;
+  }
+
+  /**
+   * @param other The instance to compare.
+   * @return Whether the contents of this instance is currently equal to the given instance.
+   */
+  public boolean contentEquals(NumberStringBuilder other) {
+    if (length != other.length) return false;
+    for (int i = 0; i < length; i++) {
+      if (chars[zero + i] != other.chars[other.zero + i]) return false;
+      if (fields[zero + i] != other.fields[other.zero + i]) return false;
+    }
+    return true;
+  }
+
+  /**
+   * Populates the given {@link FieldPosition} based on this string builder.
+   *
+   * @param fp The FieldPosition to populate.
+   * @param offset An offset to add to the field position index; can be zero.
+   */
+  public void populateFieldPosition(FieldPosition fp, int offset) {
+    java.text.Format.Field rawField = fp.getFieldAttribute();
+
+    if (rawField == null) {
+      // Backwards compatibility: read from fp.getField()
+      if (fp.getField() == NumberFormat.INTEGER_FIELD) {
+        rawField = NumberFormat.Field.INTEGER;
+      } else if (fp.getField() == NumberFormat.FRACTION_FIELD) {
+        rawField = NumberFormat.Field.FRACTION;
+      } else {
+        // No field is set
+        return;
+      }
+    }
+
+    if (!(rawField instanceof com.ibm.icu.text.NumberFormat.Field)) {
+      throw new IllegalArgumentException(
+          "You must pass an instance of com.ibm.icu.text.NumberFormat.Field as your FieldPosition attribute.  You passed: "
+              + rawField.getClass().toString());
+    }
+    /* com.ibm.icu.text.NumberFormat. */ Field field = (Field) rawField;
+
+    boolean seenStart = false;
+    int fractionStart = -1;
+    for (int i = zero; i <= zero + length; i++) {
+      Field _field = (i < zero + length) ? fields[i] : null;
+      if (seenStart && field != _field) {
+        // Special case: GROUPING_SEPARATOR counts as an INTEGER.
+        if (field == NumberFormat.Field.INTEGER && _field == NumberFormat.Field.GROUPING_SEPARATOR)
+          continue;
+        fp.setEndIndex(i - zero + offset);
+        break;
+      } else if (!seenStart && field == _field) {
+        fp.setBeginIndex(i - zero + offset);
+        seenStart = true;
+      }
+      if (_field == NumberFormat.Field.INTEGER || _field == NumberFormat.Field.DECIMAL_SEPARATOR) {
+        fractionStart = i - zero + 1;
+      }
+    }
+
+    // Backwards compatibility: FRACTION needs to start after INTEGER if empty
+    if (field == NumberFormat.Field.FRACTION && !seenStart) {
+      fp.setBeginIndex(fractionStart);
+      fp.setEndIndex(fractionStart);
+    }
+  }
+
+  public AttributedCharacterIterator getIterator() {
+    AttributedString as = new AttributedString(toString());
+    Field current = null;
+    int currentStart = -1;
+    for (int i = 0; i < length; i++) {
+      Field field = fields[i + zero];
+      if (current == NumberFormat.Field.INTEGER && field == NumberFormat.Field.GROUPING_SEPARATOR) {
+        // Special case: GROUPING_SEPARATOR counts as an INTEGER.
+        as.addAttribute(
+            NumberFormat.Field.GROUPING_SEPARATOR, NumberFormat.Field.GROUPING_SEPARATOR, i, i + 1);
+      } else if (current != field) {
+        if (current != null) {
+          as.addAttribute(current, current, currentStart, i);
+        }
+        current = field;
+        currentStart = i;
+      }
+    }
+    if (current != null) {
+      as.addAttribute(current, current, currentStart, length);
+    }
+    return as.getIterator();
+  }
+
+  @Override
+  public NumberStringBuilder clone() {
+    NumberStringBuilder other = new NumberStringBuilder(chars.length);
+    other.zero = zero;
+    other.length = length;
+    System.arraycopy(chars, zero, other.chars, zero, length);
+    System.arraycopy(fields, zero, other.fields, zero, length);
+    return other;
+  }
+
+  public NumberStringBuilder clear() {
+    zero = chars.length / 2;
+    length = 0;
+    return this;
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PNAffixGenerator.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PNAffixGenerator.java
new file mode 100644 (file)
index 0000000..bb9af1c
--- /dev/null
@@ -0,0 +1,296 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import com.ibm.icu.impl.number.Modifier.AffixModifier;
+import com.ibm.icu.impl.number.formatters.CompactDecimalFormat;
+import com.ibm.icu.impl.number.formatters.PositiveNegativeAffixFormat;
+import com.ibm.icu.impl.number.formatters.PositiveNegativeAffixFormat.IProperties;
+import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier;
+import com.ibm.icu.impl.number.modifiers.ConstantMultiFieldModifier;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.NumberFormat.Field;
+
+/**
+ * A class to convert from a bag of prefix/suffix properties into a positive and negative {@link
+ * Modifier}. This is a standard implementation used by {@link PositiveNegativeAffixFormat}, {@link
+ * CompactDecimalFormat}, {@link Parse}, and others.
+ *
+ * <p>This class is is intended to be an efficient generator for instances of Modifier by a single
+ * thread during construction of a formatter or during static formatting. It uses internal caching
+ * to avoid creating new Modifier objects when possible. It is NOT THREAD SAFE and NOT IMMUTABLE.
+ *
+ * <p>The thread-local instance of this class provided by {@link #getThreadLocalInstance} should be
+ * used in most cases instead of constructing a new instance of the object.
+ *
+ * <p>This class also handles the logic of assigning positive signs, negative signs, and currency
+ * signs according to the LDML specification.
+ */
+public class PNAffixGenerator {
+  public static class Result {
+    public AffixModifier positive = null;
+    public AffixModifier negative = null;
+  }
+
+  protected static final ThreadLocal<PNAffixGenerator> threadLocalInstance =
+      new ThreadLocal<PNAffixGenerator>() {
+        @Override
+        protected PNAffixGenerator initialValue() {
+          return new PNAffixGenerator();
+        }
+      };
+
+  public static PNAffixGenerator getThreadLocalInstance() {
+    return threadLocalInstance.get();
+  }
+
+  // These instances are used internally and cached to avoid object creation.  The resultInstance
+  // also serves as a 1-element cache to avoid creating objects when subsequent calls have
+  // identical prefixes and suffixes.  This happens, for example, when consuming CDF data.
+  private Result resultInstance = new Result();
+  private NumberStringBuilder sb1 = new NumberStringBuilder();
+  private NumberStringBuilder sb2 = new NumberStringBuilder();
+  private NumberStringBuilder sb3 = new NumberStringBuilder();
+  private NumberStringBuilder sb4 = new NumberStringBuilder();
+
+  /**
+   * Generates modifiers using default currency symbols.
+   *
+   * @param symbols The symbols to interpolate for minus, plus, percent, permille, and currency.
+   * @param properties The bag of properties to convert.
+   * @return The positive and negative {@link Modifier}.
+   */
+  public Result getModifiers(
+      DecimalFormatSymbols symbols, PositiveNegativeAffixFormat.IProperties properties) {
+    // If this method is used, the user doesn't care about currencies. Default the currency symbols
+    // to the information we can get from the DecimalFormatSymbols instance.
+    return getModifiers(
+        symbols,
+        symbols.getCurrencySymbol(),
+        symbols.getInternationalCurrencySymbol(),
+        symbols.getCurrencySymbol(),
+        properties);
+  }
+
+  /**
+   * Generates modifiers using the specified currency symbol for all three lengths of currency
+   * placeholders in the pattern string.
+   *
+   * @param symbols The symbols to interpolate for minus, plus, percent, and permille.
+   * @param currencySymbol The currency symbol.
+   * @param properties The bag of properties to convert.
+   * @return The positive and negative {@link Modifier}.
+   */
+  public Result getModifiers(
+      DecimalFormatSymbols symbols,
+      String currencySymbol,
+      PositiveNegativeAffixFormat.IProperties properties) {
+    // If this method is used, the user doesn't cares about currencies but doesn't care about
+    // supporting all three sizes of currency placeholders.  Use the one provided string for all
+    // three sizes of placeholders.
+    return getModifiers(symbols, currencySymbol, currencySymbol, currencySymbol, properties);
+  }
+
+  /**
+   * Generates modifiers using the three specified strings to replace the three lengths of currency
+   * placeholders: "¤", "¤¤", and "¤¤¤".
+   *
+   * @param symbols The symbols to interpolate for minus, plus, percent, and permille.
+   * @param curr1 The string to replace "¤".
+   * @param curr2 The string to replace "¤¤".
+   * @param curr3 The string to replace "¤¤¤".
+   * @param properties The bag of properties to convert.
+   * @return The positive and negative {@link Modifier}.
+   */
+  public Result getModifiers(
+      DecimalFormatSymbols symbols,
+      String curr1,
+      String curr2,
+      String curr3,
+      PositiveNegativeAffixFormat.IProperties properties) {
+
+    // Use a different code path for handling affixes with "always show plus sign"
+    if (properties.getPlusSignAlwaysShown()) {
+      return getModifiersWithPlusSign(symbols, curr1, curr2, curr3, properties);
+    }
+
+    CharSequence ppp = properties.getPositivePrefixPattern();
+    CharSequence psp = properties.getPositiveSuffixPattern();
+    CharSequence npp = properties.getNegativePrefixPattern();
+    CharSequence nsp = properties.getNegativeSuffixPattern();
+
+    // Set sb1/sb2 to the positive prefix/suffix.
+    sb1.clear();
+    sb2.clear();
+    AffixPatternUtils.unescape(ppp, symbols, curr1, curr2, curr3, null, sb1);
+    AffixPatternUtils.unescape(psp, symbols, curr1, curr2, curr3, null, sb2);
+    setPositiveResult(sb1, sb2, properties);
+
+    // Set sb1/sb2 to the negative prefix/suffix.
+    if (npp == null && nsp == null) {
+      // Negative prefix defaults to positive prefix prepended with the minus sign.
+      // Negative suffix defaults to positive suffix.
+      sb1.insert(0, symbols.getMinusSignString(), Field.SIGN);
+    } else {
+      sb1.clear();
+      sb2.clear();
+      AffixPatternUtils.unescape(npp, symbols, curr1, curr2, curr3, null, sb1);
+      AffixPatternUtils.unescape(nsp, symbols, curr1, curr2, curr3, null, sb2);
+    }
+    setNegativeResult(sb1, sb2, properties);
+
+    return resultInstance;
+  }
+
+  private Result getModifiersWithPlusSign(
+      DecimalFormatSymbols symbols,
+      String curr1,
+      String curr2,
+      String curr3,
+      IProperties properties) {
+
+    CharSequence ppp = properties.getPositivePrefixPattern();
+    CharSequence psp = properties.getPositiveSuffixPattern();
+    CharSequence npp = properties.getNegativePrefixPattern();
+    CharSequence nsp = properties.getNegativeSuffixPattern();
+
+    // There are three cases, listed below with their expected outcomes.
+    // TODO: Should we handle the cases when the positive subpattern has a '+' already?
+    //
+    //   1) No negative subpattern.
+    //        Positive => Positive subpattern prepended with '+'
+    //        Negative => Positive subpattern prepended with '-'
+    //   2) Negative subpattern does not have '-'.
+    //        Positive => Positive subpattern prepended with '+'
+    //        Negative => Negative subpattern
+    //   3) Negative subpattern has '-'.
+    //        Positive => Negative subpattern with '+' substituted for '-'
+    //        Negative => Negative subpattern
+
+    if (npp != null || nsp != null) {
+      // Case 2 or Case 3
+      sb1.clear();
+      sb2.clear();
+      sb3.clear();
+      sb4.clear();
+      AffixPatternUtils.unescape(npp, symbols, curr1, curr2, curr3, null, sb1);
+      AffixPatternUtils.unescape(nsp, symbols, curr1, curr2, curr3, null, sb2);
+      AffixPatternUtils.unescape(
+          npp, symbols, curr1, curr2, curr3, symbols.getPlusSignString(), sb3);
+      AffixPatternUtils.unescape(
+          nsp, symbols, curr1, curr2, curr3, symbols.getPlusSignString(), sb4);
+      if (!charSequenceEquals(sb1, sb3) || !charSequenceEquals(sb2, sb4)) {
+        // Case 3. The plus sign substitution was successful.
+        setPositiveResult(sb3, sb4, properties);
+        setNegativeResult(sb1, sb2, properties);
+        return resultInstance;
+      } else {
+        // Case 2. There was no minus sign. Set the negative result and fall through.
+        setNegativeResult(sb1, sb2, properties);
+      }
+    }
+
+    // Case 1 or 2. Set sb1/sb2 to the positive prefix/suffix.
+    sb1.clear();
+    sb2.clear();
+    AffixPatternUtils.unescape(ppp, symbols, curr1, curr2, curr3, null, sb1);
+    AffixPatternUtils.unescape(psp, symbols, curr1, curr2, curr3, null, sb2);
+
+    if (npp == null && nsp == null) {
+      // Case 1. Compute the negative result from the positive subpattern.
+      sb3.clear();
+      sb3.append(symbols.getMinusSignString(), Field.SIGN);
+      sb3.append(sb1);
+      setNegativeResult(sb3, sb2, properties);
+    }
+
+    // Case 1 or 2. Prepend a '+' sign to the positive prefix.
+    sb1.insert(0, symbols.getPlusSignString(), Field.SIGN);
+    setPositiveResult(sb1, sb2, properties);
+
+    return resultInstance;
+  }
+
+  private void setPositiveResult(
+      NumberStringBuilder prefix, NumberStringBuilder suffix, IProperties properties) {
+    if (properties.getPositivePrefix() != null || properties.getPositiveSuffix() != null) {
+      // Override with custom affixes
+      String _prefix = properties.getPositivePrefix();
+      String _suffix = properties.getPositiveSuffix();
+      if (_prefix == null) _prefix = "";
+      if (_suffix == null) _suffix = "";
+      if (_prefix.length() == 0 && _suffix.length() == 0) {
+        resultInstance.positive = ConstantAffixModifier.EMPTY;
+        return;
+      }
+      if (resultInstance.positive != null
+          && (resultInstance.positive instanceof ConstantAffixModifier)
+          && ((ConstantAffixModifier) resultInstance.positive).contentEquals(_prefix, _suffix)) {
+        // Use the cached modifier
+        return;
+      }
+      resultInstance.positive =
+          new ConstantAffixModifier(_prefix, _suffix, null, false);
+    } else {
+      // Use pattern affixes
+      if (prefix.length() == 0 && suffix.length() == 0) {
+        resultInstance.positive = ConstantAffixModifier.EMPTY;
+        return;
+      }
+      if (resultInstance.positive != null
+          && (resultInstance.positive instanceof ConstantMultiFieldModifier)
+          && ((ConstantMultiFieldModifier) resultInstance.positive).contentEquals(prefix, suffix)) {
+        // Use the cached modifier
+        return;
+      }
+      resultInstance.positive = new ConstantMultiFieldModifier(prefix, suffix, false);
+    }
+  }
+
+  private void setNegativeResult(
+      NumberStringBuilder prefix, NumberStringBuilder suffix, IProperties properties) {
+    if (properties.getNegativePrefix() != null || properties.getNegativeSuffix() != null) {
+      // Override with custom affixes
+      String _prefix = properties.getNegativePrefix();
+      String _suffix = properties.getNegativeSuffix();
+      if (_prefix == null) _prefix = "";
+      if (_suffix == null) _suffix = "";
+      if (_prefix.length() == 0 && _suffix.length() == 0) {
+        resultInstance.negative = ConstantAffixModifier.EMPTY;
+        return;
+      }
+      if (resultInstance.negative != null
+          && (resultInstance.negative instanceof ConstantAffixModifier)
+          && ((ConstantAffixModifier) resultInstance.negative).contentEquals(_prefix, _suffix)) {
+        // Use the cached modifier
+        return;
+      }
+      resultInstance.negative =
+          new ConstantAffixModifier(_prefix, _suffix, null, false);
+    } else {
+      // Use pattern affixes
+      if (prefix.length() == 0 && suffix.length() == 0) {
+        resultInstance.negative = ConstantAffixModifier.EMPTY;
+        return;
+      }
+      if (resultInstance.negative != null
+          && (resultInstance.negative instanceof ConstantMultiFieldModifier)
+          && ((ConstantMultiFieldModifier) resultInstance.negative).contentEquals(prefix, suffix)) {
+        // Use the cached modifier
+        return;
+      }
+      resultInstance.negative = new ConstantMultiFieldModifier(prefix, suffix, false);
+    }
+  }
+
+  /** A null-safe equals method for CharSequences. */
+  private static boolean charSequenceEquals(CharSequence a, CharSequence b) {
+    if (a == b) return true;
+    if (a == null || b == null) return false;
+    if (a.length() != b.length()) return false;
+    for (int i = 0; i < a.length(); i++) {
+      if (a.charAt(i) != b.charAt(i)) return false;
+    }
+    return true;
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Parse.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Parse.java
new file mode 100644 (file)
index 0000000..110171f
--- /dev/null
@@ -0,0 +1,2110 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.TextTrieMap;
+import com.ibm.icu.impl.number.Parse.ParseMode;
+import com.ibm.icu.impl.number.formatters.BigDecimalMultiplier;
+import com.ibm.icu.impl.number.formatters.CurrencyFormat;
+import com.ibm.icu.impl.number.formatters.MagnitudeMultiplier;
+import com.ibm.icu.impl.number.formatters.PaddingFormat;
+import com.ibm.icu.impl.number.formatters.PositiveDecimalFormat;
+import com.ibm.icu.impl.number.formatters.PositiveNegativeAffixFormat;
+import com.ibm.icu.lang.UCharacter;
+import com.ibm.icu.text.CurrencyPluralInfo;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.NumberFormat;
+import com.ibm.icu.text.UnicodeSet;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyStringInfo;
+import com.ibm.icu.util.CurrencyAmount;
+import com.ibm.icu.util.ULocale;
+
+/**
+ * A parser designed to convert an arbitrary human-generated string to its best representation as a
+ * number: a long, a BigInteger, or a BigDecimal.
+ *
+ * <p>The parser may traverse multiple parse paths in the same strings if there is ambiguity. For
+ * example, the string "12,345.67" has two main interpretations: it could be "12.345" in a locale
+ * that uses '.' as the grouping separator, or it could be "12345.67" in a locale that uses ',' as
+ * the grouping separator. Since the second option has a longer parse path (consumes more of the
+ * input string), the parser will accept the second option.
+ */
+public class Parse {
+
+  /** Controls the set of rules for parsing a string. */
+  public static enum ParseMode {
+    /**
+     * Lenient mode should be used if you want to accept malformed user input. It will use
+     * heuristics to attempt to parse through typographical errors in the string.
+     */
+    LENIENT,
+
+    /**
+     * Strict mode should be used if you want to require that the input is well-formed. More
+     * specifically, it differs from lenient mode in the following ways:
+     *
+     * <ul>
+     *   <li>Grouping widths must match the grouping settings. For example, "12,3,45" will fail if
+     *       the grouping width is 3, as in the pattern "#,##0".
+     *   <li>The string must contain a complete prefix and suffix. For example, if the pattern is
+     *       "{#};(#)", then "{123}" or "(123)" would match, but "{123", "123}", and "123" would all
+     *       fail. (The latter strings would be accepted in lenient mode.)
+     *   <li>Whitespace may not appear at arbitrary places in the string. In lenient mode,
+     *       whitespace is allowed to occur arbitrarily before and after prefixes and exponent
+     *       separators.
+     *   <li>Leading grouping separators are not allowed, as in ",123".
+     *   <li>Minus and plus signs can only appear if specified in the pattern. In lenient mode, a
+     *       plus or minus sign can always precede a number.
+     *   <li>The set of characters that can be interpreted as a decimal or grouping separator is
+     *       smaller.
+     *   <li><strong>If currency parsing is enabled,</strong> currencies must only appear where
+     *       specified in either the current pattern string or in a valid pattern string for the
+     *       current locale. For example, if the pattern is "¤0.00", then "$1.23" would match, but
+     *       "1.23$" would fail to match.
+     * </ul>
+     */
+    STRICT,
+
+    /**
+     * Fast mode should be used in applications that don't require prefixes and suffixes to match.
+     *
+     * <p>In addition to ignoring prefixes and suffixes, fast mode performs the following
+     * optimizations:
+     *
+     * <ul>
+     *   <li>Ignores digit strings from {@link DecimalFormatSymbols} and only uses the code point's
+     *       Unicode digit property. If you are not using custom digit strings, this should not
+     *       cause a change in behavior.
+     *   <li>Instead of traversing multiple possible parse paths, a "greedy" parsing strategy is
+     *       used, which might mean that fast mode won't accept strings that lenient or strict mode
+     *       would accept. Since prefix and suffix strings are ignored, this is not an issue unless
+     *       you are using custom symbols.
+     * </ul>
+     */
+    FAST,
+  }
+
+  /** The set of properties required for {@link Parse}. Accepts a {@link Properties} object. */
+  public static interface IProperties
+      extends PositiveNegativeAffixFormat.IProperties,
+          PaddingFormat.IProperties,
+          CurrencyFormat.ICurrencyProperties,
+          BigDecimalMultiplier.IProperties,
+          MagnitudeMultiplier.IProperties,
+          PositiveDecimalFormat.IProperties {
+
+    boolean DEFAULT_PARSE_INTEGER_ONLY = false;
+
+    /** @see #setParseIntegerOnly */
+    public boolean getParseIntegerOnly();
+
+    /**
+     * Whether to ignore the fractional part of numbers. For example, parses "123.4" to "123"
+     * instead of "123.4".
+     *
+     * @param parseIntegerOnly true to parse integers only; false to parse integers with their
+     *     fraction parts
+     * @return The property bag, for chaining.
+     */
+    public IProperties setParseIntegerOnly(boolean parseIntegerOnly);
+
+    boolean DEFAULT_PARSE_NO_EXPONENT = false;
+
+    /** @see #setParseNoExponent */
+    public boolean getParseNoExponent();
+
+    /**
+     * Whether to ignore the exponential part of numbers. For example, parses "123E4" to "123"
+     * instead of "1230000".
+     *
+     * @param parseIgnoreExponent true to ignore exponents; false to parse them.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setParseNoExponent(boolean parseIgnoreExponent);
+
+    boolean DEFAULT_DECIMAL_PATTERN_MATCH_REQUIRED = false;
+
+    /** @see #setDecimalPatternMatchRequired */
+    public boolean getDecimalPatternMatchRequired();
+
+    /**
+     * Whether to require that the presence of decimal point matches the pattern. If a decimal point
+     * is not present, but the pattern contained a decimal point, parse will not succeed: null will
+     * be returned from <code>parse()</code>, and an error index will be set in the {@link
+     * ParsePosition}.
+     *
+     * @param decimalPatternMatchRequired true to set an error if decimal is not present
+     * @return The property bag, for chaining.
+     */
+    public IProperties setDecimalPatternMatchRequired(boolean decimalPatternMatchRequired);
+
+    ParseMode DEFAULT_PARSE_MODE = null;
+
+    /** @see #setParseMode */
+    public ParseMode getParseMode();
+
+    /**
+     * Controls certain rules for how strict this parser is when reading strings. See {@link
+     * ParseMode#LENIENT} and {@link ParseMode#STRICT}.
+     *
+     * @param parseMode Either {@link ParseMode#LENIENT} or {@link ParseMode#STRICT}.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setParseMode(ParseMode parseMode);
+
+    boolean DEFAULT_PARSE_TO_BIG_DECIMAL = false;
+
+    /** @see #setParseToBigDecimal */
+    public boolean getParseToBigDecimal();
+
+    /**
+     * Whether to always return a BigDecimal from {@link Parse#parse} and all other parse methods.
+     * By default, a Long or a BigInteger are returned when possible.
+     *
+     * @param parseToBigDecimal true to always return a BigDecimal; false to return a Long or a
+     *     BigInteger when possible.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setParseToBigDecimal(boolean parseToBigDecimal);
+
+    boolean DEFAULT_PARSE_CASE_SENSITIVE = false;
+
+    /** @see #setParseCaseSensitive */
+    public boolean getParseCaseSensitive();
+
+    /**
+     * Whether to require cases to match when parsing strings; default is true. Case sensitivity
+     * applies to prefixes, suffixes, the exponent separator, the symbol "NaN", and the infinity
+     * symbol. Grouping separators, decimal separators, and padding are always case-sensitive.
+     * Currencies are always case-insensitive.
+     *
+     * <p>This setting is ignored in fast mode. In fast mode, strings are always compared in a
+     * case-sensitive way.
+     *
+     * @param parseCaseSensitive true to be case-sensitive when parsing; false to allow any case.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setParseCaseSensitive(boolean parseCaseSensitive);
+  }
+
+  /**
+   * @see #parse(String, ParsePosition, ParseMode, boolean, boolean, IProperties,
+   *     DecimalFormatSymbols)
+   */
+  private static enum StateName {
+    BEFORE_PREFIX,
+    AFTER_PREFIX,
+    AFTER_INTEGER_DIGIT,
+    AFTER_FRACTION_DIGIT,
+    AFTER_EXPONENT_SEPARATOR,
+    AFTER_EXPONENT_DIGIT,
+    BEFORE_SUFFIX,
+    BEFORE_SUFFIX_SEEN_EXPONENT,
+    AFTER_SUFFIX,
+    INSIDE_CURRENCY,
+    INSIDE_DIGIT,
+    INSIDE_STRING,
+    INSIDE_AFFIX_PATTERN;
+  }
+
+  // TODO: Does this set make sense for the whitespace characters?
+  private static final UnicodeSet UNISET_WHITESPACE =
+      new UnicodeSet("[[:whitespace:][\\u2000-\\u200D]]").freeze();
+
+  // BiDi characters are skipped over and ignored at any point in the string, even in strict mode.
+  private static final UnicodeSet UNISET_BIDI =
+      new UnicodeSet("[[\\u200E\\u200F\\u061C]]").freeze();
+
+  // TODO: Re-generate these sets from the database. They probably haven't been updated in a while.
+  private static final UnicodeSet UNISET_PERIOD_LIKE =
+      new UnicodeSet("[.\\u2024\\u3002\\uFE12\\uFE52\\uFF0E\\uFF61]").freeze();
+  private static final UnicodeSet UNISET_STRICT_PERIOD_LIKE =
+      new UnicodeSet("[.\\u2024\\uFE52\\uFF0E\\uFF61]").freeze();
+  private static final UnicodeSet UNISET_COMMA_LIKE =
+      new UnicodeSet("[,\\u060C\\u066B\\u3001\\uFE10\\uFE11\\uFE50\\uFE51\\uFF0C\\uFF64]").freeze();
+  private static final UnicodeSet UNISET_STRICT_COMMA_LIKE =
+      new UnicodeSet("[,\\u066B\\uFE10\\uFE50\\uFF0C]").freeze();
+  private static final UnicodeSet UNISET_OTHER_GROUPING_SEPARATORS =
+      new UnicodeSet(
+              "[\\ '\\u00A0\\u066C\\u2000-\\u200A\\u2018\\u2019\\u202F\\u205F\\u3000\\uFF07]")
+          .freeze();
+
+  private enum SeparatorType {
+    COMMA_LIKE,
+    PERIOD_LIKE,
+    OTHER_GROUPING,
+    UNKNOWN;
+
+    static SeparatorType fromCp(int cp, ParseMode mode) {
+      if (mode == ParseMode.FAST) {
+        return SeparatorType.UNKNOWN;
+      } else if (mode == ParseMode.STRICT) {
+        if (UNISET_STRICT_COMMA_LIKE.contains(cp)) return COMMA_LIKE;
+        if (UNISET_STRICT_PERIOD_LIKE.contains(cp)) return PERIOD_LIKE;
+        if (UNISET_OTHER_GROUPING_SEPARATORS.contains(cp)) return OTHER_GROUPING;
+        return UNKNOWN;
+      } else {
+        if (UNISET_COMMA_LIKE.contains(cp)) return COMMA_LIKE;
+        if (UNISET_PERIOD_LIKE.contains(cp)) return PERIOD_LIKE;
+        if (UNISET_OTHER_GROUPING_SEPARATORS.contains(cp)) return OTHER_GROUPING;
+        return UNKNOWN;
+      }
+    }
+  }
+
+  private static enum DigitType {
+    INTEGER,
+    FRACTION,
+    EXPONENT
+  }
+
+  /**
+   * Holds a snapshot in time of a single parse path. This includes the digits seen so far, the
+   * current state name, and other properties like the grouping separator used on this parse path,
+   * details about the exponent and negative signs, etc.
+   */
+  private static class StateItem {
+    // Parser state:
+    // The "trailingChars" is used to keep track of how many characters from the end of the string
+    // are ignorable and should be removed from the parse position should this item be accepted.
+    // The "score" is used to help rank two otherwise equivalent parse paths. Currently, the only
+    // function giving points to the score is prefix/suffix.
+    StateName name;
+    int trailingCount;
+    int score;
+
+    // Numerical value:
+    FormatQuantity4 fq = new FormatQuantity4();
+    int numDigits;
+    int trailingZeros;
+    int exponent;
+
+    // Other items that we've seen:
+    int groupingCp;
+    long groupingWidths;
+    String isoCode;
+    boolean sawNegative;
+    boolean sawNegativeExponent;
+    boolean sawCurrency;
+    boolean sawNaN;
+    boolean sawInfinity;
+    AffixHolder affix;
+    boolean sawPrefix;
+    boolean sawSuffix;
+    boolean sawDecimalPoint;
+
+    // Data for intermediate parsing steps:
+    StateName returnTo1;
+    StateName returnTo2;
+    // For string literals:
+    CharSequence currentString;
+    int currentOffset;
+    // For affix patterns:
+    CharSequence currentAffixPattern;
+    long currentStepwiseParserTag;
+    // For currency:
+    TextTrieMap<CurrencyStringInfo>.ParseState currentCurrencyTrieState;
+    // For multi-code-point digits:
+    TextTrieMap<Byte>.ParseState currentDigitTrieState;
+    DigitType currentDigitType;
+
+    /**
+     * Clears the instance so that it can be re-used.
+     *
+     * @return Myself, for chaining.
+     */
+    StateItem clear() {
+      // Parser state:
+      name = StateName.BEFORE_PREFIX;
+      trailingCount = 0;
+      score = 0;
+
+      // Numerical value:
+      fq.clear();
+      numDigits = 0;
+      trailingZeros = 0;
+      exponent = 0;
+
+      // Other items we've seen:
+      groupingCp = -1;
+      groupingWidths = 0L;
+      isoCode = null;
+      sawNegative = false;
+      sawNegativeExponent = false;
+      sawCurrency = false;
+      sawNaN = false;
+      sawInfinity = false;
+      affix = null;
+      sawPrefix = false;
+      sawSuffix = false;
+      sawDecimalPoint = false;
+
+      // Data for intermediate parsing steps:
+      returnTo1 = null;
+      returnTo2 = null;
+      currentString = null;
+      currentOffset = 0;
+      currentAffixPattern = null;
+      currentStepwiseParserTag = 0L;
+      currentCurrencyTrieState = null;
+      currentDigitTrieState = null;
+      currentDigitType = null;
+
+      return this;
+    }
+
+    /**
+     * Sets the internal value of this instance equal to another instance.
+     *
+     * <p>newName and cpOrN1 are required as parameters to this function because every time a code
+     * point is consumed and a state item is copied, both of the corresponding fields should be
+     * updated; it would be an error if they weren't updated.
+     *
+     * @param other The instance to copy from.
+     * @param newName The state name that the new copy should take on.
+     * @param trailing If positive, record this code point as trailing; if negative, reset the
+     *     trailing count to zero.
+     * @return Myself, for chaining.
+     */
+    StateItem copyFrom(StateItem other, StateName newName, int trailing) {
+      // Parser state:
+      name = newName;
+      score = other.score;
+
+      // Either reset trailingCount or add the width of the current code point.
+      trailingCount = (trailing < 0) ? 0 : other.trailingCount + Character.charCount(trailing);
+
+      // Numerical value:
+      fq.copyFrom(other.fq);
+      numDigits = other.numDigits;
+      trailingZeros = other.trailingZeros;
+      exponent = other.exponent;
+
+      // Other items we've seen:
+      groupingCp = other.groupingCp;
+      groupingWidths = other.groupingWidths;
+      isoCode = other.isoCode;
+      sawNegative = other.sawNegative;
+      sawNegativeExponent = other.sawNegativeExponent;
+      sawCurrency = other.sawCurrency;
+      sawNaN = other.sawNaN;
+      sawInfinity = other.sawInfinity;
+      affix = other.affix;
+      sawPrefix = other.sawPrefix;
+      sawSuffix = other.sawSuffix;
+      sawDecimalPoint = other.sawDecimalPoint;
+
+      // Data for intermediate parsing steps:
+      returnTo1 = other.returnTo1;
+      returnTo2 = other.returnTo2;
+      currentString = other.currentString;
+      currentOffset = other.currentOffset;
+      currentAffixPattern = other.currentAffixPattern;
+      currentStepwiseParserTag = other.currentStepwiseParserTag;
+      currentCurrencyTrieState = other.currentCurrencyTrieState;
+      currentDigitTrieState = other.currentDigitTrieState;
+      currentDigitType = other.currentDigitType;
+
+      return this;
+    }
+
+    /**
+     * Adds a digit to the internal representation of this instance.
+     *
+     * @param digit The digit that was read from the string.
+     * @param type Whether the digit occured after the decimal point.
+     */
+    void appendDigit(byte digit, DigitType type) {
+      if (type == DigitType.EXPONENT) {
+        int newExponent = exponent * 10 + digit;
+        if (newExponent < exponent) {
+          // overflow
+          exponent = Integer.MAX_VALUE;
+        } else {
+          exponent = newExponent;
+        }
+      } else {
+        numDigits++;
+        if (type == DigitType.FRACTION && digit == 0) {
+          trailingZeros++;
+        } else if (type == DigitType.FRACTION) {
+          fq.appendDigit(digit, trailingZeros, false);
+          trailingZeros = 0;
+        } else {
+          fq.appendDigit(digit, 0, true);
+        }
+      }
+    }
+
+    /** @return Whether or not this item contains a valid number. */
+    public boolean hasNumber() {
+      return numDigits > 0 || sawNaN || sawInfinity;
+    }
+
+    /**
+     * Converts the internal digits from this instance into a Number, preferring a Long, then a
+     * BigInteger, then a BigDecimal. A Double is used for NaN, infinity, and -0.0.
+     *
+     * @return The Number. Never null.
+     */
+    Number toNumber(IProperties properties) {
+      // Check for NaN, infinity, and -0.0
+      if (sawNaN) {
+        return Double.NaN;
+      }
+      if (sawInfinity) {
+        if (sawNegative) {
+          return Double.NEGATIVE_INFINITY;
+        } else {
+          return Double.POSITIVE_INFINITY;
+        }
+      }
+      if (fq.isZero() && sawNegative) {
+        return -0.0;
+      }
+
+      // Check for exponent overflow
+      boolean forceBigDecimal = properties.getParseToBigDecimal();
+      if (exponent == Integer.MAX_VALUE) {
+        if (sawNegativeExponent && sawNegative) {
+          return -0.0;
+        } else if (sawNegativeExponent) {
+          return 0.0;
+        } else if (sawNegative) {
+          return Double.NEGATIVE_INFINITY;
+        } else {
+          return Double.POSITIVE_INFINITY;
+        }
+      } else if (exponent > 1000) {
+        // BigDecimals can handle huge values better than BigIntegers.
+        forceBigDecimal = true;
+      }
+
+      // Multipliers must be applied in reverse.
+      BigDecimal multiplier = properties.getMultiplier();
+      if (properties.getMagnitudeMultiplier() != 0) {
+        if (multiplier == null) multiplier = BigDecimal.ONE;
+        multiplier = multiplier.scaleByPowerOfTen(properties.getMagnitudeMultiplier());
+      }
+      int delta = (sawNegativeExponent ? -1 : 1) * exponent;
+
+      // We need to use a math context in order to prevent non-terminating decimal expansions.
+      // This is only used when dividing by the multiplier.
+      MathContext mc = RoundingUtils.getMathContextOr16Digits(properties);
+
+      // Construct the output number.
+      // This is the only step during fast-mode parsing that incurs object creations.
+      BigDecimal result = fq.toBigDecimal();
+      if (sawNegative) result = result.negate();
+      result = result.scaleByPowerOfTen(delta);
+      if (multiplier != null) {
+        result = result.divide(multiplier, mc);
+      }
+      result = result.stripTrailingZeros();
+      if (forceBigDecimal || result.scale() > 0) {
+        return result;
+      } else if (-result.scale() + result.precision() <= 18) {
+        return result.longValueExact();
+      } else {
+        return result.toBigIntegerExact();
+      }
+    }
+
+    /**
+     * Converts the internal digits to a number, and also associates the number with the parsed
+     * currency.
+     *
+     * @return The CurrencyAmount. Never null.
+     */
+    public CurrencyAmount toCurrencyAmount(IProperties properties) {
+      assert isoCode != null;
+      Number number = toNumber(properties);
+      Currency currency = Currency.getInstance(isoCode);
+      return new CurrencyAmount(number, currency);
+    }
+
+    @Override
+    public String toString() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("<ParserStateItem ");
+      sb.append(name.name());
+      if (name == StateName.INSIDE_STRING) {
+        sb.append("{");
+        sb.append(currentString);
+        sb.append(":");
+        sb.append(currentOffset);
+        sb.append("}");
+      }
+      if (name == StateName.INSIDE_AFFIX_PATTERN) {
+        sb.append("{");
+        sb.append(currentAffixPattern);
+        sb.append(":");
+        sb.append(AffixPatternUtils.getOffset(currentStepwiseParserTag));
+        sb.append("}");
+      }
+      sb.append(" ");
+      sb.append(fq.toBigDecimal());
+      sb.append(" grouping:");
+      sb.append(groupingCp == -1 ? new char[] {'?'} : Character.toChars(groupingCp));
+      sb.append(" widths:");
+      sb.append(Long.toHexString(groupingWidths));
+      sb.append(" seen:");
+      sb.append(sawNegative ? 1 : 0);
+      sb.append(sawNegativeExponent ? 1 : 0);
+      sb.append(sawNaN ? 1 : 0);
+      sb.append(sawInfinity ? 1 : 0);
+      sb.append(sawPrefix ? 1 : 0);
+      sb.append(sawSuffix ? 1 : 0);
+      sb.append(sawDecimalPoint ? 1 : 0);
+      sb.append(" score:");
+      sb.append(score);
+      sb.append(" affix:");
+      sb.append(affix);
+      sb.append(" currency:");
+      sb.append(isoCode);
+      sb.append(">");
+      return sb.toString();
+    }
+  }
+
+  /**
+   * Holds an ordered list of {@link StateItem} and other metadata about the string to be parsed.
+   * There are two internal arrays of {@link StateItem}, which are swapped back and forth in order
+   * to avoid object creations. The items in one array can be populated at the same time that items
+   * in the other array are being read from.
+   */
+  private static class ParserState {
+
+    // Basic ParserStateItem lists:
+    StateItem[] items = new StateItem[16];
+    StateItem[] prevItems = new StateItem[16];
+    int length;
+    int prevLength;
+
+    // Properties and Symbols memory:
+    IProperties properties;
+    DecimalFormatSymbols symbols;
+    ParseMode mode;
+    boolean caseSensitive;
+    boolean parseCurrency;
+
+    // Other pre-computed fields:
+    int decimalCp1;
+    int decimalCp2;
+    int groupingCp1;
+    int groupingCp2;
+    SeparatorType decimalType1;
+    SeparatorType decimalType2;
+    SeparatorType groupingType1;
+    SeparatorType groupingType2;
+    TextTrieMap<Byte> digitTrie;
+    Set<AffixHolder> affixHolders = new HashSet<AffixHolder>();
+
+    ParserState() {
+      for (int i = 0; i < items.length; i++) {
+        items[i] = new StateItem();
+        prevItems[i] = new StateItem();
+      }
+    }
+
+    /**
+     * Clears the internal state in order to prepare for parsing a new string.
+     *
+     * @return Myself, for chaining.
+     */
+    ParserState clear() {
+      length = 0;
+      prevLength = 0;
+      digitTrie = null;
+      affixHolders.clear();
+      return this;
+    }
+
+    /**
+     * Swaps the internal arrays of {@link StateItem}. Sets the length of the primary list to zero,
+     * so that it can be appended to.
+     */
+    void swap() {
+      StateItem[] temp = prevItems;
+      prevItems = items;
+      items = temp;
+      prevLength = length;
+      length = 0;
+    }
+
+    /**
+     * Swaps the internal arrays of {@link StateItem}. Sets the length of the primary list to the
+     * length of the previous list, so that it can be read from.
+     */
+    void swapBack() {
+      StateItem[] temp = prevItems;
+      prevItems = items;
+      items = temp;
+      length = prevLength;
+      prevLength = 0;
+    }
+
+    /**
+     * Gets the next available {@link StateItem} from the primary list for writing. This method
+     * should be thought of like a list append method, except that there are no object creations
+     * taking place.
+     *
+     * <p>It is the caller's responsibility to call either {@link StateItem#clear} or {@link
+     * StateItem#copyFrom} on the returned object.
+     *
+     * @return A dirty {@link StateItem}.
+     */
+    StateItem getNext() {
+      if (length >= items.length) {
+        // TODO: What to do here? Expand the array?
+        // This case is rare and would happen only with specially designed input.
+        // For now, just overwrite the last entry.
+        length = items.length - 1;
+      }
+      StateItem item = items[length];
+      length++;
+      return item;
+    }
+
+    /** @return The index of the last inserted StateItem via a call to {@link #getNext}. */
+    public int lastInsertedIndex() {
+      assert length > 0;
+      return length - 1;
+    }
+
+    /**
+     * Gets a {@link StateItem} from the primary list. Assumes that the item has already been added
+     * via a call to {@link #getNext}.
+     *
+     * @param i The index of the item to get.
+     * @return The item.
+     */
+    public StateItem getItem(int i) {
+      assert i >= 0 && i < length;
+      return items[i];
+    }
+  }
+
+  private static class AffixHolder {
+    final String p; // prefix
+    final String s; // suffix
+    final boolean strings;
+    final boolean negative;
+
+    static final AffixHolder EMPTY_POSITIVE = new AffixHolder("", "", true, false);
+    static final AffixHolder EMPTY_NEGATIVE = new AffixHolder("", "", true, true);
+    static final AffixHolder DEFAULT_POSITIVE = new AffixHolder("+", "", false, false);
+    static final AffixHolder DEFAULT_NEGATIVE = new AffixHolder("-", "", false, true);
+
+    static void addToState(ParserState state, IProperties properties) {
+      AffixHolder pp = fromPropertiesPositivePattern(properties);
+      AffixHolder np = fromPropertiesNegativePattern(properties);
+      AffixHolder ps = fromPropertiesPositiveString(properties);
+      AffixHolder ns = fromPropertiesNegativeString(properties);
+      if (pp == null && ps == null) {
+        if (properties.getPlusSignAlwaysShown()) {
+          state.affixHolders.add(DEFAULT_POSITIVE);
+        } else {
+          state.affixHolders.add(EMPTY_POSITIVE);
+        }
+      } else {
+        if (pp != null) state.affixHolders.add(pp);
+        if (ps != null) state.affixHolders.add(ps);
+      }
+      if (np == null && ns == null) {
+        state.affixHolders.add(DEFAULT_NEGATIVE);
+      } else {
+        if (np != null) state.affixHolders.add(np);
+        if (ns != null) state.affixHolders.add(ns);
+      }
+    }
+
+    static AffixHolder fromPropertiesPositivePattern(IProperties properties) {
+      String ppp = properties.getPositivePrefixPattern();
+      String psp = properties.getPositiveSuffixPattern();
+      return getInstance(ppp, psp, false, false);
+    }
+
+    static AffixHolder fromPropertiesNegativePattern(IProperties properties) {
+      String npp = properties.getNegativePrefixPattern();
+      String nsp = properties.getNegativeSuffixPattern();
+      return getInstance(npp, nsp, false, true);
+    }
+
+    static AffixHolder fromPropertiesPositiveString(IProperties properties) {
+      String pp = properties.getPositivePrefix();
+      String ps = properties.getPositiveSuffix();
+      return getInstance(pp, ps, true, false);
+    }
+
+    static AffixHolder fromPropertiesNegativeString(IProperties properties) {
+      String np = properties.getNegativePrefix();
+      String ns = properties.getNegativeSuffix();
+      return getInstance(np, ns, true, true);
+    }
+
+    static AffixHolder getInstance(String p, String s, boolean strings, boolean negative) {
+      if (p == null && s == null) return null;
+      if (p == null) p = "";
+      if (s == null) s = "";
+      if (p.length() == 0 && s.length() == 0) return negative ? EMPTY_NEGATIVE : EMPTY_POSITIVE;
+      return new AffixHolder(p, s, strings, negative);
+    }
+
+    AffixHolder(String pp, String sp, boolean strings, boolean negative) {
+      this.p = pp;
+      this.s = sp;
+      this.strings = strings;
+      this.negative = negative;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (other == null) return false;
+      if (this == other) return true;
+      if (!(other instanceof AffixHolder)) return false;
+      AffixHolder _other = (AffixHolder) other;
+      if (!p.equals(_other.p)) return false;
+      if (!s.equals(_other.s)) return false;
+      if (strings != _other.strings) return false;
+      if (negative != _other.negative) return false;
+      return true;
+    }
+
+    @Override
+    public int hashCode() {
+      return p.hashCode() ^ s.hashCode();
+    }
+
+    @Override
+    public String toString() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("{");
+      sb.append(p);
+      sb.append("|");
+      sb.append(s);
+      sb.append("|");
+      sb.append(strings ? 'S' : 'P');
+      sb.append("}");
+      return sb.toString();
+    }
+  }
+
+  /**
+   * A class that holds information about all currency affix patterns for the locale. This allows
+   * the parser to accept currencies in any format that are valid for the locale.
+   */
+  private static class CurrencyAffixPatterns {
+    private final Set<AffixHolder> set = new HashSet<AffixHolder>();
+
+    private static final ConcurrentHashMap<ULocale, CurrencyAffixPatterns> currencyAffixPatterns =
+        new ConcurrentHashMap<ULocale, CurrencyAffixPatterns>();
+
+    static void addToState(ULocale uloc, ParserState state) {
+      if (!currencyAffixPatterns.containsKey(uloc)) {
+        // There can be multiple threads computing the same CurrencyAffixPatterns simultaneously,
+        // but that scenario is harmless.
+        CurrencyAffixPatterns value = new CurrencyAffixPatterns(uloc);
+        currencyAffixPatterns.put(uloc, value);
+      }
+      CurrencyAffixPatterns instance = currencyAffixPatterns.get(uloc);
+      state.affixHolders.addAll(instance.set);
+    }
+
+    private CurrencyAffixPatterns(ULocale uloc) {
+      // Get the basic currency pattern.
+      String pattern = NumberFormat.getPattern(uloc, NumberFormat.CURRENCYSTYLE);
+      addPattern(pattern);
+
+      // Get the currency plural patterns.
+      // TODO: Update this after CurrencyPluralInfo is replaced.
+      CurrencyPluralInfo pluralInfo = CurrencyPluralInfo.getInstance(uloc);
+      for (StandardPlural plural : StandardPlural.VALUES) {
+        pattern = pluralInfo.getCurrencyPluralPattern(plural.getKeyword());
+        addPattern(pattern);
+      }
+    }
+
+    private static final ThreadLocal<Properties> threadLocalProperties =
+        new ThreadLocal<Properties>() {
+          @Override
+          protected Properties initialValue() {
+            return new Properties();
+          }
+        };
+
+    private void addPattern(String pattern) {
+      Properties properties = threadLocalProperties.get();
+      try {
+        PatternString.parseToExistingProperties(pattern, properties);
+      } catch (IllegalArgumentException e) {
+        // This should only happen if there is a bug in CLDR data. Fail silently.
+      }
+      set.add(AffixHolder.fromPropertiesPositivePattern(properties));
+      set.add(AffixHolder.fromPropertiesNegativePattern(properties));
+    }
+  }
+
+  /**
+   * Makes a {@link TextTrieMap} for parsing digit strings. A trie is required only if the digit
+   * strings are longer than one code point. In order for this to be the case, the user would have
+   * needed to specify custom multi-character digits, like "(0)".
+   *
+   * @param digitStrings The list of digit strings from DecimalFormatSymbols.
+   * @return A trie, or null if a trie is not required.
+   */
+  static TextTrieMap<Byte> makeDigitTrie(String[] digitStrings) {
+    boolean requiresTrie = false;
+    for (int i = 0; i < 10; i++) {
+      String str = digitStrings[i];
+      if (Character.charCount(Character.codePointAt(str, 0)) != str.length()) {
+        requiresTrie = true;
+        break;
+      }
+    }
+    if (!requiresTrie) return null;
+
+    TextTrieMap<Byte> trieMap = new TextTrieMap<Byte>(false);
+    for (int i = 0; i < 10; i++) {
+      trieMap.put(digitStrings[i], (byte) i);
+    }
+    return trieMap;
+  }
+
+  protected static final ThreadLocal<ParserState> threadLocalParseState =
+      new ThreadLocal<ParserState>() {
+        @Override
+        protected ParserState initialValue() {
+          return new ParserState();
+        }
+      };
+
+  protected static final ThreadLocal<ParsePosition> threadLocalParsePosition =
+      new ThreadLocal<ParsePosition>() {
+        @Override
+        protected ParsePosition initialValue() {
+          return new ParsePosition(0);
+        }
+      };
+
+  /**
+   * @internal
+   * @deprecated This API is ICU internal only. TODO: Remove this set from ScientificNumberFormat.
+   */
+  @Deprecated
+  public static final UnicodeSet UNISET_PLUS =
+      new UnicodeSet(
+              0x002B, 0x002B, 0x207A, 0x207A, 0x208A, 0x208A, 0x2795, 0x2795, 0xFB29, 0xFB29,
+              0xFE62, 0xFE62, 0xFF0B, 0xFF0B)
+          .freeze();
+
+  /**
+   * @internal
+   * @deprecated This API is ICU internal only. TODO: Remove this set from ScientificNumberFormat.
+   */
+  @Deprecated
+  public static final UnicodeSet UNISET_MINUS =
+      new UnicodeSet(
+              0x002D, 0x002D, 0x207B, 0x207B, 0x208B, 0x208B, 0x2212, 0x2212, 0x2796, 0x2796,
+              0xFE63, 0xFE63, 0xFF0D, 0xFF0D)
+          .freeze();
+
+  public static Number parse(String input, IProperties properties, DecimalFormatSymbols symbols) {
+    ParsePosition ppos = threadLocalParsePosition.get();
+    ppos.setIndex(0);
+    return parse(input, ppos, properties, symbols);
+  }
+
+  // TODO: DELETE ME once debugging is finished
+  public static volatile boolean DEBUGGING = false;
+
+  /**
+   * Implements an iterative parser that maintains a lists of possible states at each code point in
+   * the string. At each code point in the string, the list of possible states is updated based on
+   * the states coming from the previous code point. The parser stops when it reaches the end of the
+   * string or when there are no possible parse paths remaining in the string.
+   *
+   * <p>TODO: This API is not fully flushed out. Right now this is internal-only.
+   *
+   * @param input The string to parse.
+   * @param ppos A {@link ParsePosition} to hold the index at which parsing stopped.
+   * @param properties A property bag, used only for determining the prefix/suffix strings and the
+   *     padding character.
+   * @param symbols A {@link DecimalFormatSymbols} object, used for determining locale-specific
+   *     symbols for grouping/decimal separators, digit strings, and prefix/suffix substitutions.
+   * @return A Number matching the parser's best interpretation of the string.
+   */
+  public static Number parse(
+      CharSequence input,
+      ParsePosition ppos,
+      IProperties properties,
+      DecimalFormatSymbols symbols) {
+    StateItem best = _parse(input, ppos, false, properties, symbols);
+    return (best == null) ? null : best.toNumber(properties);
+  }
+
+  public static CurrencyAmount parseCurrency(
+      String input, IProperties properties, DecimalFormatSymbols symbols) throws ParseException {
+    return parseCurrency(input, null, properties, symbols);
+  }
+
+  public static CurrencyAmount parseCurrency(
+      CharSequence input, ParsePosition ppos, IProperties properties, DecimalFormatSymbols symbols)
+      throws ParseException {
+    if (ppos == null) {
+      ppos = threadLocalParsePosition.get();
+      ppos.setIndex(0);
+      ppos.setErrorIndex(-1);
+    }
+    StateItem best = _parse(input, ppos, true, properties, symbols);
+    return (best == null) ? null : best.toCurrencyAmount(properties);
+  }
+
+  private static StateItem _parse(
+      CharSequence input,
+      ParsePosition ppos,
+      boolean parseCurrency,
+      IProperties properties,
+      DecimalFormatSymbols symbols) {
+
+    if (input == null || ppos == null || properties == null || symbols == null) {
+      throw new IllegalArgumentException("All arguments are required for parse.");
+    }
+
+    ParseMode mode = properties.getParseMode();
+    if (mode == null) mode = ParseMode.LENIENT;
+    boolean integerOnly = properties.getParseIntegerOnly();
+    boolean ignoreExponent = properties.getParseNoExponent();
+
+    // Set up the initial state
+    ParserState state = threadLocalParseState.get().clear();
+    state.properties = properties;
+    state.symbols = symbols;
+    state.mode = mode;
+    state.parseCurrency = parseCurrency;
+    state.caseSensitive = properties.getParseCaseSensitive();
+    state.decimalCp1 = Character.codePointAt(symbols.getDecimalSeparatorString(), 0);
+    state.decimalCp2 = Character.codePointAt(symbols.getMonetaryDecimalSeparatorString(), 0);
+    state.groupingCp1 = Character.codePointAt(symbols.getGroupingSeparatorString(), 0);
+    state.groupingCp2 = Character.codePointAt(symbols.getMonetaryGroupingSeparatorString(), 0);
+    state.decimalType1 = SeparatorType.fromCp(state.decimalCp1, mode);
+    state.decimalType2 = SeparatorType.fromCp(state.decimalCp2, mode);
+    state.groupingType1 = SeparatorType.fromCp(state.groupingCp1, mode);
+    state.groupingType2 = SeparatorType.fromCp(state.groupingCp2, mode);
+    StateItem initialStateItem = state.getNext().clear();
+    initialStateItem.name = StateName.BEFORE_PREFIX;
+
+    if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+      state.digitTrie = makeDigitTrie(symbols.getDigitStringsLocal());
+      AffixHolder.addToState(state, properties);
+      if (parseCurrency) {
+        CurrencyAffixPatterns.addToState(symbols.getULocale(), state);
+      }
+    }
+
+    if (DEBUGGING) {
+      System.out.println("Parsing: " + input);
+      System.out.println(properties);
+      System.out.println(state.affixHolders);
+    }
+
+    // Start walking through the string, one codepoint at a time. Backtracking is not allowed. This
+    // is to enforce linear runtime and prevent cases that could result in an infinite loop.
+    int offset = ppos.getIndex();
+    for (; offset < input.length(); ) {
+      int cp = Character.codePointAt(input, offset);
+      state.swap();
+      for (int i = 0; i < state.prevLength; i++) {
+        StateItem item = state.prevItems[i];
+        if (DEBUGGING) {
+          System.out.println(":" + offset + " " + item);
+        }
+
+        // In the switch statement below, if you see a line like:
+        //    if (state.length > 0 && mode == ParseMode.FAST) break;
+        // it is used for accelerating the fast parse mode. The check is performed only in the
+        // states BEFORE_PREFIX, AFTER_INTEGER_DIGIT, and AFTER_FRACTION_DIGIT, which are the
+        // most common states.
+
+        switch (item.name) {
+          case BEFORE_PREFIX:
+            // Beginning of string
+            if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+              acceptMinusOrPlusSign(cp, StateName.BEFORE_PREFIX, state, item, false);
+              if (state.length > 0 && mode == ParseMode.FAST) break;
+            }
+            acceptIntegerDigit(cp, StateName.AFTER_INTEGER_DIGIT, state, item);
+            if (state.length > 0 && mode == ParseMode.FAST) break;
+            acceptBidi(cp, StateName.BEFORE_PREFIX, state, item);
+            if (state.length > 0 && mode == ParseMode.FAST) break;
+            acceptWhitespace(cp, StateName.BEFORE_PREFIX, state, item);
+            if (state.length > 0 && mode == ParseMode.FAST) break;
+            acceptPadding(cp, StateName.BEFORE_PREFIX, state, item);
+            if (state.length > 0 && mode == ParseMode.FAST) break;
+            acceptNan(cp, StateName.BEFORE_SUFFIX, state, item);
+            if (state.length > 0 && mode == ParseMode.FAST) break;
+            acceptInfinity(cp, StateName.BEFORE_SUFFIX, state, item);
+            if (state.length > 0 && mode == ParseMode.FAST) break;
+            if (!integerOnly) {
+              acceptDecimalPoint(cp, StateName.AFTER_FRACTION_DIGIT, state, item);
+              if (state.length > 0 && mode == ParseMode.FAST) break;
+            }
+            if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+              acceptPrefix(cp, StateName.AFTER_PREFIX, state, item);
+            }
+            if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+              acceptGrouping(cp, StateName.AFTER_INTEGER_DIGIT, state, item);
+              if (state.length > 0 && mode == ParseMode.FAST) break;
+              if (parseCurrency) {
+                acceptCurrency(cp, StateName.BEFORE_PREFIX, state, item);
+              }
+            }
+            break;
+
+          case AFTER_PREFIX:
+            // Prefix is consumed
+            acceptBidi(cp, StateName.AFTER_PREFIX, state, item);
+            acceptPadding(cp, StateName.AFTER_PREFIX, state, item);
+            acceptNan(cp, StateName.BEFORE_SUFFIX, state, item);
+            acceptInfinity(cp, StateName.BEFORE_SUFFIX, state, item);
+            acceptIntegerDigit(cp, StateName.AFTER_INTEGER_DIGIT, state, item);
+            if (!integerOnly) {
+              acceptDecimalPoint(cp, StateName.AFTER_FRACTION_DIGIT, state, item);
+            }
+            if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+              acceptWhitespace(cp, StateName.AFTER_PREFIX, state, item);
+              acceptGrouping(cp, StateName.AFTER_INTEGER_DIGIT, state, item);
+              if (parseCurrency) {
+                acceptCurrency(cp, StateName.AFTER_PREFIX, state, item);
+              }
+            }
+            break;
+
+          case AFTER_INTEGER_DIGIT:
+            // Previous character was an integer digit (or grouping/whitespace)
+            acceptIntegerDigit(cp, StateName.AFTER_INTEGER_DIGIT, state, item);
+            if (state.length > 0 && mode == ParseMode.FAST) break;
+            if (!integerOnly) {
+              acceptDecimalPoint(cp, StateName.AFTER_FRACTION_DIGIT, state, item);
+              if (state.length > 0 && mode == ParseMode.FAST) break;
+            }
+            acceptGrouping(cp, StateName.AFTER_INTEGER_DIGIT, state, item);
+            if (state.length > 0 && mode == ParseMode.FAST) break;
+            acceptBidi(cp, StateName.AFTER_INTEGER_DIGIT, state, item);
+            if (state.length > 0 && mode == ParseMode.FAST) break;
+            acceptPadding(cp, StateName.BEFORE_SUFFIX, state, item);
+            if (state.length > 0 && mode == ParseMode.FAST) break;
+            if (!ignoreExponent) {
+              acceptExponentSeparator(cp, StateName.AFTER_EXPONENT_SEPARATOR, state, item);
+              if (state.length > 0 && mode == ParseMode.FAST) break;
+            }
+            if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+              acceptSuffix(cp, StateName.AFTER_SUFFIX, state, item);
+            }
+            if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+              acceptWhitespace(cp, StateName.BEFORE_SUFFIX, state, item);
+              if (state.length > 0 && mode == ParseMode.FAST) break;
+              acceptMinusOrPlusSign(cp, StateName.BEFORE_SUFFIX, state, item, false);
+              if (state.length > 0 && mode == ParseMode.FAST) break;
+              if (parseCurrency) {
+                acceptCurrency(cp, StateName.BEFORE_SUFFIX, state, item);
+              }
+            }
+            break;
+
+          case AFTER_FRACTION_DIGIT:
+            // We encountered a decimal point
+            acceptFractionDigit(cp, StateName.AFTER_FRACTION_DIGIT, state, item);
+            if (state.length > 0 && mode == ParseMode.FAST) break;
+            acceptBidi(cp, StateName.AFTER_FRACTION_DIGIT, state, item);
+            if (state.length > 0 && mode == ParseMode.FAST) break;
+            acceptPadding(cp, StateName.BEFORE_SUFFIX, state, item);
+            if (state.length > 0 && mode == ParseMode.FAST) break;
+            if (!ignoreExponent) {
+              acceptExponentSeparator(cp, StateName.AFTER_EXPONENT_SEPARATOR, state, item);
+              if (state.length > 0 && mode == ParseMode.FAST) break;
+            }
+            if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+              acceptSuffix(cp, StateName.AFTER_SUFFIX, state, item);
+            }
+            if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+              acceptWhitespace(cp, StateName.BEFORE_SUFFIX, state, item);
+              if (state.length > 0 && mode == ParseMode.FAST) break;
+              acceptMinusOrPlusSign(cp, StateName.BEFORE_SUFFIX, state, item, false);
+              if (state.length > 0 && mode == ParseMode.FAST) break;
+              if (parseCurrency) {
+                acceptCurrency(cp, StateName.BEFORE_SUFFIX, state, item);
+              }
+            }
+            break;
+
+          case AFTER_EXPONENT_SEPARATOR:
+            acceptBidi(cp, StateName.AFTER_EXPONENT_SEPARATOR, state, item);
+            acceptMinusOrPlusSign(cp, StateName.AFTER_EXPONENT_SEPARATOR, state, item, true);
+            acceptExponentDigit(cp, StateName.AFTER_EXPONENT_DIGIT, state, item);
+            break;
+
+          case AFTER_EXPONENT_DIGIT:
+            acceptBidi(cp, StateName.AFTER_EXPONENT_DIGIT, state, item);
+            acceptPadding(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+            acceptExponentDigit(cp, StateName.AFTER_EXPONENT_DIGIT, state, item);
+            if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+              acceptSuffix(cp, StateName.AFTER_SUFFIX, state, item);
+            }
+            if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+              acceptWhitespace(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+              acceptMinusOrPlusSign(cp, StateName.BEFORE_SUFFIX, state, item, false);
+              if (parseCurrency) {
+                acceptCurrency(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+              }
+            }
+            break;
+
+          case BEFORE_SUFFIX:
+            // Accept whitespace, suffixes, and exponent separators
+            acceptBidi(cp, StateName.BEFORE_SUFFIX, state, item);
+            acceptPadding(cp, StateName.BEFORE_SUFFIX, state, item);
+            if (!ignoreExponent) {
+              acceptExponentSeparator(cp, StateName.AFTER_EXPONENT_SEPARATOR, state, item);
+            }
+            if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+              acceptSuffix(cp, StateName.AFTER_SUFFIX, state, item);
+            }
+            if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+              acceptWhitespace(cp, StateName.BEFORE_SUFFIX, state, item);
+              acceptMinusOrPlusSign(cp, StateName.BEFORE_SUFFIX, state, item, false);
+              if (parseCurrency) {
+                acceptCurrency(cp, StateName.BEFORE_SUFFIX, state, item);
+              }
+            }
+            break;
+
+          case BEFORE_SUFFIX_SEEN_EXPONENT:
+            // Accept whitespace and suffixes but not exponent separators
+            acceptBidi(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+            acceptPadding(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+            if (mode == ParseMode.LENIENT || mode == ParseMode.STRICT) {
+              acceptSuffix(cp, StateName.AFTER_SUFFIX, state, item);
+            }
+            if (mode == ParseMode.LENIENT || mode == ParseMode.FAST) {
+              acceptWhitespace(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+              acceptMinusOrPlusSign(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item, false);
+              if (parseCurrency) {
+                acceptCurrency(cp, StateName.BEFORE_SUFFIX_SEEN_EXPONENT, state, item);
+              }
+            }
+            break;
+
+          case AFTER_SUFFIX:
+            if ((mode == ParseMode.LENIENT || mode == ParseMode.FAST) && parseCurrency) {
+              // Continue traversing in case there is a currency symbol to consume
+              acceptBidi(cp, StateName.AFTER_SUFFIX, state, item);
+              acceptPadding(cp, StateName.AFTER_SUFFIX, state, item);
+              acceptWhitespace(cp, StateName.AFTER_SUFFIX, state, item);
+              acceptMinusOrPlusSign(cp, StateName.AFTER_SUFFIX, state, item, false);
+              if (parseCurrency) {
+                acceptCurrency(cp, StateName.AFTER_SUFFIX, state, item);
+              }
+            }
+            // Otherwise, do not accept any more characters.
+            break;
+
+          case INSIDE_CURRENCY:
+            acceptCurrencyOffset(cp, state, item);
+            break;
+
+          case INSIDE_DIGIT:
+            acceptDigitTrieOffset(cp, state, item);
+            break;
+
+          case INSIDE_STRING:
+            acceptStringOffset(cp, state, item);
+            // Accept arbitrary bidi in the middle of strings.
+            if (state.length == 0 && UNISET_BIDI.contains(cp)) {
+              state.getNext().copyFrom(item, item.name, cp);
+            }
+            break;
+
+          case INSIDE_AFFIX_PATTERN:
+            acceptAffixPatternOffset(cp, state, item);
+            // Accept arbitrary bidi and whitespace (if lenient) in the middle of affixes.
+            if (state.length == 0 && isIgnorable(cp, state)) {
+              state.getNext().copyFrom(item, item.name, cp);
+            }
+            break;
+        }
+      }
+
+      if (state.length == 0) {
+        // No parse paths continue past this point. We have found the longest parsable string
+        // from the input. Restore previous state without the offset and break.
+        state.swapBack();
+        break;
+      }
+
+      offset += Character.charCount(cp);
+    }
+
+    // Post-processing
+    if (state.length == 0) {
+      if (DEBUGGING) {
+        System.out.println("No matches found");
+        System.out.println("- - - - - - - - - -");
+      }
+      return null;
+    } else {
+
+      // Loop through the candidates.  "continue" skips a candidate as invalid.
+      StateItem best = null;
+      outer:
+      for (int i = 0; i < state.length; i++) {
+        StateItem item = state.items[i];
+
+        if (DEBUGGING) {
+          System.out.println(":end " + item);
+        }
+
+        // Check that at least one digit was read.
+        if (!item.hasNumber()) {
+          if (DEBUGGING) System.out.println("-> rejected due to no number value");
+          continue;
+        }
+
+        if (mode == ParseMode.STRICT) {
+          // Perform extra checks for strict mode.
+          // We require that the affixes match.
+          boolean sawPrefix = item.sawPrefix || (item.affix != null && item.affix.p.isEmpty());
+          boolean sawSuffix = item.sawSuffix || (item.affix != null && item.affix.s.isEmpty());
+          boolean hasEmptyAffix =
+              state.affixHolders.contains(AffixHolder.EMPTY_POSITIVE)
+                  || state.affixHolders.contains(AffixHolder.EMPTY_NEGATIVE);
+          if (sawPrefix && sawSuffix) {
+            // OK
+          } else if (!sawPrefix && !sawSuffix && hasEmptyAffix) {
+            // OK
+          } else {
+            // Has a prefix or suffix that doesn't match
+            if (DEBUGGING) System.out.println("-> rejected due to mismatched prefix/suffix");
+            continue;
+          }
+
+          // Check that grouping sizes are valid.
+          int grouping1 = properties.getGroupingSize();
+          int grouping2 = properties.getSecondaryGroupingSize();
+          grouping1 = grouping1 > 0 ? grouping1 : grouping2;
+          grouping2 = grouping2 > 0 ? grouping2 : grouping1;
+          long groupingWidths = item.groupingWidths;
+          int numGroupingRegions = 16 - Long.numberOfLeadingZeros(groupingWidths) / 4;
+          // If the last grouping is zero, accept strings like "1," but reject string like "1,.23"
+          // Strip off multiple last-groupings to handle cases like "123,," or "123  "
+          while (numGroupingRegions > 1 && (groupingWidths & 0xf) == 0) {
+            if (item.sawDecimalPoint) {
+              if (DEBUGGING) System.out.println("-> rejected due to decimal point after grouping");
+              continue outer;
+            } else {
+              groupingWidths >>>= 4;
+              numGroupingRegions--;
+            }
+          }
+          if (grouping1 < 0) {
+            // OK (no grouping data available)
+          } else if (numGroupingRegions <= 1) {
+            // OK (no grouping digits)
+          } else if ((groupingWidths & 0xf) != grouping1) {
+            // First grouping size is invalid
+            if (DEBUGGING) System.out.println("-> rejected due to first grouping violation");
+            continue;
+          } else if (((groupingWidths >>> ((numGroupingRegions - 1) * 4)) & 0xf) > grouping2) {
+            // String like "1234,567" where the highest grouping is too large
+            if (DEBUGGING) System.out.println("-> rejected due to final grouping violation");
+            continue;
+          } else {
+            for (int j = 1; j < numGroupingRegions - 1; j++) {
+              if (((groupingWidths >>> (j * 4)) & 0xf) != grouping2) {
+                // A grouping size somewhere in the middle is invalid
+                if (DEBUGGING) System.out.println("-> rejected due to inner grouping violation");
+                continue outer;
+              }
+            }
+          }
+        }
+
+        // Optionally require that the presence of a decimal point matches the pattern.
+        if (properties.getDecimalPatternMatchRequired()
+            && item.sawDecimalPoint != PositiveDecimalFormat.allowsDecimalPoint(properties)) {
+          if (DEBUGGING) System.out.println("-> rejected due to decimal point violation");
+          continue;
+        }
+
+        // When parsing currencies, require that a currency symbol was found.
+        if (parseCurrency && !item.sawCurrency) {
+          if (DEBUGGING) System.out.println("-> rejected due to lack of currency");
+          continue;
+        }
+
+        // If we get here, then this candidate is acceptable.
+        // Use the earliest candidate in the list, or the one with the highest score.
+        if (best == null) {
+          best = item;
+        } else if (item.score > best.score) {
+          best = item;
+        }
+      }
+
+      if (DEBUGGING) {
+        System.out.println("- - - - - - - - - -");
+      }
+
+      if (best != null) {
+        ppos.setIndex(offset - best.trailingCount);
+        return best;
+      } else {
+        ppos.setErrorIndex(offset);
+        return null;
+      }
+    }
+  }
+
+  /**
+   * If <code>cp</code> is whitespace (as determined by the unicode set {@link #UNISET_WHITESPACE}),
+   * copies <code>item</code> to the new list in <code>state</code> and sets its state name to
+   * <code>nextName</code>.
+   *
+   * @param cp The code point to check.
+   * @param nextName The new state name if the check passes.
+   * @param state The state object to update.
+   * @param item The old state leading into the code point.
+   */
+  private static void acceptWhitespace(
+      int cp, StateName nextName, ParserState state, StateItem item) {
+    if (UNISET_WHITESPACE.contains(cp)) {
+      state.getNext().copyFrom(item, nextName, cp);
+    }
+  }
+
+  /**
+   * If <code>cp</code> is a bidi control character (as determined by the unicode set {@link
+   * #UNISET_BIDI}), copies <code>item</code> to the new list in <code>state</code> and sets its
+   * state name to <code>nextName</code>.
+   *
+   * @param cp The code point to check.
+   * @param nextName The new state name if the check passes.
+   * @param state The state object to update.
+   * @param item The old state leading into the code point.
+   */
+  private static void acceptBidi(int cp, StateName nextName, ParserState state, StateItem item) {
+    if (UNISET_BIDI.contains(cp)) {
+      state.getNext().copyFrom(item, nextName, cp);
+    }
+  }
+
+  /**
+   * If <code>cp</code> is a padding character (as determined by {@link ParserState#paddingCp}),
+   * copies <code>item</code> to the new list in <code>state</code> and sets its state name to
+   * <code>nextName</code>.
+   *
+   * @param cp The code point to check.
+   * @param nextName The new state name if the check passes.
+   * @param state The state object to update.
+   * @param item The old state leading into the code point.
+   */
+  private static void acceptPadding(int cp, StateName nextName, ParserState state, StateItem item) {
+    CharSequence padding = state.properties.getPadString();
+    if (padding == null || padding.length() == 0) return;
+    int referenceCp = Character.codePointAt(padding, 0);
+    if (cp == referenceCp) {
+      state.getNext().copyFrom(item, nextName, cp);
+    }
+  }
+
+  private static void acceptIntegerDigit(
+      int cp, StateName nextName, ParserState state, StateItem item) {
+    acceptDigitHelper(cp, nextName, state, item, DigitType.INTEGER);
+  }
+
+  private static void acceptFractionDigit(
+      int cp, StateName nextName, ParserState state, StateItem item) {
+    acceptDigitHelper(cp, nextName, state, item, DigitType.FRACTION);
+  }
+
+  private static void acceptExponentDigit(
+      int cp, StateName nextName, ParserState state, StateItem item) {
+    acceptDigitHelper(cp, nextName, state, item, DigitType.EXPONENT);
+  }
+
+  /**
+   * If <code>cp</code> is a digit character (as determined by either {@link UCharacter#digit} or
+   * {@link ParserState#digitCps}), copies <code>item</code> to the new list in <code>state</code>
+   * and sets its state name to one determined by <code>type</code>. Also copies the digit into a
+   * field in the new item determined by <code>type</code>.
+   *
+   * <p>This function guarantees that it will add no more than one {@link StateItem} to the {@link
+   * ParserState}. This means that {@link ParserState#lastInsertedIndex()} can be called to access
+   * the {@link StateItem} that was inserted.
+   *
+   * @param cp The code point to check.
+   * @param nextName The state to set if a digit is accepted.
+   * @param state The state object to update.
+   * @param item The old state leading into the code point.
+   * @param type The digit type, which determines the next state and the field into which to insert
+   *     the digit.
+   */
+  private static void acceptDigitHelper(
+      int cp, StateName nextName, ParserState state, StateItem item, DigitType type) {
+    // Check the Unicode digit character property
+    byte digit = (byte) UCharacter.digit(cp, 10);
+    StateItem next = null;
+
+    // Look for the digit:
+    if (digit >= 0) {
+      // Code point is a number
+      next = state.getNext().copyFrom(item, nextName, -1);
+    }
+
+    // Do not perform the expensive string manipulations in fast mode.
+    if (digit < 0 && (state.mode == ParseMode.LENIENT || state.mode == ParseMode.STRICT)) {
+      if (state.digitTrie == null) {
+        // Check custom digits, all of which are at most one code point
+        for (byte d = 0; d < 10; d++) {
+          int referenceCp = Character.codePointAt(state.symbols.getDigitStringsLocal()[d], 0);
+          if (cp == referenceCp) {
+            digit = d;
+            next = state.getNext().copyFrom(item, nextName, -1);
+          }
+        }
+      } else {
+        // Custom digits have more than one code point
+        acceptDigitTrie(cp, nextName, state, item, type);
+      }
+    }
+
+    // Save state:
+    if (next != null) {
+      next.appendDigit(digit, type);
+      if (type == DigitType.INTEGER && (next.groupingWidths & 0xf) < 15) {
+        next.groupingWidths++;
+      }
+    }
+  }
+
+  /**
+   * If <code>cp</code> is a sign (as determined by the unicode sets {@link #UNISET_PLUS} and {@link
+   * #UNISET_MINUS}), copies <code>item</code> to the new list in <code>state</code>. Loops back to
+   * the same state name.
+   *
+   * @param cp The code point to check.
+   * @param state The state object to update.
+   * @param item The old state leading into the code point.
+   */
+  private static void acceptMinusOrPlusSign(
+      int cp, StateName nextName, ParserState state, StateItem item, boolean exponent) {
+    acceptMinusOrPlusSign(cp, nextName, null, state, item, exponent);
+  }
+
+  private static void acceptMinusOrPlusSign(
+      int cp,
+      StateName returnTo1,
+      StateName returnTo2,
+      ParserState state,
+      StateItem item,
+      boolean exponent) {
+    if (UNISET_PLUS.contains(cp)) {
+      StateItem next = state.getNext().copyFrom(item, returnTo1, -1);
+      next.returnTo1 = returnTo2;
+    } else if (UNISET_MINUS.contains(cp)) {
+      StateItem next = state.getNext().copyFrom(item, returnTo1, -1);
+      next.returnTo1 = returnTo2;
+      if (exponent) {
+        next.sawNegativeExponent = true;
+      } else {
+        next.sawNegative = true;
+      }
+    }
+  }
+
+  /**
+   * If <code>cp</code> is a grouping separator (as determined by the unicode set {@link
+   * #UNISET_GROUPING}), copies <code>item</code> to the new list in <code>state</code> and loops
+   * back to the same state. Also accepts if <code>cp</code> is the locale-specific grouping
+   * separator in {@link ParserState#groupingCp}, in which case the {@link
+   * StateItem#usesLocaleSymbols} flag is also set.
+   *
+   * @param cp The code point to check.
+   * @param state The state object to update.
+   * @param item The old state leading into the code point.
+   */
+  private static void acceptGrouping(
+      int cp, StateName nextName, ParserState state, StateItem item) {
+    // Do not accept mixed grouping separators in the same string.
+    if (item.groupingCp == -1) {
+      // First time seeing a grouping separator.
+      SeparatorType cpType = SeparatorType.fromCp(cp, state.mode);
+
+      // Always accept if exactly the same as the locale symbol.
+      // Otherwise, reject if UNKNOWN or in the same class as the decimal separator.
+      if (cp != state.groupingCp1 && cp != state.groupingCp2) {
+        if (cpType == SeparatorType.UNKNOWN) {
+          return;
+        }
+        if (cpType == SeparatorType.COMMA_LIKE
+            && (state.decimalType1 == SeparatorType.COMMA_LIKE
+                || state.decimalType2 == SeparatorType.COMMA_LIKE)) {
+          return;
+        }
+        if (cpType == SeparatorType.PERIOD_LIKE
+            && (state.decimalType1 == SeparatorType.PERIOD_LIKE
+                || state.decimalType2 == SeparatorType.PERIOD_LIKE)) {
+          return;
+        }
+      }
+
+      // A match was found.
+      StateItem next = state.getNext().copyFrom(item, nextName, cp);
+      next.groupingCp = cp;
+      next.groupingWidths <<= 4;
+    } else {
+      // Have already seen a grouping separator.
+      if (cp == item.groupingCp) {
+        StateItem next = state.getNext().copyFrom(item, nextName, cp);
+        next.groupingWidths <<= 4;
+      }
+    }
+  }
+
+  /**
+   * If <code>cp</code> is a decimal (as determined by the unicode set {@link #UNISET_DECIMAL}),
+   * copies <code>item</code> to the new list in <code>state</code> and goes to {@link
+   * StateName#AFTER_FRACTION_DIGIT}. Also accepts if <code>cp</code> is the locale-specific decimal
+   * point in {@link ParserState#decimalCp}, in which case the {@link StateItem#usesLocaleSymbols}
+   * flag is also set.
+   *
+   * @param cp The code point to check.
+   * @param state The state object to update.
+   * @param item The old state leading into the code point.
+   */
+  private static void acceptDecimalPoint(
+      int cp, StateName nextName, ParserState state, StateItem item) {
+    if (cp == item.groupingCp) {
+      // Don't accept a decimal point that is the same as the grouping separator
+      return;
+    }
+
+    SeparatorType cpType = SeparatorType.fromCp(cp, state.mode);
+
+    // We require that the decimal separator be in the same class as the locale.
+    if (cpType != state.decimalType1 && cpType != state.decimalType2) {
+      return;
+    }
+
+    // If in UNKNOWN or OTHER, require an exact match.
+    if (cpType == SeparatorType.OTHER_GROUPING || cpType == SeparatorType.UNKNOWN) {
+      if (cp != state.decimalCp1 && cp != state.decimalCp2) {
+        return;
+      }
+    }
+
+    // A match was found.
+    StateItem next = state.getNext().copyFrom(item, nextName, -1);
+    next.sawDecimalPoint = true;
+  }
+
+  private static void acceptNan(int cp, StateName nextName, ParserState state, StateItem item) {
+    CharSequence nan = state.symbols.getNaN();
+    long added = acceptString(cp, nextName, null, state, item, nan, 0);
+
+    // Set state in the items that were added by the function call
+    for (int i = Long.numberOfTrailingZeros(added); (1L << i) <= added; i++) {
+      if (((1L << i) & added) != 0) {
+        state.getItem(i).sawNaN = true;
+      }
+    }
+  }
+
+  private static void acceptInfinity(
+      int cp, StateName nextName, ParserState state, StateItem item) {
+    CharSequence inf = state.symbols.getInfinity();
+    long added = acceptString(cp, nextName, null, state, item, inf, 0);
+
+    // Set state in the items that were added by the function call
+    for (int i = Long.numberOfTrailingZeros(added); (1L << i) <= added; i++) {
+      if (((1L << i) & added) != 0) {
+        state.getItem(i).sawInfinity = true;
+      }
+    }
+  }
+
+  private static void acceptExponentSeparator(
+      int cp, StateName nextName, ParserState state, StateItem item) {
+    CharSequence exp = state.symbols.getExponentSeparator();
+    acceptString(cp, nextName, null, state, item, exp, 0);
+  }
+
+  private static void acceptPrefix(int cp, StateName nextName, ParserState state, StateItem item) {
+    for (AffixHolder holder : state.affixHolders) {
+      acceptAffixHolder(cp, nextName, state, item, holder, true);
+    }
+  }
+
+  private static void acceptSuffix(int cp, StateName nextName, ParserState state, StateItem item) {
+    if (item.affix != null) {
+      acceptAffixHolder(cp, nextName, state, item, item.affix, false);
+    } else {
+      for (AffixHolder holder : state.affixHolders) {
+        acceptAffixHolder(cp, nextName, state, item, holder, false);
+      }
+    }
+  }
+
+  private static void acceptAffixHolder(
+      int cp,
+      StateName nextName,
+      ParserState state,
+      StateItem item,
+      AffixHolder holder,
+      boolean prefix) {
+    if (holder == null) return;
+    String str = prefix ? holder.p : holder.s;
+    if (holder.strings) {
+      long added = acceptString(cp, nextName, null, state, item, str, 0);
+      // At most one item can be added upon consuming a string.
+      if (added != 0) {
+        int i = state.lastInsertedIndex();
+        // The following six lines are duplicated below; not enough for their own function.
+        state.getItem(i).affix = holder;
+        if (prefix) state.getItem(i).sawPrefix = true;
+        if (!prefix) state.getItem(i).sawSuffix = true;
+        if (holder.negative) state.getItem(i).sawNegative = true;
+        state.getItem(i).score++; // reward for consuming a prefix/suffix.
+      }
+    } else {
+      long added = acceptAffixPattern(cp, nextName, state, item, str, 0);
+      // Multiple items can be added upon consuming an affix pattern.
+      for (int i = Long.numberOfTrailingZeros(added); (1L << i) <= added; i++) {
+        if (((1L << i) & added) != 0) {
+          // The following six lines are duplicated above; not enough for their own function.
+          state.getItem(i).affix = holder;
+          if (prefix) state.getItem(i).sawPrefix = true;
+          if (!prefix) state.getItem(i).sawSuffix = true;
+          if (holder.negative) state.getItem(i).sawNegative = true;
+          state.getItem(i).score++; // reward for consuming a prefix/suffix.
+        }
+      }
+    }
+  }
+
+  private static void acceptStringOffset(int cp, ParserState state, StateItem item) {
+    acceptString(
+        cp, item.returnTo1, item.returnTo2, state, item, item.currentString, item.currentOffset);
+  }
+
+  /**
+   * Accepts a code point if the code point is compatible with the string at the given offset.
+   *
+   * <p>This method will add no more than one {@link StateItem} to the {@link ParserState}, which
+   * means that at most one bit will be set in the return value, corresponding to the return value
+   * of {@link ParserState#lastInsertedIndex()}.
+   *
+   * @param cp The current code point, which will be checked for a match to the string.
+   * @param returnTo1 The state to return to after reaching the end of the string.
+   * @param returnTo2 The state to save in <code>returnTo1</code> after reaching the end of the
+   *     string. Set to null if returning to the main state loop.
+   * @param state The current {@link ParserState}
+   * @param item The current {@link StateItem}
+   * @param str The string against which to check for a match.
+   * @param offset The number of chars into the string. Initial value should be 0.
+   * @return A bitmask where the bits correspond to the items that were added. Set to 0L if no items
+   *     were added.
+   */
+  private static long acceptString(
+      int cp,
+      StateName returnTo1,
+      StateName returnTo2,
+      ParserState state,
+      StateItem item,
+      CharSequence str,
+      int offset) {
+    if (str == null || str.length() == 0) return 0L;
+
+    // Fast path for fast mode
+    if (state.mode == ParseMode.FAST && Character.codePointAt(str, offset) != cp) return 0L;
+
+    // Skip over bidi code points at the beginning of the string.
+    // They will be accepted in the main loop.
+    int count = 0;
+    int referenceCp = -1;
+    boolean equals = false;
+    for (; offset < str.length(); offset += count) {
+      referenceCp = Character.codePointAt(str, offset);
+      count = Character.charCount(referenceCp);
+      equals = codePointEquals(cp, referenceCp, state);
+      if (!UNISET_BIDI.contains(cp)) break;
+    }
+
+    if (equals) {
+      // Matches first code point of the string
+      StateItem next = state.getNext().copyFrom(item, null, cp);
+
+      // Skip over ignorable code points in the middle of the string.
+      // They will be accepted in the main loop.
+      offset += count;
+      for (; offset < str.length(); offset += count) {
+        referenceCp = Character.codePointAt(str, offset);
+        count = Character.charCount(referenceCp);
+        if (!UNISET_BIDI.contains(cp)) break;
+      }
+
+      if (offset < str.length()) {
+        // String has more interesting code points.
+        next.name = StateName.INSIDE_STRING;
+        next.returnTo1 = returnTo1;
+        next.returnTo2 = returnTo2;
+        next.currentString = str;
+        next.currentOffset = offset;
+      } else {
+        // We've reached the end of the string.
+        next.name = returnTo1;
+        next.trailingCount = 0;
+        next.returnTo1 = returnTo2;
+        next.returnTo2 = null;
+      }
+      return 1L << state.lastInsertedIndex();
+    }
+    return 0L;
+  }
+
+  private static void acceptAffixPatternOffset(int cp, ParserState state, StateItem item) {
+    acceptAffixPattern(
+        cp, item.returnTo1, state, item, item.currentAffixPattern, item.currentStepwiseParserTag);
+  }
+
+  /**
+   * Accepts a code point if the code point is compatible with the affix pattern at the offset
+   * encoded in the tag argument.
+   *
+   * @param cp The current code point, which will be checked for a match to the string.
+   * @param returnTo The state to return to after reaching the end of the string.
+   * @param state The current {@link ParserState}
+   * @param item The current {@link StateItem}
+   * @param str The string containing the affix pattern.
+   * @param tag The current state of the stepwise parser. Initial value should be 0L.
+   * @return A bitmask where the bits correspond to the items that were added. Set to 0L if no items
+   *     were added.
+   */
+  private static long acceptAffixPattern(
+      int cp, StateName returnTo, ParserState state, StateItem item, CharSequence str, long tag) {
+    if (str == null || str.length() == 0) return 0L;
+
+    // Skip over ignorable code points at the beginning of the affix pattern.
+    // They will be accepted in the main loop.
+    int typeOrCp = 0;
+    boolean hasNext = true;
+    while (hasNext) {
+      tag = AffixPatternUtils.nextToken(tag, str);
+      typeOrCp = AffixPatternUtils.getTypeOrCp(tag);
+      hasNext = AffixPatternUtils.hasNext(tag, str);
+      if (typeOrCp < 0 || !isIgnorable(typeOrCp, state)) break;
+    }
+
+    // Convert from the returned tag to a code point, string, or currency to check
+    int resolvedCp = -1;
+    CharSequence resolvedStr = null;
+    boolean resolvedMinusSign = false;
+    boolean resolvedPlusSign = false;
+    boolean resolvedCurrency = false;
+    if (typeOrCp < 0) {
+      // Symbol
+      switch (typeOrCp) {
+        case AffixPatternUtils.TYPE_MINUS_SIGN:
+          resolvedMinusSign = true;
+          break;
+        case AffixPatternUtils.TYPE_PLUS_SIGN:
+          resolvedPlusSign = true;
+          break;
+        case AffixPatternUtils.TYPE_PERCENT:
+          resolvedStr = state.symbols.getPercentString();
+          break;
+        case AffixPatternUtils.TYPE_PERMILLE:
+          resolvedStr = state.symbols.getPerMillString();
+          break;
+        case AffixPatternUtils.TYPE_CURRENCY_SINGLE:
+        case AffixPatternUtils.TYPE_CURRENCY_DOUBLE:
+        case AffixPatternUtils.TYPE_CURRENCY_TRIPLE:
+          resolvedCurrency = true;
+          break;
+        default:
+          throw new AssertionError();
+      }
+    } else {
+      resolvedCp = typeOrCp;
+    }
+
+    // Skip over ignorable code points in the middle of the affix pattern.
+    // They will be accepted in the main loop.
+    while (hasNext) {
+      long futureTag = AffixPatternUtils.nextToken(tag, str);
+      int futureTypeOrCp = AffixPatternUtils.getTypeOrCp(futureTag);
+      if (futureTypeOrCp < 0 || !isIgnorable(futureTypeOrCp, state)) break;
+      tag = futureTag;
+      typeOrCp = futureTypeOrCp;
+      hasNext = AffixPatternUtils.hasNext(tag, str);
+    }
+
+    long added = 0L;
+    if (resolvedCp >= 0) {
+      // Code point
+      if (!codePointEquals(cp, resolvedCp, state)) return 0L;
+      StateItem next = state.getNext().copyFrom(item, null, cp);
+
+      if (hasNext) {
+        // Additional tokens in affix string.
+        next.name = StateName.INSIDE_AFFIX_PATTERN;
+        next.returnTo1 = returnTo;
+      } else {
+        // Reached last token in affix string.
+        next.name = returnTo;
+        next.trailingCount = 0;
+        next.returnTo1 = null;
+      }
+      added |= 1L << state.lastInsertedIndex();
+    }
+    if (resolvedMinusSign || resolvedPlusSign) {
+      // Sign
+      if (hasNext) {
+        acceptMinusOrPlusSign(cp, StateName.INSIDE_AFFIX_PATTERN, returnTo, state, item, false);
+      } else {
+        acceptMinusOrPlusSign(cp, returnTo, null, state, item, false);
+      }
+      // Decide whether to accept a custom string
+      if (resolvedMinusSign) {
+        String mss = state.symbols.getMinusSignString();
+        int mssCp = Character.codePointAt(mss, 0);
+        if (mss.length() != Character.charCount(mssCp) || !UNISET_MINUS.contains(mssCp)) {
+          resolvedStr = mss;
+        }
+      }
+      if (resolvedPlusSign) {
+        String pss = state.symbols.getPlusSignString();
+        int pssCp = Character.codePointAt(pss, 0);
+        if (pss.length() != Character.charCount(pssCp) || !UNISET_MINUS.contains(pssCp)) {
+          resolvedStr = pss;
+        }
+      }
+    }
+    if (resolvedStr != null) {
+      // String symbol
+      if (hasNext) {
+        added |=
+            acceptString(cp, StateName.INSIDE_AFFIX_PATTERN, returnTo, state, item, resolvedStr, 0);
+      } else {
+        added |= acceptString(cp, returnTo, null, state, item, resolvedStr, 0);
+      }
+    }
+    if (resolvedCurrency) {
+      // Currency symbol
+      if (hasNext) {
+        added |= acceptCurrency(cp, StateName.INSIDE_AFFIX_PATTERN, returnTo, state, item);
+      } else {
+        added |= acceptCurrency(cp, returnTo, null, state, item);
+      }
+    }
+
+    // Set state in the items that were added by the function calls
+    for (int i = Long.numberOfTrailingZeros(added); (1L << i) <= added; i++) {
+      if (((1L << i) & added) != 0) {
+        state.getItem(i).currentAffixPattern = str;
+        state.getItem(i).currentStepwiseParserTag = tag;
+      }
+    }
+    return added;
+  }
+
+  /**
+   * This method can add up to four items to the new list in <code>state</code>.
+   *
+   * <p>If <code>cp</code> is equal to any known ISO code or long name, copies <code>item</code> to
+   * the new list in <code>state</code> and sets its ISO code to the corresponding currency.
+   *
+   * <p>If <code>cp</code> is the first code point of any ISO code or long name having more them one
+   * code point in length, copies <code>item</code> to the new list in <code>state</code> along with
+   * an instance of {@link TextTrieMap.ParseState} for tracking the following code points.
+   *
+   * @param cp The code point to check.
+   * @param state The state object to update.
+   * @param item The old state leading into the code point.
+   */
+  private static void acceptCurrency(
+      int cp, StateName nextName, ParserState state, StateItem item) {
+    acceptCurrency(cp, nextName, null, state, item);
+  }
+
+  private static long acceptCurrency(
+      int cp, StateName returnTo1, StateName returnTo2, ParserState state, StateItem item) {
+    if (item.sawCurrency) return 0L;
+    long added = 0L;
+
+    // Accept from local currency information
+    String str1, str2;
+    Currency currency = state.properties.getCurrency();
+    if (currency != null) {
+      str1 = currency.getName(state.symbols.getULocale(), Currency.SYMBOL_NAME, null);
+      str2 = currency.getCurrencyCode();
+      // TODO: Should we also accept long names? In currency mode, they are in the CLDR data.
+    } else {
+      currency = state.symbols.getCurrency();
+      str1 = state.symbols.getCurrencySymbol();
+      str2 = state.symbols.getInternationalCurrencySymbol();
+    }
+    added |= acceptString(cp, returnTo1, returnTo2, state, item, str1, 0);
+    added |= acceptString(cp, returnTo1, returnTo2, state, item, str2, 0);
+    for (int i = Long.numberOfTrailingZeros(added); (1L << i) <= added; i++) {
+      if (((1L << i) & added) != 0) {
+        state.getItem(i).sawCurrency = true;
+        state.getItem(i).isoCode = str2;
+      }
+    }
+
+    // Accept from CLDR data
+    if (state.parseCurrency) {
+      ULocale uloc = state.symbols.getULocale();
+      TextTrieMap<Currency.CurrencyStringInfo>.ParseState trie1 =
+          Currency.openParseState(uloc, cp, Currency.LONG_NAME);
+      TextTrieMap<Currency.CurrencyStringInfo>.ParseState trie2 =
+          Currency.openParseState(uloc, cp, Currency.SYMBOL_NAME);
+      added |= acceptCurrencyHelper(cp, returnTo1, returnTo2, state, item, trie1);
+      added |= acceptCurrencyHelper(cp, returnTo1, returnTo2, state, item, trie2);
+    }
+
+    return added;
+  }
+
+  /**
+   * If <code>cp</code> is the next code point of any currency, copies <code>item</code> to the new
+   * list in <code>state</code> along with an instance of {@link TextTrieMap.ParseState} for
+   * tracking the following code points.
+   *
+   * <p>This method should only be called in a state following {@link #acceptCurrency}.
+   *
+   * @param cp The code point to check.
+   * @param state The state object to update.
+   * @param item The old state leading into the code point.
+   */
+  private static void acceptCurrencyOffset(int cp, ParserState state, StateItem item) {
+    acceptCurrencyHelper(
+        cp, item.returnTo1, item.returnTo2, state, item, item.currentCurrencyTrieState);
+  }
+
+  private static long acceptCurrencyHelper(
+      int cp,
+      StateName returnTo1,
+      StateName returnTo2,
+      ParserState state,
+      StateItem item,
+      TextTrieMap<Currency.CurrencyStringInfo>.ParseState trieState) {
+    if (trieState == null) return 0L;
+    trieState.accept(cp);
+    long added = 0L;
+    Iterator<Currency.CurrencyStringInfo> currentMatches = trieState.getCurrentMatches();
+    if (currentMatches != null) {
+      // Match on current code point
+      // TODO: What should happen with multiple currency matches?
+      StateItem next = state.getNext().copyFrom(item, returnTo1, -1);
+      next.returnTo1 = returnTo2;
+      next.returnTo2 = null;
+      next.sawCurrency = true;
+      next.isoCode = currentMatches.next().getISOCode();
+      added |= 1L << state.lastInsertedIndex();
+    }
+    if (!trieState.atEnd()) {
+      // Prepare for matches on future code points
+      StateItem next = state.getNext().copyFrom(item, StateName.INSIDE_CURRENCY, -1);
+      next.returnTo1 = returnTo1;
+      next.returnTo2 = returnTo2;
+      next.currentCurrencyTrieState = trieState;
+      added |= 1L << state.lastInsertedIndex();
+    }
+    return added;
+  }
+
+  private static long acceptDigitTrie(
+      int cp, StateName nextName, ParserState state, StateItem item, DigitType type) {
+    assert state.digitTrie != null;
+    TextTrieMap<Byte>.ParseState trieState = state.digitTrie.openParseState(cp);
+    if (trieState == null) return 0L;
+    return acceptDigitTrieHelper(cp, nextName, state, item, type, trieState);
+  }
+
+  private static void acceptDigitTrieOffset(int cp, ParserState state, StateItem item) {
+    acceptDigitTrieHelper(
+        cp, item.returnTo1, state, item, item.currentDigitType, item.currentDigitTrieState);
+  }
+
+  private static long acceptDigitTrieHelper(
+      int cp,
+      StateName returnTo1,
+      ParserState state,
+      StateItem item,
+      DigitType type,
+      TextTrieMap<Byte>.ParseState trieState) {
+    if (trieState == null) return 0L;
+    trieState.accept(cp);
+    long added = 0L;
+    Iterator<Byte> currentMatches = trieState.getCurrentMatches();
+    if (currentMatches != null) {
+      // Match on current code point
+      byte digit = currentMatches.next();
+      StateItem next = state.getNext().copyFrom(item, returnTo1, -1);
+      next.returnTo1 = null;
+      next.appendDigit(digit, type);
+      added |= 1L << state.lastInsertedIndex();
+    }
+    if (!trieState.atEnd()) {
+      // Prepare for matches on future code points
+      StateItem next = state.getNext().copyFrom(item, StateName.INSIDE_DIGIT, -1);
+      next.returnTo1 = returnTo1;
+      next.currentDigitTrieState = trieState;
+      next.currentDigitType = type;
+      added |= 1L << state.lastInsertedIndex();
+    }
+    return added;
+  }
+
+  /**
+   * Checks whether the two given code points are equal after applying case mapping as requested in
+   * the ParserState.
+   *
+   * @see #acceptString
+   * @see #acceptAffixPattern
+   */
+  private static boolean codePointEquals(int cp1, int cp2, ParserState state) {
+    if (!state.caseSensitive) {
+      cp1 = UCharacter.foldCase(cp1, true);
+      cp2 = UCharacter.foldCase(cp2, true);
+    }
+    return cp1 == cp2;
+  }
+
+  /**
+   * Checks whether the given code point is "ignorable" and should be skipped. BiDi characters are
+   * always ignorable, and whitespace is ignorable in lenient mode.
+   *
+   * @param cp The code point to test. Returns false if cp is negative.
+   * @param state The current {@link ParserState}, used for determining strict mode.
+   * @return true if cp is bidi or whitespace in lenient mode; false otherwise.
+   */
+  private static boolean isIgnorable(int cp, ParserState state) {
+    if (cp < 0) return false;
+    if (UNISET_BIDI.contains(cp)) return true;
+    return state.mode == ParseMode.LENIENT && UNISET_WHITESPACE.contains(cp);
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternString.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternString.java
new file mode 100644 (file)
index 0000000..ad5c287
--- /dev/null
@@ -0,0 +1,855 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+
+import com.ibm.icu.impl.number.formatters.PaddingFormat;
+import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
+import com.ibm.icu.text.DecimalFormatSymbols;
+
+/**
+ * Handles parsing and creation of the compact pattern string representation of a decimal format.
+ */
+public class PatternString {
+
+  /**
+   * Parses a pattern string into a new property bag.
+   *
+   * @param pattern The pattern string, like "#,##0.00"
+   * @param ignoreRounding Whether to leave out rounding information (minFrac, maxFrac, and rounding
+   *     increment) when parsing the pattern. This may be desirable if a custom rounding mode, such
+   *     as CurrencyUsage, is to be used instead.
+   * @return A property bag object.
+   * @throws IllegalArgumentException If there is a syntax error in the pattern string.
+   */
+  public static Properties parseToProperties(String pattern, boolean ignoreRounding) {
+    Properties properties = new Properties();
+    LdmlDecimalPatternParser.parse(pattern, properties, ignoreRounding);
+    return properties;
+  }
+
+  public static Properties parseToProperties(String pattern) {
+    return parseToProperties(pattern, false);
+  }
+
+  /**
+   * Parses a pattern string into an existing property bag. All properties that can be encoded into
+   * a pattern string will be overwritten with either their default value or with the value coming
+   * from the pattern string. Properties that cannot be encoded into a pattern string, such as
+   * rounding mode, are not modified.
+   *
+   * @param pattern The pattern string, like "#,##0.00"
+   * @param properties The property bag object to overwrite.
+   * @param ignoreRounding Whether to leave out rounding information (minFrac, maxFrac, and rounding
+   *     increment) when parsing the pattern. This may be desirable if a custom rounding mode, such
+   *     as CurrencyUsage, is to be used instead.
+   * @throws IllegalArgumentException If there was a syntax error in the pattern string.
+   */
+  public static void parseToExistingProperties(
+      String pattern, Properties properties, boolean ignoreRounding) {
+    LdmlDecimalPatternParser.parse(pattern, properties, ignoreRounding);
+  }
+
+  public static void parseToExistingProperties(String pattern, Properties properties) {
+    parseToExistingProperties(pattern, properties, false);
+  }
+
+  /**
+   * Creates a pattern string from a property bag.
+   *
+   * <p>Since pattern strings support only a subset of the functionality available in a property
+   * bag, a new property bag created from the string returned by this function may not be the same
+   * as the original property bag.
+   *
+   * @param properties The property bag to serialize.
+   * @return A pattern string approximately serializing the property bag.
+   */
+  public static String propertiesToString(Properties properties) {
+    StringBuilder sb = new StringBuilder();
+
+    // Convenience references
+    // The Math.min() calls prevent DoS
+    int dosMax = 100;
+    int groupingSize = Math.min(properties.getSecondaryGroupingSize(), dosMax);
+    int firstGroupingSize = Math.min(properties.getGroupingSize(), dosMax);
+    int paddingWidth = Math.min(properties.getFormatWidth(), dosMax);
+    PadPosition paddingLocation = properties.getPadPosition();
+    String paddingString = properties.getPadString();
+    int minInt = Math.max(Math.min(properties.getMinimumIntegerDigits(), dosMax), 0);
+    int maxInt = Math.min(properties.getMaximumIntegerDigits(), dosMax);
+    int minFrac = Math.max(Math.min(properties.getMinimumFractionDigits(), dosMax), 0);
+    int maxFrac = Math.min(properties.getMaximumFractionDigits(), dosMax);
+    int minSig = Math.min(properties.getMinimumSignificantDigits(), dosMax);
+    int maxSig = Math.min(properties.getMaximumSignificantDigits(), dosMax);
+    boolean alwaysShowDecimal = properties.getDecimalSeparatorAlwaysShown();
+    int exponentDigits = Math.min(properties.getMinimumExponentDigits(), dosMax);
+    boolean exponentShowPlusSign = properties.getExponentSignAlwaysShown();
+    String pp = properties.getPositivePrefix();
+    String ppp = properties.getPositivePrefixPattern();
+    String ps = properties.getPositiveSuffix();
+    String psp = properties.getPositiveSuffixPattern();
+    String np = properties.getNegativePrefix();
+    String npp = properties.getNegativePrefixPattern();
+    String ns = properties.getNegativeSuffix();
+    String nsp = properties.getNegativeSuffixPattern();
+
+    // Prefixes
+    if (ppp != null) sb.append(ppp);
+    AffixPatternUtils.escape(pp, sb);
+    int afterPrefixPos = sb.length();
+
+    // Figure out the grouping sizes.
+    int grouping1, grouping2, grouping;
+    if (groupingSize != Math.min(dosMax, Properties.DEFAULT_SECONDARY_GROUPING_SIZE)
+        && firstGroupingSize != Math.min(dosMax, Properties.DEFAULT_GROUPING_SIZE)
+        && groupingSize != firstGroupingSize) {
+      grouping = groupingSize;
+      grouping1 = groupingSize;
+      grouping2 = firstGroupingSize;
+    } else if (groupingSize != Math.min(dosMax, Properties.DEFAULT_SECONDARY_GROUPING_SIZE)) {
+      grouping = groupingSize;
+      grouping1 = 0;
+      grouping2 = groupingSize;
+    } else if (firstGroupingSize != Math.min(dosMax, Properties.DEFAULT_GROUPING_SIZE)) {
+      grouping = groupingSize;
+      grouping1 = 0;
+      grouping2 = firstGroupingSize;
+    } else {
+      grouping = 0;
+      grouping1 = 0;
+      grouping2 = 0;
+    }
+    int groupingLength = grouping1 + grouping2 + 1;
+
+    // Figure out the digits we need to put in the pattern.
+    BigDecimal roundingInterval = properties.getRoundingIncrement();
+    StringBuilder digitsString = new StringBuilder();
+    int digitsStringScale = 0;
+    if (maxSig != Math.min(dosMax, Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS)) {
+      // Significant Digits.
+      while (digitsString.length() < minSig) {
+        digitsString.append('@');
+      }
+      while (digitsString.length() < maxSig) {
+        digitsString.append('#');
+      }
+    } else if (roundingInterval != Properties.DEFAULT_ROUNDING_INCREMENT) {
+      // Rounding Interval.
+      digitsStringScale = -roundingInterval.scale();
+      // TODO: Check for DoS here?
+      String str = roundingInterval.scaleByPowerOfTen(roundingInterval.scale()).toPlainString();
+      if (str.charAt(0) == '\'') {
+        // TODO: Unsupported operation exception or fail silently?
+        digitsString.append(str, 1, str.length());
+      } else {
+        digitsString.append(str);
+      }
+    }
+    while (digitsString.length() + digitsStringScale < minInt) {
+      digitsString.insert(0, '0');
+    }
+    while (-digitsStringScale < minFrac) {
+      digitsString.append('0');
+      digitsStringScale--;
+    }
+
+    // Write the digits to the string builder
+    int m0 = Math.max(groupingLength, digitsString.length() + digitsStringScale);
+    m0 = (maxInt != dosMax) ? Math.max(maxInt, m0) - 1 : m0 - 1;
+    int mN = (maxFrac != dosMax) ? Math.min(-maxFrac, digitsStringScale) : digitsStringScale;
+    for (int magnitude = m0; magnitude >= mN; magnitude--) {
+      int di = digitsString.length() + digitsStringScale - magnitude - 1;
+      if (di < 0 || di >= digitsString.length()) {
+        sb.append('#');
+      } else {
+        sb.append(digitsString.charAt(di));
+      }
+      if (magnitude > grouping2 && grouping > 0 && (magnitude - grouping2) % grouping == 0) {
+        sb.append(',');
+      } else if (magnitude > 0 && magnitude == grouping2) {
+        sb.append(',');
+      } else if (magnitude == 0 && (alwaysShowDecimal || mN < 0)) {
+        sb.append('.');
+      }
+    }
+
+    // Exponential notation
+    if (exponentDigits != Math.min(dosMax, Properties.DEFAULT_MINIMUM_EXPONENT_DIGITS)) {
+      sb.append('E');
+      if (exponentShowPlusSign) {
+        sb.append('+');
+      }
+      for (int i = 0; i < exponentDigits; i++) {
+        sb.append('0');
+      }
+    }
+
+    // Suffixes
+    int beforeSuffixPos = sb.length();
+    if (psp != null) sb.append(psp);
+    AffixPatternUtils.escape(ps, sb);
+
+    // Resolve Padding
+    if (paddingWidth != Properties.DEFAULT_FORMAT_WIDTH) {
+      while (paddingWidth - sb.length() > 0) {
+        sb.insert(afterPrefixPos, '#');
+        beforeSuffixPos++;
+      }
+      int addedLength;
+      switch (paddingLocation) {
+        case BEFORE_PREFIX:
+          addedLength = escapePaddingString(paddingString, sb, 0);
+          sb.insert(0, '*');
+          afterPrefixPos += addedLength + 1;
+          beforeSuffixPos += addedLength + 1;
+          break;
+        case AFTER_PREFIX:
+          addedLength = escapePaddingString(paddingString, sb, afterPrefixPos);
+          sb.insert(afterPrefixPos, '*');
+          afterPrefixPos += addedLength + 1;
+          beforeSuffixPos += addedLength + 1;
+          break;
+        case BEFORE_SUFFIX:
+          escapePaddingString(paddingString, sb, beforeSuffixPos);
+          sb.insert(beforeSuffixPos, '*');
+          break;
+        case AFTER_SUFFIX:
+          sb.append('*');
+          escapePaddingString(paddingString, sb, sb.length());
+          break;
+      }
+    }
+
+    // Negative affixes
+    // Ignore if the negative prefix pattern is "-" and the negative suffix is empty
+    if (np != null
+        || ns != null
+        || (npp == null && nsp != null)
+        || (npp != null && (npp.length() != 1 || npp.charAt(0) != '-' || nsp.length() != 0))) {
+      sb.append(';');
+      if (npp != null) sb.append(npp);
+      AffixPatternUtils.escape(np, sb);
+      // Copy the positive digit format into the negative.
+      // This is optional; the pattern is the same as if '#' were appended here instead.
+      sb.append(sb, afterPrefixPos, beforeSuffixPos);
+      if (nsp != null) sb.append(nsp);
+      AffixPatternUtils.escape(ns, sb);
+    }
+
+    return sb.toString();
+  }
+
+  /** @return The number of chars inserted. */
+  private static int escapePaddingString(CharSequence input, StringBuilder output, int startIndex) {
+    if (input == null || input.length() == 0) input = PaddingFormat.FALLBACK_PADDING_STRING;
+    int startLength = output.length();
+    if (input.length() == 1) {
+      if (input.equals("'")) {
+        output.insert(startIndex, "''");
+      } else {
+        output.insert(startIndex, input);
+      }
+    } else {
+      output.insert(startIndex, '\'');
+      int offset = 1;
+      for (int i = 0; i < input.length(); i++) {
+        // it's okay to deal in chars here because the quote mark is the only interesting thing.
+        char ch = input.charAt(i);
+        if (ch == '\'') {
+          output.insert(startIndex + offset, "''");
+          offset += 2;
+        } else {
+          output.insert(startIndex + offset, ch);
+          offset += 1;
+        }
+      }
+      output.insert(startIndex + offset, '\'');
+    }
+    return output.length() - startLength;
+  }
+
+  /**
+   * Converts a pattern between standard notation and localized notation. Localized notation means
+   * that instead of using generic placeholders in the pattern, you use the corresponding
+   * locale-specific characters instead. For example, in locale <em>fr-FR</em>, the period in the
+   * pattern "0.000" means "decimal" in standard notation (as it does in every other locale), but it
+   * means "grouping" in localized notation.
+   *
+   * @param input The pattern to convert.
+   * @param symbols The symbols corresponding to the localized pattern.
+   * @param toLocalized true to convert from standard to localized notation; false to convert from
+   *     localized to standard notation.
+   * @return The pattern expressed in the other notation.
+   * @deprecated ICU 59 This method is provided for backwards compatibility and should not be used
+   *     in any new code.
+   */
+  @Deprecated
+  public static String convertLocalized(
+      CharSequence input, DecimalFormatSymbols symbols, boolean toLocalized) {
+    if (input == null) return null;
+
+    /// This is not the prettiest function in the world, but it gets the job done. ///
+
+    // Construct a table of code points to be converted between localized and standard.
+    int[][] table = new int[6][2];
+    int standIdx = toLocalized ? 0 : 1;
+    int localIdx = toLocalized ? 1 : 0;
+    table[0][standIdx] = '%';
+    table[0][localIdx] = symbols.getPercent();
+    table[1][standIdx] = '‰';
+    table[1][localIdx] = symbols.getPerMill();
+    table[2][standIdx] = '.';
+    table[2][localIdx] = symbols.getDecimalSeparator();
+    table[3][standIdx] = ',';
+    table[3][localIdx] = symbols.getGroupingSeparator();
+    table[4][standIdx] = '-';
+    table[4][localIdx] = symbols.getMinusSign();
+    table[5][standIdx] = '+';
+    table[5][localIdx] = symbols.getPlusSign();
+
+    // Special case: localIdx characters are NOT allowed to be quotes, like in de_CH.
+    // Use '’' instead.
+    for (int i = 0; i < table.length; i++) {
+      if (table[i][localIdx] == '\'') {
+        table[i][localIdx] = '’';
+      }
+    }
+
+    // Iterate through the string and convert
+    int offset = 0;
+    int state = 0;
+    StringBuilder result = new StringBuilder();
+    for (; offset < input.length(); ) {
+      int cp = Character.codePointAt(input, offset);
+      int cpToAppend = cp;
+
+      if (state == 1 || state == 3 || state == 4) {
+        // Inside user-specified quote
+        if (cp == '\'') {
+          if (state == 1) {
+            state = 0;
+          } else if (state == 3) {
+            state = 2;
+            cpToAppend = -1;
+          } else {
+            state = 2;
+          }
+        }
+      } else {
+        // Base state or inside special character quote
+        if (cp == '\'') {
+          if (state == 2 && offset + 1 < input.length()) {
+            int nextCp = Character.codePointAt(input, offset + 1);
+            if (nextCp == '\'') {
+              // escaped quote
+              state = 4;
+            } else {
+              // begin user-specified quote sequence
+              // we are already in a quote sequence, so omit the opening quote
+              state = 3;
+              cpToAppend = -1;
+            }
+          } else {
+            state = 1;
+          }
+        } else {
+          boolean needsSpecialQuote = false;
+          for (int i = 0; i < table.length; i++) {
+            if (table[i][0] == cp) {
+              cpToAppend = table[i][1];
+              needsSpecialQuote = false; // in case an earlier translation triggered it
+              break;
+            } else if (table[i][1] == cp) {
+              needsSpecialQuote = true;
+            }
+          }
+          if (state == 0 && needsSpecialQuote) {
+            state = 2;
+            result.appendCodePoint('\'');
+          } else if (state == 2 && !needsSpecialQuote) {
+            state = 0;
+            result.appendCodePoint('\'');
+          }
+        }
+      }
+      if (cpToAppend != -1) {
+        result.appendCodePoint(cpToAppend);
+      }
+      offset += Character.charCount(cp);
+    }
+    if (state == 2) {
+      result.appendCodePoint('\'');
+    }
+    return result.toString();
+  }
+
+  /** Implements a recursive descent parser for decimal format patterns. */
+  static class LdmlDecimalPatternParser {
+
+    /**
+     * An internal, intermediate data structure used for storing parse results before they are
+     * finalized into a DecimalFormatPattern.Builder.
+     */
+    private static class PatternParseResult {
+      SubpatternParseResult positive = new SubpatternParseResult();
+      SubpatternParseResult negative = null;
+
+      /** Finalizes the temporary data stored in the PatternParseResult to the Builder. */
+      void saveToProperties(Properties properties, boolean ignoreRounding) {
+        // Translate from PatternState to Properties.
+        // Note that most data from "negative" is ignored per the specification of DecimalFormat.
+
+        // Grouping settings
+        if (positive.groupingSizes[1] != -1) {
+          properties.setGroupingSize(positive.groupingSizes[0]);
+        } else {
+          properties.setGroupingSize(Properties.DEFAULT_GROUPING_SIZE);
+        }
+        if (positive.groupingSizes[2] != -1) {
+          properties.setSecondaryGroupingSize(positive.groupingSizes[1]);
+        } else {
+          properties.setSecondaryGroupingSize(Properties.DEFAULT_SECONDARY_GROUPING_SIZE);
+        }
+
+        // For backwards compatibility, require that the pattern emit at least one min digit.
+        int minInt, minFrac;
+        if (positive.totalIntegerDigits == 0 && positive.maximumFractionDigits > 0) {
+          // patterns like ".##"
+          minInt = 0;
+          minFrac = Math.max(1, positive.minimumFractionDigits);
+        } else if (positive.minimumIntegerDigits == 0 && positive.minimumFractionDigits == 0) {
+          // patterns like "#.##"
+          minInt = 1;
+          minFrac = 0;
+        } else {
+          minInt = positive.minimumIntegerDigits;
+          minFrac = positive.minimumFractionDigits;
+        }
+
+        // Rounding settings
+        // Don't set basic rounding when there is a currency sign; defer to CurrencyUsage
+        if (positive.minimumSignificantDigits > 0) {
+          properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS);
+          properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS);
+          properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
+          properties.setMinimumSignificantDigits(positive.minimumSignificantDigits);
+          properties.setMaximumSignificantDigits(positive.maximumSignificantDigits);
+        } else if (!positive.rounding.isZero()) {
+          if (!ignoreRounding) {
+            properties.setMinimumFractionDigits(minFrac);
+            properties.setMaximumFractionDigits(positive.maximumFractionDigits);
+            properties.setRoundingIncrement(positive.rounding.toBigDecimal());
+          } else {
+            properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS);
+            properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS);
+            properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
+          }
+          properties.setMinimumSignificantDigits(Properties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS);
+          properties.setMaximumSignificantDigits(Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS);
+        } else {
+          if (!ignoreRounding) {
+            properties.setMinimumFractionDigits(minFrac);
+            properties.setMaximumFractionDigits(positive.maximumFractionDigits);
+            properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
+          } else {
+            properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS);
+            properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS);
+            properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
+          }
+          properties.setMinimumSignificantDigits(Properties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS);
+          properties.setMaximumSignificantDigits(Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS);
+        }
+
+        // If the pattern ends with a '.' then force the decimal point.
+        if (positive.hasDecimal && positive.maximumFractionDigits == 0) {
+          properties.setDecimalSeparatorAlwaysShown(true);
+        } else {
+          properties.setDecimalSeparatorAlwaysShown(false);
+        }
+
+        // Scientific notation settings
+        if (positive.exponentDigits > 0) {
+          properties.setExponentSignAlwaysShown(positive.exponentShowPlusSign);
+          properties.setMinimumExponentDigits(positive.exponentDigits);
+          if (positive.minimumSignificantDigits == 0) {
+            // patterns without '@' can define max integer digits, used for engineering notation
+            properties.setMinimumIntegerDigits(positive.minimumIntegerDigits);
+            properties.setMaximumIntegerDigits(positive.totalIntegerDigits);
+          } else {
+            // patterns with '@' cannot define max integer digits
+            properties.setMinimumIntegerDigits(1);
+            properties.setMaximumIntegerDigits(Properties.DEFAULT_MAXIMUM_INTEGER_DIGITS);
+          }
+        } else {
+          properties.setExponentSignAlwaysShown(Properties.DEFAULT_EXPONENT_SIGN_ALWAYS_SHOWN);
+          properties.setMinimumExponentDigits(Properties.DEFAULT_MINIMUM_EXPONENT_DIGITS);
+          properties.setMinimumIntegerDigits(minInt);
+          properties.setMaximumIntegerDigits(Properties.DEFAULT_MAXIMUM_INTEGER_DIGITS);
+        }
+
+        // Padding settings
+        if (positive.padding.length() > 0) {
+          // The width of the positive prefix and suffix templates are included in the padding
+          int paddingWidth =
+              positive.paddingWidth
+                  + AffixPatternUtils.unescapedLength(positive.prefix)
+                  + AffixPatternUtils.unescapedLength(positive.suffix);
+          properties.setFormatWidth(paddingWidth);
+          if (positive.padding.length() == 1) {
+            properties.setPadString(positive.padding.toString());
+          } else if (positive.padding.length() == 2) {
+            if (positive.padding.charAt(0) == '\'') {
+              properties.setPadString("'");
+            } else {
+              properties.setPadString(positive.padding.toString());
+            }
+          } else {
+            properties.setPadString(
+                positive.padding.subSequence(1, positive.padding.length() - 1).toString());
+          }
+          assert positive.paddingLocation != null;
+          properties.setPadPosition(positive.paddingLocation);
+        } else {
+          properties.setFormatWidth(Properties.DEFAULT_FORMAT_WIDTH);
+          properties.setPadString(Properties.DEFAULT_PAD_STRING);
+          properties.setPadPosition(Properties.DEFAULT_PAD_POSITION);
+        }
+
+        // Set the affixes
+        // Always call the setter, even if the prefixes are empty, especially in the case of the
+        // negative prefix pattern, to prevent default values from overriding the pattern.
+        properties.setPositivePrefixPattern(positive.prefix.toString());
+        properties.setPositiveSuffixPattern(positive.suffix.toString());
+        if (negative != null) {
+          properties.setNegativePrefixPattern(negative.prefix.toString());
+          properties.setNegativeSuffixPattern(negative.suffix.toString());
+        } else {
+          properties.setNegativePrefixPattern(null);
+          properties.setNegativeSuffixPattern(null);
+        }
+
+        // Set the magnitude multiplier
+        if (positive.hasPercentSign) {
+          properties.setMagnitudeMultiplier(2);
+        } else if (positive.hasPerMilleSign) {
+          properties.setMagnitudeMultiplier(3);
+        } else {
+          properties.setMagnitudeMultiplier(Properties.DEFAULT_MAGNITUDE_MULTIPLIER);
+        }
+      }
+    }
+
+    private static class SubpatternParseResult {
+      int[] groupingSizes = new int[] {0, -1, -1};
+      int minimumIntegerDigits = 0;
+      int totalIntegerDigits = 0;
+      int minimumFractionDigits = 0;
+      int maximumFractionDigits = 0;
+      int minimumSignificantDigits = 0;
+      int maximumSignificantDigits = 0;
+      boolean hasDecimal = false;
+      int paddingWidth = 0;
+      PadPosition paddingLocation = null;
+      FormatQuantity4 rounding = new FormatQuantity4();
+      boolean exponentShowPlusSign = false;
+      int exponentDigits = 0;
+      boolean hasPercentSign = false;
+      boolean hasPerMilleSign = false;
+      boolean hasCurrencySign = false;
+
+      StringBuilder padding = new StringBuilder();
+      StringBuilder prefix = new StringBuilder();
+      StringBuilder suffix = new StringBuilder();
+    }
+
+    /** An internal class used for tracking the cursor during parsing of a pattern string. */
+    private static class ParserState {
+      final String pattern;
+      int offset;
+
+      ParserState(String pattern) {
+        this.pattern = pattern;
+        this.offset = 0;
+      }
+
+      int peek() {
+        if (offset == pattern.length()) {
+          return -1;
+        } else {
+          return pattern.codePointAt(offset);
+        }
+      }
+
+      int next() {
+        int codePoint = peek();
+        offset += Character.charCount(codePoint);
+        return codePoint;
+      }
+
+      IllegalArgumentException toParseException(String message) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("Unexpected character in decimal format pattern: '");
+        sb.append(pattern);
+        sb.append("': ");
+        sb.append(message);
+        sb.append(": ");
+        if (peek() == -1) {
+          sb.append("EOL");
+        } else {
+          sb.append("'");
+          sb.append(Character.toChars(peek()));
+          sb.append("'");
+        }
+        return new IllegalArgumentException(sb.toString());
+      }
+    }
+
+    static void parse(String pattern, Properties properties, boolean ignoreRounding) {
+      if (pattern == null || pattern.length() == 0) {
+        // Backwards compatibility requires that we reset to the default values.
+        // TODO: Only overwrite the properties that "saveToProperties" normally touches?
+        properties.clear();
+        return;
+      }
+
+      // TODO: Use whitespace characters from PatternProps
+      // TODO: Use thread locals here.
+      ParserState state = new ParserState(pattern);
+      PatternParseResult result = new PatternParseResult();
+      consumePattern(state, result);
+      result.saveToProperties(properties, ignoreRounding);
+    }
+
+    private static void consumePattern(ParserState state, PatternParseResult result) {
+      // pattern := subpattern (';' subpattern)?
+      consumeSubpattern(state, result.positive);
+      if (state.peek() == ';') {
+        state.next(); // consume the ';'
+        result.negative = new SubpatternParseResult();
+        consumeSubpattern(state, result.negative);
+      }
+      if (state.peek() != -1) {
+        throw state.toParseException("pattern");
+      }
+    }
+
+    private static void consumeSubpattern(ParserState state, SubpatternParseResult result) {
+      // subpattern := literals? number exponent? literals?
+      consumePadding(state, result, PadPosition.BEFORE_PREFIX);
+      consumeAffix(state, result, result.prefix);
+      consumePadding(state, result, PadPosition.AFTER_PREFIX);
+      consumeFormat(state, result);
+      consumeExponent(state, result);
+      consumePadding(state, result, PadPosition.BEFORE_SUFFIX);
+      consumeAffix(state, result, result.suffix);
+      consumePadding(state, result, PadPosition.AFTER_SUFFIX);
+    }
+
+    private static void consumePadding(
+        ParserState state, SubpatternParseResult result, PadPosition paddingLocation) {
+      if (state.peek() != '*') {
+        return;
+      }
+      result.paddingLocation = paddingLocation;
+      state.next(); // consume the '*'
+      consumeLiteral(state, result.padding);
+    }
+
+    private static void consumeAffix(
+        ParserState state, SubpatternParseResult result, StringBuilder destination) {
+      // literals := { literal }
+      while (true) {
+        switch (state.peek()) {
+          case '#':
+          case '@':
+          case ';':
+          case '*':
+          case '.':
+          case ',':
+          case '0':
+          case '1':
+          case '2':
+          case '3':
+          case '4':
+          case '5':
+          case '6':
+          case '7':
+          case '8':
+          case '9':
+          case -1:
+            // Characters that cannot appear unquoted in a literal
+            return;
+
+          case '%':
+            result.hasPercentSign = true;
+            break;
+
+          case '‰':
+            result.hasPerMilleSign = true;
+            break;
+
+          case '¤':
+            result.hasCurrencySign = true;
+            break;
+        }
+        consumeLiteral(state, destination);
+      }
+    }
+
+    private static void consumeLiteral(ParserState state, StringBuilder destination) {
+      if (state.peek() == -1) {
+        throw state.toParseException("expected unquoted literal but found end of string");
+      } else if (state.peek() == '\'') {
+        destination.appendCodePoint(state.next()); // consume the starting quote
+        while (state.peek() != '\'') {
+          if (state.peek() == -1) {
+            throw state.toParseException("expected quoted literal but found end of string");
+          } else {
+            destination.appendCodePoint(state.next()); // consume a quoted character
+          }
+        }
+        destination.appendCodePoint(state.next()); // consume the ending quote
+      } else {
+        // consume a non-quoted literal character
+        destination.appendCodePoint(state.next());
+      }
+    }
+
+    private static void consumeFormat(ParserState state, SubpatternParseResult result) {
+      consumeIntegerFormat(state, result);
+      if (state.peek() == '.') {
+        state.next(); // consume the decimal point
+        result.hasDecimal = true;
+        result.paddingWidth += 1;
+        consumeFractionFormat(state, result);
+      }
+    }
+
+    private static void consumeIntegerFormat(ParserState state, SubpatternParseResult result) {
+      boolean seenSignificantDigitMarker = false;
+      boolean seenDigit = false;
+
+      while (true) {
+        switch (state.peek()) {
+          case ',':
+            result.paddingWidth += 1;
+            result.groupingSizes[2] = result.groupingSizes[1];
+            result.groupingSizes[1] = result.groupingSizes[0];
+            result.groupingSizes[0] = 0;
+            break;
+
+          case '#':
+            if (seenDigit) throw state.toParseException("# cannot follow 0 before decimal point");
+            result.paddingWidth += 1;
+            result.groupingSizes[0] += 1;
+            result.totalIntegerDigits += (seenSignificantDigitMarker ? 0 : 1);
+            // no change to result.minimumIntegerDigits
+            // no change to result.minimumSignificantDigits
+            result.maximumSignificantDigits += (seenSignificantDigitMarker ? 1 : 0);
+            result.rounding.appendDigit((byte) 0, 0, true);
+            break;
+
+          case '@':
+            seenSignificantDigitMarker = true;
+            if (seenDigit) throw state.toParseException("Can't mix @ and 0 in pattern");
+            result.paddingWidth += 1;
+            result.groupingSizes[0] += 1;
+            result.totalIntegerDigits += 1;
+            // no change to result.minimumIntegerDigits
+            result.minimumSignificantDigits += 1;
+            result.maximumSignificantDigits += 1;
+            result.rounding.appendDigit((byte) 0, 0, true);
+            break;
+
+          case '0':
+          case '1':
+          case '2':
+          case '3':
+          case '4':
+          case '5':
+          case '6':
+          case '7':
+          case '8':
+          case '9':
+            seenDigit = true;
+            if (seenSignificantDigitMarker)
+              throw state.toParseException("Can't mix @ and 0 in pattern");
+            // TODO: Crash here if we've seen the significant digit marker? See NumberFormatTestCases.txt
+            result.paddingWidth += 1;
+            result.groupingSizes[0] += 1;
+            result.totalIntegerDigits += 1;
+            result.minimumIntegerDigits += 1;
+            // no change to result.minimumSignificantDigits
+            result.maximumSignificantDigits += (seenSignificantDigitMarker ? 1 : 0);
+            result.rounding.appendDigit((byte) (state.peek() - '0'), 0, true);
+            break;
+
+          default:
+            return;
+        }
+        state.next(); // consume the symbol
+      }
+    }
+
+    private static void consumeFractionFormat(ParserState state, SubpatternParseResult result) {
+      int zeroCounter = 0;
+      boolean seenHash = false;
+      while (true) {
+        switch (state.peek()) {
+          case '#':
+            seenHash = true;
+            result.paddingWidth += 1;
+            // no change to result.minimumFractionDigits
+            result.maximumFractionDigits += 1;
+            zeroCounter++;
+            break;
+
+          case '0':
+          case '1':
+          case '2':
+          case '3':
+          case '4':
+          case '5':
+          case '6':
+          case '7':
+          case '8':
+          case '9':
+            if (seenHash) throw state.toParseException("0 cannot follow # after decimal point");
+            result.paddingWidth += 1;
+            result.minimumFractionDigits += 1;
+            result.maximumFractionDigits += 1;
+            if (state.peek() == '0') {
+              zeroCounter++;
+            } else {
+              result.rounding.appendDigit((byte) (state.peek() - '0'), zeroCounter, false);
+              zeroCounter = 0;
+            }
+            break;
+
+          default:
+            return;
+        }
+        state.next(); // consume the symbol
+      }
+    }
+
+    private static void consumeExponent(ParserState state, SubpatternParseResult result) {
+      if (state.peek() != 'E') {
+        return;
+      }
+      state.next(); // consume the E
+      result.paddingWidth++;
+      if (state.peek() == '+') {
+        state.next(); // consume the +
+        result.exponentShowPlusSign = true;
+        result.paddingWidth++;
+      }
+      while (state.peek() == '0') {
+        state.next(); // consume the 0
+        result.exponentDigits += 1;
+        result.paddingWidth++;
+      }
+    }
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Properties.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Properties.java
new file mode 100644 (file)
index 0000000..9abc90c
--- /dev/null
@@ -0,0 +1,994 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+
+import com.ibm.icu.impl.number.Parse.ParseMode;
+import com.ibm.icu.impl.number.formatters.BigDecimalMultiplier;
+import com.ibm.icu.impl.number.formatters.CompactDecimalFormat;
+import com.ibm.icu.impl.number.formatters.CurrencyFormat;
+import com.ibm.icu.impl.number.formatters.CurrencyFormat.CurrencyStyle;
+import com.ibm.icu.impl.number.formatters.MagnitudeMultiplier;
+import com.ibm.icu.impl.number.formatters.MeasureFormat;
+import com.ibm.icu.impl.number.formatters.PaddingFormat;
+import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
+import com.ibm.icu.impl.number.formatters.PositiveDecimalFormat;
+import com.ibm.icu.impl.number.formatters.PositiveNegativeAffixFormat;
+import com.ibm.icu.impl.number.formatters.ScientificFormat;
+import com.ibm.icu.impl.number.rounders.IncrementRounder;
+import com.ibm.icu.impl.number.rounders.MagnitudeRounder;
+import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder;
+import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder.SignificantDigitsMode;
+import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
+import com.ibm.icu.text.CurrencyPluralInfo;
+import com.ibm.icu.text.MeasureFormat.FormatWidth;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyUsage;
+import com.ibm.icu.util.MeasureUnit;
+
+public class Properties
+    implements Cloneable,
+        Serializable,
+        PositiveDecimalFormat.IProperties,
+        PositiveNegativeAffixFormat.IProperties,
+        MagnitudeMultiplier.IProperties,
+        ScientificFormat.IProperties,
+        MeasureFormat.IProperties,
+        CompactDecimalFormat.IProperties,
+        PaddingFormat.IProperties,
+        BigDecimalMultiplier.IProperties,
+        CurrencyFormat.IProperties,
+        Parse.IProperties,
+        IncrementRounder.IProperties,
+        MagnitudeRounder.IProperties,
+        SignificantDigitsRounder.IProperties {
+
+  private static final Properties DEFAULT = new Properties();
+
+  /** Auto-generated. */
+  private static final long serialVersionUID = 4095518955889349243L;
+
+  // The setters in this class should NOT have any side-effects or perform any validation.  It is
+  // up to the consumer of the property bag to deal with property validation.
+
+  // The fields are all marked "transient" because custom serialization is being used.
+
+  /*--------------------------------------------------------------------------------------------+/
+  /| IMPORTANT!                                                                                 |/
+  /| WHEN ADDING A NEW PROPERTY, add it here, in #_clear(), in #_copyFrom(), in #equals(),      |/
+  /| and in #_hashCode().                                                                       |/
+  /|                                                                                            |/
+  /| The unit test PropertiesTest will catch if you forget to add it to #clear(), #copyFrom(),  |/
+  /| or #equals(), but it will NOT catch if you forget to add it to #hashCode().                |/
+  /+--------------------------------------------------------------------------------------------*/
+
+  private transient CompactStyle compactStyle;
+  private transient Currency currency;
+  private transient CurrencyPluralInfo currencyPluralInfo;
+  private transient CurrencyStyle currencyStyle;
+  private transient CurrencyUsage currencyUsage;
+  private transient boolean decimalPatternMatchRequired;
+  private transient boolean decimalSeparatorAlwaysShown;
+  private transient boolean exponentSignAlwaysShown;
+  private transient int formatWidth;
+  private transient int groupingSize;
+  private transient int magnitudeMultiplier;
+  private transient MathContext mathContext;
+  private transient int maximumFractionDigits;
+  private transient int maximumIntegerDigits;
+  private transient int maximumSignificantDigits;
+  private transient FormatWidth measureFormatWidth;
+  private transient MeasureUnit measureUnit;
+  private transient int minimumExponentDigits;
+  private transient int minimumFractionDigits;
+  private transient int minimumGroupingDigits;
+  private transient int minimumIntegerDigits;
+  private transient int minimumSignificantDigits;
+  private transient BigDecimal multiplier;
+  private transient String negativePrefix;
+  private transient String negativePrefixPattern;
+  private transient String negativeSuffix;
+  private transient String negativeSuffixPattern;
+  private transient PadPosition padPosition;
+  private transient String padString;
+  private transient boolean parseCaseSensitive;
+  private transient boolean parseNoExponent;
+  private transient boolean parseIntegerOnly;
+  private transient ParseMode parseMode;
+  private transient boolean parseToBigDecimal;
+  private transient boolean plusSignAlwaysShown;
+  private transient String positivePrefix;
+  private transient String positivePrefixPattern;
+  private transient String positiveSuffix;
+  private transient String positiveSuffixPattern;
+  private transient BigDecimal roundingIncrement;
+  private transient RoundingMode roundingMode;
+  private transient int secondaryGroupingSize;
+  private transient SignificantDigitsMode significantDigitsMode;
+
+  /*--------------------------------------------------------------------------------------------+/
+  /| IMPORTANT!                                                                                 |/
+  /| WHEN ADDING A NEW PROPERTY, add it here, in #_clear(), in #_copyFrom(), in #equals(),      |/
+  /| and in #_hashCode().                                                                       |/
+  /|                                                                                            |/
+  /| The unit test PropertiesTest will catch if you forget to add it to #clear(), #copyFrom(),  |/
+  /| or #equals(), but it will NOT catch if you forget to add it to #hashCode().                |/
+  /+--------------------------------------------------------------------------------------------*/
+
+  public Properties() {
+    clear();
+  }
+
+  private Properties _clear() {
+    compactStyle = DEFAULT_COMPACT_STYLE;
+    currency = DEFAULT_CURRENCY;
+    currencyPluralInfo = DEFAULT_CURRENCY_PLURAL_INFO;
+    currencyStyle = DEFAULT_CURRENCY_STYLE;
+    currencyUsage = DEFAULT_CURRENCY_USAGE;
+    decimalPatternMatchRequired = DEFAULT_DECIMAL_PATTERN_MATCH_REQUIRED;
+    decimalSeparatorAlwaysShown = DEFAULT_DECIMAL_SEPARATOR_ALWAYS_SHOWN;
+    exponentSignAlwaysShown = DEFAULT_EXPONENT_SIGN_ALWAYS_SHOWN;
+    formatWidth = DEFAULT_FORMAT_WIDTH;
+    groupingSize = DEFAULT_GROUPING_SIZE;
+    magnitudeMultiplier = DEFAULT_MAGNITUDE_MULTIPLIER;
+    mathContext = DEFAULT_MATH_CONTEXT;
+    maximumFractionDigits = DEFAULT_MAXIMUM_FRACTION_DIGITS;
+    maximumIntegerDigits = DEFAULT_MAXIMUM_INTEGER_DIGITS;
+    maximumSignificantDigits = DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS;
+    measureFormatWidth = DEFAULT_MEASURE_FORMAT_WIDTH;
+    measureUnit = DEFAULT_MEASURE_UNIT;
+    minimumExponentDigits = DEFAULT_MINIMUM_EXPONENT_DIGITS;
+    minimumFractionDigits = DEFAULT_MINIMUM_FRACTION_DIGITS;
+    minimumGroupingDigits = DEFAULT_MINIMUM_GROUPING_DIGITS;
+    minimumIntegerDigits = DEFAULT_MINIMUM_INTEGER_DIGITS;
+    minimumSignificantDigits = DEFAULT_MINIMUM_SIGNIFICANT_DIGITS;
+    multiplier = DEFAULT_MULTIPLIER;
+    negativePrefix = DEFAULT_NEGATIVE_PREFIX;
+    negativePrefixPattern = DEFAULT_NEGATIVE_PREFIX_PATTERN;
+    negativeSuffix = DEFAULT_NEGATIVE_SUFFIX;
+    negativeSuffixPattern = DEFAULT_NEGATIVE_SUFFIX_PATTERN;
+    padPosition = DEFAULT_PAD_POSITION;
+    padString = DEFAULT_PAD_STRING;
+    parseCaseSensitive = DEFAULT_PARSE_CASE_SENSITIVE;
+    parseIntegerOnly = DEFAULT_PARSE_INTEGER_ONLY;
+    parseMode = DEFAULT_PARSE_MODE;
+    parseNoExponent = DEFAULT_PARSE_NO_EXPONENT;
+    parseToBigDecimal = DEFAULT_PARSE_TO_BIG_DECIMAL;
+    plusSignAlwaysShown = DEFAULT_PLUS_SIGN_ALWAYS_SHOWN;
+    positivePrefix = DEFAULT_POSITIVE_PREFIX;
+    positivePrefixPattern = DEFAULT_POSITIVE_PREFIX_PATTERN;
+    positiveSuffix = DEFAULT_POSITIVE_SUFFIX;
+    positiveSuffixPattern = DEFAULT_POSITIVE_SUFFIX_PATTERN;
+    roundingIncrement = DEFAULT_ROUNDING_INCREMENT;
+    roundingMode = DEFAULT_ROUNDING_MODE;
+    secondaryGroupingSize = DEFAULT_SECONDARY_GROUPING_SIZE;
+    significantDigitsMode = DEFAULT_SIGNIFICANT_DIGITS_MODE;
+    return this;
+  }
+
+  private Properties _copyFrom(Properties other) {
+    compactStyle = other.compactStyle;
+    currency = other.currency;
+    currencyPluralInfo = other.currencyPluralInfo;
+    currencyStyle = other.currencyStyle;
+    currencyUsage = other.currencyUsage;
+    decimalPatternMatchRequired = other.decimalPatternMatchRequired;
+    decimalSeparatorAlwaysShown = other.decimalSeparatorAlwaysShown;
+    exponentSignAlwaysShown = other.exponentSignAlwaysShown;
+    formatWidth = other.formatWidth;
+    groupingSize = other.groupingSize;
+    magnitudeMultiplier = other.magnitudeMultiplier;
+    mathContext = other.mathContext;
+    maximumFractionDigits = other.maximumFractionDigits;
+    maximumIntegerDigits = other.maximumIntegerDigits;
+    maximumSignificantDigits = other.maximumSignificantDigits;
+    measureFormatWidth = other.measureFormatWidth;
+    measureUnit = other.measureUnit;
+    minimumExponentDigits = other.minimumExponentDigits;
+    minimumFractionDigits = other.minimumFractionDigits;
+    minimumGroupingDigits = other.minimumGroupingDigits;
+    minimumIntegerDigits = other.minimumIntegerDigits;
+    minimumSignificantDigits = other.minimumSignificantDigits;
+    multiplier = other.multiplier;
+    negativePrefix = other.negativePrefix;
+    negativePrefixPattern = other.negativePrefixPattern;
+    negativeSuffix = other.negativeSuffix;
+    negativeSuffixPattern = other.negativeSuffixPattern;
+    padPosition = other.padPosition;
+    padString = other.padString;
+    parseCaseSensitive = other.parseCaseSensitive;
+    parseIntegerOnly = other.parseIntegerOnly;
+    parseMode = other.parseMode;
+    parseNoExponent = other.parseNoExponent;
+    parseToBigDecimal = other.parseToBigDecimal;
+    plusSignAlwaysShown = other.plusSignAlwaysShown;
+    positivePrefix = other.positivePrefix;
+    positivePrefixPattern = other.positivePrefixPattern;
+    positiveSuffix = other.positiveSuffix;
+    positiveSuffixPattern = other.positiveSuffixPattern;
+    roundingIncrement = other.roundingIncrement;
+    roundingMode = other.roundingMode;
+    secondaryGroupingSize = other.secondaryGroupingSize;
+    significantDigitsMode = other.significantDigitsMode;
+    return this;
+  }
+
+  private boolean _equals(Properties other) {
+    boolean eq = true;
+    eq = eq && _equalsHelper(compactStyle, other.compactStyle);
+    eq = eq && _equalsHelper(currency, other.currency);
+    eq = eq && _equalsHelper(currencyPluralInfo, other.currencyPluralInfo);
+    eq = eq && _equalsHelper(currencyStyle, other.currencyStyle);
+    eq = eq && _equalsHelper(currencyUsage, other.currencyUsage);
+    eq = eq && _equalsHelper(decimalPatternMatchRequired, other.decimalPatternMatchRequired);
+    eq = eq && _equalsHelper(decimalSeparatorAlwaysShown, other.decimalSeparatorAlwaysShown);
+    eq = eq && _equalsHelper(exponentSignAlwaysShown, other.exponentSignAlwaysShown);
+    eq = eq && _equalsHelper(formatWidth, other.formatWidth);
+    eq = eq && _equalsHelper(groupingSize, other.groupingSize);
+    eq = eq && _equalsHelper(magnitudeMultiplier, other.magnitudeMultiplier);
+    eq = eq && _equalsHelper(mathContext, other.mathContext);
+    eq = eq && _equalsHelper(maximumFractionDigits, other.maximumFractionDigits);
+    eq = eq && _equalsHelper(maximumIntegerDigits, other.maximumIntegerDigits);
+    eq = eq && _equalsHelper(maximumSignificantDigits, other.maximumSignificantDigits);
+    eq = eq && _equalsHelper(measureFormatWidth, other.measureFormatWidth);
+    eq = eq && _equalsHelper(measureUnit, other.measureUnit);
+    eq = eq && _equalsHelper(minimumExponentDigits, other.minimumExponentDigits);
+    eq = eq && _equalsHelper(minimumFractionDigits, other.minimumFractionDigits);
+    eq = eq && _equalsHelper(minimumGroupingDigits, other.minimumGroupingDigits);
+    eq = eq && _equalsHelper(minimumIntegerDigits, other.minimumIntegerDigits);
+    eq = eq && _equalsHelper(minimumSignificantDigits, other.minimumSignificantDigits);
+    eq = eq && _equalsHelper(multiplier, other.multiplier);
+    eq = eq && _equalsHelper(negativePrefix, other.negativePrefix);
+    eq = eq && _equalsHelper(negativePrefixPattern, other.negativePrefixPattern);
+    eq = eq && _equalsHelper(negativeSuffix, other.negativeSuffix);
+    eq = eq && _equalsHelper(negativeSuffixPattern, other.negativeSuffixPattern);
+    eq = eq && _equalsHelper(padPosition, other.padPosition);
+    eq = eq && _equalsHelper(padString, other.padString);
+    eq = eq && _equalsHelper(parseCaseSensitive, other.parseCaseSensitive);
+    eq = eq && _equalsHelper(parseIntegerOnly, other.parseIntegerOnly);
+    eq = eq && _equalsHelper(parseMode, other.parseMode);
+    eq = eq && _equalsHelper(parseNoExponent, other.parseNoExponent);
+    eq = eq && _equalsHelper(parseToBigDecimal, other.parseToBigDecimal);
+    eq = eq && _equalsHelper(plusSignAlwaysShown, other.plusSignAlwaysShown);
+    eq = eq && _equalsHelper(positivePrefix, other.positivePrefix);
+    eq = eq && _equalsHelper(positivePrefixPattern, other.positivePrefixPattern);
+    eq = eq && _equalsHelper(positiveSuffix, other.positiveSuffix);
+    eq = eq && _equalsHelper(positiveSuffixPattern, other.positiveSuffixPattern);
+    eq = eq && _equalsHelper(roundingIncrement, other.roundingIncrement);
+    eq = eq && _equalsHelper(roundingMode, other.roundingMode);
+    eq = eq && _equalsHelper(secondaryGroupingSize, other.secondaryGroupingSize);
+    eq = eq && _equalsHelper(significantDigitsMode, other.significantDigitsMode);
+    return eq;
+  }
+
+  private boolean _equalsHelper(boolean mine, boolean theirs) {
+    return mine == theirs;
+  }
+
+  private boolean _equalsHelper(int mine, int theirs) {
+    return mine == theirs;
+  }
+
+  private boolean _equalsHelper(Object mine, Object theirs) {
+    if (mine == theirs) return true;
+    if (mine == null) return false;
+    return mine.equals(theirs);
+  }
+
+  private int _hashCode() {
+    int hashCode = 0;
+    hashCode ^= _hashCodeHelper(compactStyle);
+    hashCode ^= _hashCodeHelper(currency);
+    hashCode ^= _hashCodeHelper(currencyPluralInfo);
+    hashCode ^= _hashCodeHelper(currencyStyle);
+    hashCode ^= _hashCodeHelper(currencyUsage);
+    hashCode ^= _hashCodeHelper(decimalPatternMatchRequired);
+    hashCode ^= _hashCodeHelper(decimalSeparatorAlwaysShown);
+    hashCode ^= _hashCodeHelper(exponentSignAlwaysShown);
+    hashCode ^= _hashCodeHelper(formatWidth);
+    hashCode ^= _hashCodeHelper(groupingSize);
+    hashCode ^= _hashCodeHelper(magnitudeMultiplier);
+    hashCode ^= _hashCodeHelper(mathContext);
+    hashCode ^= _hashCodeHelper(maximumFractionDigits);
+    hashCode ^= _hashCodeHelper(maximumIntegerDigits);
+    hashCode ^= _hashCodeHelper(maximumSignificantDigits);
+    hashCode ^= _hashCodeHelper(measureFormatWidth);
+    hashCode ^= _hashCodeHelper(measureUnit);
+    hashCode ^= _hashCodeHelper(minimumExponentDigits);
+    hashCode ^= _hashCodeHelper(minimumFractionDigits);
+    hashCode ^= _hashCodeHelper(minimumGroupingDigits);
+    hashCode ^= _hashCodeHelper(minimumIntegerDigits);
+    hashCode ^= _hashCodeHelper(minimumSignificantDigits);
+    hashCode ^= _hashCodeHelper(multiplier);
+    hashCode ^= _hashCodeHelper(negativePrefix);
+    hashCode ^= _hashCodeHelper(negativePrefixPattern);
+    hashCode ^= _hashCodeHelper(negativeSuffix);
+    hashCode ^= _hashCodeHelper(negativeSuffixPattern);
+    hashCode ^= _hashCodeHelper(padPosition);
+    hashCode ^= _hashCodeHelper(padString);
+    hashCode ^= _hashCodeHelper(parseCaseSensitive);
+    hashCode ^= _hashCodeHelper(parseIntegerOnly);
+    hashCode ^= _hashCodeHelper(parseMode);
+    hashCode ^= _hashCodeHelper(parseNoExponent);
+    hashCode ^= _hashCodeHelper(parseToBigDecimal);
+    hashCode ^= _hashCodeHelper(plusSignAlwaysShown);
+    hashCode ^= _hashCodeHelper(positivePrefix);
+    hashCode ^= _hashCodeHelper(positivePrefixPattern);
+    hashCode ^= _hashCodeHelper(positiveSuffix);
+    hashCode ^= _hashCodeHelper(positiveSuffixPattern);
+    hashCode ^= _hashCodeHelper(roundingIncrement);
+    hashCode ^= _hashCodeHelper(roundingMode);
+    hashCode ^= _hashCodeHelper(secondaryGroupingSize);
+    hashCode ^= _hashCodeHelper(significantDigitsMode);
+    return hashCode;
+  }
+
+  private int _hashCodeHelper(boolean value) {
+    return value ? 1 : 0;
+  }
+
+  private int _hashCodeHelper(int value) {
+    return value * 13;
+  }
+
+  private int _hashCodeHelper(Object value) {
+    if (value == null) return 0;
+    return value.hashCode();
+  }
+
+  public Properties clear() {
+    return _clear();
+  }
+
+  /** Creates and returns a shallow copy of the property bag. */
+  @Override
+  public Properties clone() {
+    // super.clone() returns a shallow copy.
+    try {
+      return (Properties) super.clone();
+    } catch (CloneNotSupportedException e) {
+      // Should never happen since super is Object
+      throw new UnsupportedOperationException(e);
+    }
+  }
+
+  /**
+   * Shallow-copies the properties from the given property bag into this property bag.
+   *
+   * @param other The property bag from which to copy and which will not be modified.
+   * @return The current property bag (the one modified by this operation), for chaining.
+   */
+  public Properties copyFrom(Properties other) {
+    return _copyFrom(other);
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (other == null) return false;
+    if (this == other) return true;
+    if (!(other instanceof Properties)) return false;
+    return _equals((Properties) other);
+  }
+
+  @Override
+  public CompactStyle getCompactStyle() {
+    return compactStyle;
+  }
+
+  @Override
+  public Currency getCurrency() {
+    return currency;
+  }
+
+  /// BEGIN GETTERS/SETTERS ///
+
+  @Override
+  @Deprecated
+  public CurrencyPluralInfo getCurrencyPluralInfo() {
+    return currencyPluralInfo;
+  }
+
+  @Override
+  public CurrencyStyle getCurrencyStyle() {
+    return currencyStyle;
+  }
+
+  @Override
+  public CurrencyUsage getCurrencyUsage() {
+    return currencyUsage;
+  }
+
+  @Override
+  public boolean getDecimalPatternMatchRequired() {
+    return decimalPatternMatchRequired;
+  }
+
+  @Override
+  public boolean getDecimalSeparatorAlwaysShown() {
+    return decimalSeparatorAlwaysShown;
+  }
+
+  @Override
+  public boolean getExponentSignAlwaysShown() {
+    return exponentSignAlwaysShown;
+  }
+
+  @Override
+  public int getFormatWidth() {
+    return formatWidth;
+  }
+
+  @Override
+  public int getGroupingSize() {
+    return groupingSize;
+  }
+
+  @Override
+  public int getMagnitudeMultiplier() {
+    return magnitudeMultiplier;
+  }
+
+  @Override
+  public MathContext getMathContext() {
+    return mathContext;
+  }
+
+  @Override
+  public int getMaximumFractionDigits() {
+    return maximumFractionDigits;
+  }
+
+  @Override
+  public int getMaximumIntegerDigits() {
+    return maximumIntegerDigits;
+  }
+
+  @Override
+  public int getMaximumSignificantDigits() {
+    return maximumSignificantDigits;
+  }
+
+  @Override
+  public FormatWidth getMeasureFormatWidth() {
+    return measureFormatWidth;
+  }
+
+  @Override
+  public MeasureUnit getMeasureUnit() {
+    return measureUnit;
+  }
+
+  @Override
+  public int getMinimumExponentDigits() {
+    return minimumExponentDigits;
+  }
+
+  @Override
+  public int getMinimumFractionDigits() {
+    return minimumFractionDigits;
+  }
+
+  @Override
+  public int getMinimumGroupingDigits() {
+    return minimumGroupingDigits;
+  }
+
+  @Override
+  public int getMinimumIntegerDigits() {
+    return minimumIntegerDigits;
+  }
+
+  @Override
+  public int getMinimumSignificantDigits() {
+    return minimumSignificantDigits;
+  }
+
+  @Override
+  public BigDecimal getMultiplier() {
+    return multiplier;
+  }
+
+  @Override
+  public String getNegativePrefix() {
+    return negativePrefix;
+  }
+
+  @Override
+  public String getNegativePrefixPattern() {
+    return negativePrefixPattern;
+  }
+
+  @Override
+  public String getNegativeSuffix() {
+    return negativeSuffix;
+  }
+
+  @Override
+  public String getNegativeSuffixPattern() {
+    return negativeSuffixPattern;
+  }
+
+  @Override
+  public PadPosition getPadPosition() {
+    return padPosition;
+  }
+
+  @Override
+  public String getPadString() {
+    return padString;
+  }
+
+  @Override
+  public boolean getParseCaseSensitive() {
+    return parseCaseSensitive;
+  }
+
+  @Override
+  public boolean getParseIntegerOnly() {
+    return parseIntegerOnly;
+  }
+
+  @Override
+  public ParseMode getParseMode() {
+    return parseMode;
+  }
+
+  @Override
+  public boolean getParseNoExponent() {
+    return parseNoExponent;
+  }
+
+  @Override
+  public boolean getParseToBigDecimal() {
+    return parseToBigDecimal;
+  }
+
+  @Override
+  public boolean getPlusSignAlwaysShown() {
+    return plusSignAlwaysShown;
+  }
+
+  @Override
+  public String getPositivePrefix() {
+    return positivePrefix;
+  }
+
+  @Override
+  public String getPositivePrefixPattern() {
+    return positivePrefixPattern;
+  }
+
+  @Override
+  public String getPositiveSuffix() {
+    return positiveSuffix;
+  }
+
+  @Override
+  public String getPositiveSuffixPattern() {
+    return positiveSuffixPattern;
+  }
+
+  @Override
+  public BigDecimal getRoundingIncrement() {
+    return roundingIncrement;
+  }
+
+  @Override
+  public RoundingMode getRoundingMode() {
+    return roundingMode;
+  }
+
+  @Override
+  public int getSecondaryGroupingSize() {
+    return secondaryGroupingSize;
+  }
+
+  @Override
+  public SignificantDigitsMode getSignificantDigitsMode() {
+    return significantDigitsMode;
+  }
+
+  @Override
+  public int hashCode() {
+    return _hashCode();
+  }
+
+  /** Custom serialization: re-create object from serialized properties. */
+  private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
+    ois.defaultReadObject();
+
+    // Initialize to empty
+    clear();
+
+    // Extra int for possible future use
+    ois.readInt();
+
+    // 1) How many fields were serialized?
+    int count = ois.readInt();
+
+    // 2) Read each field by its name and value
+    for (int i=0; i<count; i++) {
+      String name = (String) ois.readObject();
+      Object value = ois.readObject();
+
+      // Get the field reference
+      Field field = null;
+      try {
+        field = Properties.class.getDeclaredField(name);
+      } catch (NoSuchFieldException e) {
+        // The field name does not exist! Possibly corrupted serialization. Ignore this entry.
+        continue;
+      } catch (SecurityException e) {
+        // Should not happen
+        throw new AssertionError(e);
+      }
+
+      // NOTE: If the type of a field were changed in the future, this would be the place to check:
+      // If the variable `value` is the old type, perform any conversions necessary.
+
+      // Save value into the field
+      try {
+        field.set(this, value);
+      } catch (IllegalArgumentException e) {
+        // Should not happen
+        throw new AssertionError(e);
+      } catch (IllegalAccessException e) {
+        // Should not happen
+        throw new AssertionError(e);
+      }
+    }
+  }
+
+  @Override
+  public Properties setCompactStyle(CompactStyle compactStyle) {
+    this.compactStyle = compactStyle;
+    return this;
+  }
+
+  @Override
+  public Properties setCurrency(Currency currency) {
+    this.currency = currency;
+    return this;
+  }
+
+  @Override
+  @Deprecated
+  public Properties setCurrencyPluralInfo(CurrencyPluralInfo currencyPluralInfo) {
+    // TODO: In order to maintain immutability, we have to perform a clone here.
+    // It would be better to just retire CurrencyPluralInfo entirely.
+    if (currencyPluralInfo != null) {
+      currencyPluralInfo = (CurrencyPluralInfo) currencyPluralInfo.clone();
+    }
+    this.currencyPluralInfo = currencyPluralInfo;
+    return this;
+  }
+
+  @Override
+  public Properties setCurrencyStyle(CurrencyStyle currencyStyle) {
+    this.currencyStyle = currencyStyle;
+    return this;
+  }
+
+  @Override
+  public Properties setCurrencyUsage(CurrencyUsage currencyUsage) {
+    this.currencyUsage = currencyUsage;
+    return this;
+  }
+
+  @Override
+  public Properties setDecimalPatternMatchRequired(boolean decimalPatternMatchRequired) {
+    this.decimalPatternMatchRequired = decimalPatternMatchRequired;
+    return this;
+  }
+
+  @Override
+  public Properties setDecimalSeparatorAlwaysShown(boolean alwaysShowDecimal) {
+    this.decimalSeparatorAlwaysShown = alwaysShowDecimal;
+    return this;
+  }
+
+  @Override
+  public Properties setExponentSignAlwaysShown(boolean exponentSignAlwaysShown) {
+    this.exponentSignAlwaysShown = exponentSignAlwaysShown;
+    return this;
+  }
+
+  @Override
+  public Properties setFormatWidth(int paddingWidth) {
+    this.formatWidth = paddingWidth;
+    return this;
+  }
+
+  @Override
+  public Properties setGroupingSize(int groupingSize) {
+    this.groupingSize = groupingSize;
+    return this;
+  }
+
+  @Override
+  public Properties setMagnitudeMultiplier(int magnitudeMultiplier) {
+    this.magnitudeMultiplier = magnitudeMultiplier;
+    return this;
+  }
+
+  @Override
+  public Properties setMathContext(MathContext mathContext) {
+    this.mathContext = mathContext;
+    return this;
+  }
+
+  @Override
+  public Properties setMaximumFractionDigits(int maximumFractionDigits) {
+    this.maximumFractionDigits = maximumFractionDigits;
+    return this;
+  }
+
+  @Override
+  public Properties setMaximumIntegerDigits(int maximumIntegerDigits) {
+    this.maximumIntegerDigits = maximumIntegerDigits;
+    return this;
+  }
+
+  @Override
+  public Properties setMaximumSignificantDigits(int maximumSignificantDigits) {
+    this.maximumSignificantDigits = maximumSignificantDigits;
+    return this;
+  }
+
+  @Override
+  public Properties setMeasureFormatWidth(FormatWidth measureFormatWidth) {
+    this.measureFormatWidth = measureFormatWidth;
+    return this;
+  }
+
+  @Override
+  public Properties setMeasureUnit(MeasureUnit measureUnit) {
+    this.measureUnit = measureUnit;
+    return this;
+  }
+
+  @Override
+  public Properties setMinimumExponentDigits(int exponentDigits) {
+    this.minimumExponentDigits = exponentDigits;
+    return this;
+  }
+
+  @Override
+  public Properties setMinimumFractionDigits(int minimumFractionDigits) {
+    this.minimumFractionDigits = minimumFractionDigits;
+    return this;
+  }
+
+  @Override
+  public Properties setMinimumGroupingDigits(int minimumGroupingDigits) {
+    this.minimumGroupingDigits = minimumGroupingDigits;
+    return this;
+  }
+
+  @Override
+  public Properties setMinimumIntegerDigits(int minimumIntegerDigits) {
+    this.minimumIntegerDigits = minimumIntegerDigits;
+    return this;
+  }
+
+  @Override
+  public Properties setMinimumSignificantDigits(int minimumSignificantDigits) {
+    this.minimumSignificantDigits = minimumSignificantDigits;
+    return this;
+  }
+
+  @Override
+  public Properties setMultiplier(BigDecimal multiplier) {
+    this.multiplier = multiplier;
+    return this;
+  }
+
+  @Override
+  public Properties setNegativePrefix(String negativePrefix) {
+    this.negativePrefix = negativePrefix;
+    return this;
+  }
+
+  @Override
+  public Properties setNegativePrefixPattern(String negativePrefixPattern) {
+    this.negativePrefixPattern = negativePrefixPattern;
+    return this;
+  }
+
+  @Override
+  public Properties setNegativeSuffix(String negativeSuffix) {
+    this.negativeSuffix = negativeSuffix;
+    return this;
+  }
+
+  @Override
+  public Properties setNegativeSuffixPattern(String negativeSuffixPattern) {
+    this.negativeSuffixPattern = negativeSuffixPattern;
+    return this;
+  }
+
+  @Override
+  public Properties setPadPosition(PadPosition paddingLocation) {
+    this.padPosition = paddingLocation;
+    return this;
+  }
+
+  @Override
+  public Properties setPadString(String paddingString) {
+    this.padString = paddingString;
+    return this;
+  }
+
+  @Override
+  public Properties setParseCaseSensitive(boolean parseCaseSensitive) {
+    this.parseCaseSensitive = parseCaseSensitive;
+    return this;
+  }
+
+  @Override
+  public Properties setParseIntegerOnly(boolean parseIntegerOnly) {
+    this.parseIntegerOnly = parseIntegerOnly;
+    return this;
+  }
+
+  @Override
+  public Properties setParseMode(ParseMode parseMode) {
+    this.parseMode = parseMode;
+    return this;
+  }
+
+  @Override
+  public Properties setParseNoExponent(boolean parseNoExponent) {
+    this.parseNoExponent = parseNoExponent;
+    return this;
+  }
+
+  @Override
+  public Properties setParseToBigDecimal(boolean parseToBigDecimal) {
+    this.parseToBigDecimal = parseToBigDecimal;
+    return this;
+  }
+
+  @Override
+  public Properties setPlusSignAlwaysShown(boolean plusSignAlwaysShown) {
+    this.plusSignAlwaysShown = plusSignAlwaysShown;
+    return this;
+  }
+
+  @Override
+  public Properties setPositivePrefix(String positivePrefix) {
+    this.positivePrefix = positivePrefix;
+    return this;
+  }
+
+  @Override
+  public Properties setPositivePrefixPattern(String positivePrefixPattern) {
+    this.positivePrefixPattern = positivePrefixPattern;
+    return this;
+  }
+
+  @Override
+  public Properties setPositiveSuffix(String positiveSuffix) {
+    this.positiveSuffix = positiveSuffix;
+    return this;
+  }
+
+  @Override
+  public Properties setPositiveSuffixPattern(String positiveSuffixPattern) {
+    this.positiveSuffixPattern = positiveSuffixPattern;
+    return this;
+  }
+
+  @Override
+  public Properties setRoundingIncrement(BigDecimal roundingIncrement) {
+    this.roundingIncrement = roundingIncrement;
+    return this;
+  }
+
+  @Override
+  public Properties setRoundingMode(RoundingMode roundingMode) {
+    this.roundingMode = roundingMode;
+    return this;
+  }
+
+  @Override
+  public Properties setSecondaryGroupingSize(int secondaryGroupingSize) {
+    this.secondaryGroupingSize = secondaryGroupingSize;
+    return this;
+  }
+
+  @Override
+  public Properties setSignificantDigitsMode(SignificantDigitsMode significantDigitsMode) {
+    this.significantDigitsMode = significantDigitsMode;
+    return this;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder result = new StringBuilder();
+    result.append("<Properties");
+    Field[] fields = Properties.class.getDeclaredFields();
+    for (Field field : fields) {
+      Object myValue, defaultValue;
+      try {
+        myValue = field.get(this);
+        defaultValue = field.get(DEFAULT);
+      } catch (IllegalArgumentException e) {
+        e.printStackTrace();
+        continue;
+      } catch (IllegalAccessException e) {
+        e.printStackTrace();
+        continue;
+      }
+      if (myValue == null && defaultValue == null) {
+        continue;
+      } else if (myValue == null || defaultValue == null) {
+        result.append(" " + field.getName() + ":" + myValue);
+      } else if (!myValue.equals(defaultValue)) {
+        result.append(" " + field.getName() + ":" + myValue);
+      }
+    }
+    result.append(">");
+    return result.toString();
+  }
+
+  /**
+   * Custom serialization: save fields along with their name, so that fields can be easily added in
+   * the future in any order. Only save fields that differ from their default value.
+   */
+  private void writeObject(ObjectOutputStream oos) throws IOException {
+    oos.defaultWriteObject();
+
+    // Extra int for possible future use
+    oos.writeInt(0);
+
+    ArrayList<Field> fieldsToSerialize = new ArrayList<Field>();
+    ArrayList<Object> valuesToSerialize = new ArrayList<Object>();
+    Field[] fields = Properties.class.getDeclaredFields();
+    for (Field field : fields) {
+      if (Modifier.isStatic(field.getModifiers())) {
+        continue;
+      }
+      try {
+        Object myValue = field.get(this);
+        if (myValue == null) {
+          // All *Object* values default to null; no need to serialize.
+          continue;
+        }
+        Object defaultValue = field.get(DEFAULT);
+        if (!myValue.equals(defaultValue)) {
+          fieldsToSerialize.add(field);
+          valuesToSerialize.add(myValue);
+        }
+      } catch (IllegalArgumentException e) {
+        // Should not happen
+        throw new AssertionError(e);
+      } catch (IllegalAccessException e) {
+        // Should not happen
+        throw new AssertionError(e);
+      }
+    }
+
+    // 1) How many fields are to be serialized?
+    int count = fieldsToSerialize.size();
+    oos.writeInt(count);
+
+    // 2) Write each field with its name and value
+    for (int i = 0; i < count; i++) {
+      Field field = fieldsToSerialize.get(i);
+      Object value = valuesToSerialize.get(i);
+      oos.writeObject(field.getName());
+      oos.writeObject(value);
+    }
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Rounder.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/Rounder.java
new file mode 100644 (file)
index 0000000..1e090cc
--- /dev/null
@@ -0,0 +1,265 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.MathContext;
+import java.math.RoundingMode;
+
+import com.ibm.icu.impl.number.formatters.CompactDecimalFormat;
+import com.ibm.icu.impl.number.formatters.ScientificFormat;
+
+/**
+ * The base class for a Rounder used by ICU Decimal Format.
+ *
+ * <p>A Rounder must implement the method {@link #apply}. An implementation must:
+ *
+ * <ol>
+ *   <li>Either have the code <code>applyDefaults(input);</code> in its apply function, or otherwise
+ *       ensure that minFrac, maxFrac, minInt, and maxInt are obeyed, paying special attention to
+ *       the case when the input is zero.
+ *   <li>Call one of {@link FormatQuantity#roundToInterval}, {@link
+ *       FormatQuantity#roundToMagnitude}, or {@link FormatQuantity#roundToInfinity} on the input.
+ * </ol>
+ *
+ * <p>In order to be used by {@link CompactDecimalFormat} and {@link ScientificFormat}, among
+ * others, your rounder must be stable upon <em>decreasing</em> the magnitude of the input number.
+ * For example, if your rounder converts "999" to "1000", it must also convert "99.9" to "100" and
+ * "0.999" to "1". (The opposite does not need to be the case: you can round "0.999" to "1" but keep
+ * "999" as "999".)
+ *
+ * @see com.ibm.icu.impl.number.rounders.MagnitudeRounder
+ * @see com.ibm.icu.impl.number.rounders.IncrementRounder
+ * @see com.ibm.icu.impl.number.rounders.SignificantDigitsRounder
+ * @see com.ibm.icu.impl.number.rounders.NoRounder
+ */
+public abstract class Rounder extends Format.BeforeFormat {
+
+  public static interface IBasicRoundingProperties {
+
+    static int DEFAULT_MINIMUM_INTEGER_DIGITS = -1;
+
+    /** @see #setMinimumIntegerDigits */
+    public int getMinimumIntegerDigits();
+
+    /**
+     * Sets the minimum number of digits to display before the decimal point. If the number has
+     * fewer than this number of digits, the number will be padded with zeros. The pattern "#00.0#",
+     * for example, corresponds to 2 minimum integer digits, and the number 5.3 would be formatted
+     * as "05.3" in locale <em>en-US</em>.
+     *
+     * @param minimumIntegerDigits The minimum number of integer digits to output.
+     * @return The property bag, for chaining.
+     */
+    public IBasicRoundingProperties setMinimumIntegerDigits(int minimumIntegerDigits);
+
+    static int DEFAULT_MAXIMUM_INTEGER_DIGITS = -1;
+
+    /** @see #setMaximumIntegerDigits */
+    public int getMaximumIntegerDigits();
+
+    /**
+     * Sets the maximum number of digits to display before the decimal point. If the number has more
+     * than this number of digits, the extra digits will be truncated. For example, if maximum
+     * integer digits is 2, and you attempt to format the number 1970, you will get "70" in locale
+     * <em>en-US</em>. It is not possible to specify the maximum integer digits using a pattern
+     * string, except in the special case of a scientific format pattern.
+     *
+     * @param maximumIntegerDigits The maximum number of integer digits to output.
+     * @return The property bag, for chaining.
+     */
+    public IBasicRoundingProperties setMaximumIntegerDigits(int maximumIntegerDigits);
+
+    static int DEFAULT_MINIMUM_FRACTION_DIGITS = -1;
+
+    /** @see #setMinimumFractionDigits */
+    public int getMinimumFractionDigits();
+
+    /**
+     * Sets the minimum number of digits to display after the decimal point. If the number has fewer
+     * than this number of digits, the number will be padded with zeros. The pattern "#00.0#", for
+     * example, corresponds to 1 minimum fraction digit, and the number 456 would be formatted as
+     * "456.0" in locale <em>en-US</em>.
+     *
+     * @param minimumFractionDigits The minimum number of fraction digits to output.
+     * @return The property bag, for chaining.
+     */
+    public IBasicRoundingProperties setMinimumFractionDigits(int minimumFractionDigits);
+
+    static int DEFAULT_MAXIMUM_FRACTION_DIGITS = -1;
+
+    /** @see #setMaximumFractionDigits */
+    public int getMaximumFractionDigits();
+
+    /**
+     * Sets the maximum number of digits to display after the decimal point. If the number has fewer
+     * than this number of digits, the number will be rounded off using the rounding mode specified
+     * by {@link #setRoundingMode(RoundingMode)}. The pattern "#00.0#", for example, corresponds to
+     * 2 maximum fraction digits, and the number 456.789 would be formatted as "456.79" in locale
+     * <em>en-US</em> with the default rounding mode. Note that the number 456.999 would be
+     * formatted as "457.0" given the same configurations.
+     *
+     * @param maximumFractionDigits The maximum number of fraction digits to output.
+     * @return The property bag, for chaining.
+     */
+    public IBasicRoundingProperties setMaximumFractionDigits(int maximumFractionDigits);
+
+    static RoundingMode DEFAULT_ROUNDING_MODE = null;
+
+    /** @see #setRoundingMode */
+    public RoundingMode getRoundingMode();
+
+    /**
+     * Sets the rounding mode, which determines under which conditions extra decimal places are
+     * rounded either up or down. See {@link RoundingMode} for details on the choices of rounding
+     * mode. The default if not set explicitly is {@link RoundingMode#HALF_EVEN}.
+     *
+     * <p>This setting is ignored if {@link #setMathContext} is used.
+     *
+     * @param roundingMode The rounding mode to use when rounding is required.
+     * @return The property bag, for chaining.
+     * @see RoundingMode
+     * @see #setMathContext
+     */
+    public IBasicRoundingProperties setRoundingMode(RoundingMode roundingMode);
+
+    static MathContext DEFAULT_MATH_CONTEXT = null;
+
+    /** @see #setMathContext */
+    public MathContext getMathContext();
+
+    /**
+     * Sets the {@link MathContext} to be used during math and rounding operations. A MathContext
+     * encapsulates a RoundingMode and the number of significant digits in the output.
+     *
+     * @param mathContext The math context to use when rounding is required.
+     * @return The property bag, for chaining.
+     * @see MathContext
+     * @see #setRoundingMode
+     */
+    public IBasicRoundingProperties setMathContext(MathContext mathContext);
+  }
+
+  public static interface MultiplierGenerator {
+    public int getMultiplier(int magnitude);
+  }
+
+  // Properties available to all rounding strategies
+  protected final MathContext mathContext;
+  protected final int minInt;
+  protected final int maxInt;
+  protected final int minFrac;
+  protected final int maxFrac;
+
+  /**
+   * Constructor that uses integer and fraction digit lengths from IBasicRoundingProperties.
+   *
+   * @param properties
+   */
+  protected Rounder(IBasicRoundingProperties properties) {
+    mathContext = RoundingUtils.getMathContextOrUnlimited(properties);
+
+    int _maxInt = properties.getMaximumIntegerDigits();
+    int _minInt = properties.getMinimumIntegerDigits();
+    int _maxFrac = properties.getMaximumFractionDigits();
+    int _minFrac = properties.getMinimumFractionDigits();
+
+    // Validate min/max int/frac.
+    // For backwards compatibility, minimum overrides maximum if the two conflict.
+    // The following logic ensures that there is always a minimum of at least one digit.
+    if (_minInt == 0 && _maxFrac != 0) {
+      // Force a digit to the right of the decimal point.
+      minFrac = _minFrac <= 0 ? 1 : _minFrac;
+      maxFrac = _maxFrac < 0 ? Integer.MAX_VALUE : _maxFrac < minFrac ? minFrac : _maxFrac;
+      minInt = 0;
+      maxInt = _maxInt < 0 ? Integer.MAX_VALUE : _maxInt;
+    } else {
+      // Force a digit to the left of the decimal point.
+      minFrac = _minFrac < 0 ? 0 : _minFrac;
+      maxFrac = _maxFrac < 0 ? Integer.MAX_VALUE : _maxFrac < minFrac ? minFrac : _maxFrac;
+      minInt = _minInt <= 0 ? 1 : _minInt;
+      maxInt = _maxInt < 0 ? Integer.MAX_VALUE : _maxInt < minInt ? minInt : _maxInt;
+    }
+  }
+
+  /**
+   * Perform rounding and specification of integer and fraction digit lengths on the input quantity.
+   * Calling this method will change the state of the FormatQuantity.
+   *
+   * @param input The {@link FormatQuantity} to be modified and rounded.
+   */
+  public abstract void apply(FormatQuantity input);
+
+  /**
+   * Rounding can affect the magnitude. First we attempt to adjust according to the original
+   * magnitude, and if the magnitude changes, we adjust according to a magnitude one greater. Note
+   * that this algorithm assumes that increasing the multiplier never increases the number of digits
+   * that can be displayed.
+   *
+   * @param input The quantity to be rounded.
+   * @param mg The implementation that returns magnitude adjustment based on a given starting
+   *     magnitude.
+   * @return The multiplier that was chosen to best fit the input.
+   */
+  public int chooseMultiplierAndApply(FormatQuantity input, MultiplierGenerator mg) {
+    FormatQuantity copy = input.clone();
+
+    int magnitude = input.getMagnitude();
+    int multiplier = mg.getMultiplier(magnitude);
+    input.adjustMagnitude(multiplier);
+    apply(input);
+    if (input.getMagnitude() == magnitude + multiplier + 1) {
+      magnitude += 1;
+      input.copyFrom(copy);
+      multiplier = mg.getMultiplier(magnitude);
+      input.adjustMagnitude(multiplier);
+      assert input.getMagnitude() == magnitude + multiplier - 1;
+      apply(input);
+      assert input.getMagnitude() == magnitude + multiplier;
+    }
+
+    return multiplier;
+  }
+
+  /**
+   * Implementations can call this method to perform default logic for min/max digits. This method
+   * performs logic for handling of a zero input.
+   *
+   * @param input The digits being formatted.
+   */
+  protected void applyDefaults(FormatQuantity input) {
+    input.setIntegerFractionLength(minInt, maxInt, minFrac, maxFrac);
+  }
+
+  private static final ThreadLocal<Properties> threadLocalProperties =
+      new ThreadLocal<Properties>() {
+        @Override
+        protected Properties initialValue() {
+          return new Properties();
+        }
+      };
+
+  /**
+   * Gets a thread-local property bag that can be used to deliver properties to a constructor.
+   * Rounders themselves are guaranteed to not internally use a copy of this property bag.
+   *
+   * @return A clean, thread-local property bag.
+   */
+  public static Properties getThreadLocalProperties() {
+    return threadLocalProperties.get().clear();
+  }
+
+  @Override
+  public void before(FormatQuantity input, ModifierHolder mods) {
+    apply(input);
+  }
+
+  @Override
+  public void export(Properties properties) {
+    properties.setMathContext(mathContext);
+    properties.setRoundingMode(mathContext.getRoundingMode());
+    properties.setMinimumFractionDigits(minFrac);
+    properties.setMinimumIntegerDigits(minInt);
+    properties.setMaximumFractionDigits(maxFrac);
+    properties.setMaximumIntegerDigits(maxInt);
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/RoundingUtils.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/RoundingUtils.java
new file mode 100644 (file)
index 0000000..3994eb2
--- /dev/null
@@ -0,0 +1,165 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+
+import com.ibm.icu.impl.number.Rounder.IBasicRoundingProperties;
+
+/** @author sffc */
+public class RoundingUtils {
+
+  public static final int SECTION_LOWER = 1;
+  public static final int SECTION_MIDPOINT = 2;
+  public static final int SECTION_UPPER = 3;
+
+  /**
+   * Converts a rounding mode and metadata about the quantity being rounded to a boolean determining
+   * whether the value should be rounded toward infinity or toward zero.
+   *
+   * <p>The parameters are of type int because benchmarks on an x86-64 processor against OpenJDK
+   * showed that ints were demonstrably faster than enums in switch statements.
+   *
+   * @param isEven Whether the digit immediately before the rounding magnitude is even.
+   * @param isNegative Whether the quantity is negative.
+   * @param section Whether the part of the quantity to the right of the rounding magnitude is
+   *     exactly halfway between two digits, whether it is in the lower part (closer to zero), or
+   *     whether it is in the upper part (closer to infinity). See {@link #SECTION_LOWER}, {@link
+   *     #SECTION_MIDPOINT}, and {@link #SECTION_UPPER}.
+   * @param roundingMode The integer version of the {@link RoundingMode}, which you can get via
+   *     {@link RoundingMode#ordinal}.
+   * @param reference A reference object to be used when throwing an ArithmeticException.
+   * @return true if the number should be rounded toward zero; false if it should be rounded toward
+   *     infinity.
+   */
+  public static boolean getRoundingDirection(
+      boolean isEven, boolean isNegative, int section, int roundingMode, Object reference) {
+    switch (roundingMode) {
+      case BigDecimal.ROUND_UP:
+        // round away from zero
+        return false;
+
+      case BigDecimal.ROUND_DOWN:
+        // round toward zero
+        return true;
+
+      case BigDecimal.ROUND_CEILING:
+        // round toward positive infinity
+        return isNegative;
+
+      case BigDecimal.ROUND_FLOOR:
+        // round toward negative infinity
+        return !isNegative;
+
+      case BigDecimal.ROUND_HALF_UP:
+        switch (section) {
+          case SECTION_MIDPOINT:
+            return false;
+          case SECTION_LOWER:
+            return true;
+          case SECTION_UPPER:
+            return false;
+        }
+        break;
+
+      case BigDecimal.ROUND_HALF_DOWN:
+        switch (section) {
+          case SECTION_MIDPOINT:
+            return true;
+          case SECTION_LOWER:
+            return true;
+          case SECTION_UPPER:
+            return false;
+        }
+        break;
+
+      case BigDecimal.ROUND_HALF_EVEN:
+        switch (section) {
+          case SECTION_MIDPOINT:
+            return isEven;
+          case SECTION_LOWER:
+            return true;
+          case SECTION_UPPER:
+            return false;
+        }
+        break;
+    }
+
+    // Rounding mode UNNECESSARY
+    throw new ArithmeticException("Rounding is required on " + reference.toString());
+  }
+
+  /**
+   * Gets whether the given rounding mode's rounding boundary is at the midpoint. The rounding
+   * boundary is the point at which a number switches from being rounded down to being rounded up.
+   * For example, with rounding mode HALF_EVEN, HALF_UP, or HALF_DOWN, the rounding boundary is at
+   * the midpoint, and this function would return true. However, for UP, DOWN, CEILING, and FLOOR,
+   * the rounding boundary is at the "edge", and this function would return false.
+   *
+   * @param roundingMode The integer version of the {@link RoundingMode}.
+   * @return true if rounding mode is HALF_EVEN, HALF_UP, or HALF_DOWN; false otherwise.
+   */
+  public static boolean roundsAtMidpoint(int roundingMode) {
+    switch (roundingMode) {
+      case BigDecimal.ROUND_UP:
+      case BigDecimal.ROUND_DOWN:
+      case BigDecimal.ROUND_CEILING:
+      case BigDecimal.ROUND_FLOOR:
+        return false;
+
+      default:
+        return true;
+    }
+  }
+
+  private static final MathContext[] MATH_CONTEXT_BY_ROUNDING_MODE_UNLIMITED =
+      new MathContext[RoundingMode.values().length];
+
+  private static final MathContext[] MATH_CONTEXT_BY_ROUNDING_MODE_16_DIGITS =
+      new MathContext[RoundingMode.values().length];
+
+  static {
+    for (int i = 0; i < MATH_CONTEXT_BY_ROUNDING_MODE_16_DIGITS.length; i++) {
+      MATH_CONTEXT_BY_ROUNDING_MODE_UNLIMITED[i] = new MathContext(0, RoundingMode.valueOf(i));
+      MATH_CONTEXT_BY_ROUNDING_MODE_16_DIGITS[i] = new MathContext(16);
+    }
+  }
+
+  /**
+   * Gets the user-specified math context out of the property bag. If there is none, falls back to a
+   * math context with unlimited precision and the user-specified rounding mode, which defaults to
+   * HALF_EVEN (the IEEE 754R default).
+   *
+   * @param properties The property bag.
+   * @return A {@link MathContext}. Never null.
+   */
+  public static MathContext getMathContextOrUnlimited(IBasicRoundingProperties properties) {
+    MathContext mathContext = properties.getMathContext();
+    if (mathContext == null) {
+      RoundingMode roundingMode = properties.getRoundingMode();
+      if (roundingMode == null) roundingMode = RoundingMode.HALF_EVEN;
+      mathContext = MATH_CONTEXT_BY_ROUNDING_MODE_UNLIMITED[roundingMode.ordinal()];
+    }
+    return mathContext;
+  }
+
+  /**
+   * Gets the user-specified math context out of the property bag. If there is none, falls back to a
+   * math context with 16 digits of precision (the 64-bit IEEE 754R default) and the user-specified
+   * rounding mode, which defaults to HALF_EVEN (the IEEE 754R default).
+   *
+   * @param properties The property bag.
+   * @return A {@link MathContext}. Never null.
+   */
+  public static MathContext getMathContextOr16Digits(IBasicRoundingProperties properties) {
+    MathContext mathContext = properties.getMathContext();
+    if (mathContext == null) {
+      RoundingMode roundingMode = properties.getRoundingMode();
+      if (roundingMode == null) roundingMode = RoundingMode.HALF_EVEN;
+      mathContext = MATH_CONTEXT_BY_ROUNDING_MODE_16_DIGITS[roundingMode.ordinal()];
+    }
+    return mathContext;
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/demo.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/demo.java
new file mode 100644 (file)
index 0000000..8a91de7
--- /dev/null
@@ -0,0 +1,123 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number;
+
+import java.math.BigDecimal;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
+import com.ibm.icu.impl.number.formatters.RangeFormat;
+import com.ibm.icu.impl.number.modifiers.SimpleModifier;
+import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.util.MeasureUnit;
+
+public class demo {
+
+  public static void main(String[] args) throws ParseException {
+    SimpleModifier.testFormatAsPrefixSuffix();
+
+    System.out.println(new FormatQuantity1(3.14159));
+    System.out.println(new FormatQuantity1(3.14159, true));
+    System.out.println(new FormatQuantity2(3.14159));
+
+    System.out.println(
+        PatternString.propertiesToString(PatternString.parseToProperties("+**##,##,#00.05#%")));
+
+    ParsePosition ppos = new ParsePosition(0);
+    System.out.println(
+        Parse.parse(
+            "dd123",
+            ppos,
+            new Properties().setPositivePrefix("dd").setNegativePrefix("ddd"),
+            DecimalFormatSymbols.getInstance()));
+    System.out.println(ppos);
+
+    List<Format> formats = new ArrayList<Format>();
+
+    Properties properties = new Properties();
+    Format ndf = Endpoint.fromBTA(properties);
+    formats.add(ndf);
+
+    properties =
+        new Properties()
+            .setMinimumSignificantDigits(3)
+            .setMaximumSignificantDigits(3)
+            .setCompactStyle(CompactStyle.LONG);
+    Format cdf = Endpoint.fromBTA(properties);
+    formats.add(cdf);
+
+    properties =
+        new Properties().setFormatWidth(10).setPadPosition(PadPosition.AFTER_PREFIX);
+    Format pdf = Endpoint.fromBTA(properties);
+    formats.add(pdf);
+
+    properties =
+        new Properties()
+            .setMinimumExponentDigits(1)
+            .setMaximumIntegerDigits(3)
+            .setMaximumFractionDigits(1);
+    Format exf = Endpoint.fromBTA(properties);
+    formats.add(exf);
+
+    properties = new Properties().setRoundingIncrement(new BigDecimal("0.5"));
+    Format rif = Endpoint.fromBTA(properties);
+    formats.add(rif);
+
+    properties = new Properties().setMeasureUnit(MeasureUnit.HECTARE);
+    Format muf = Endpoint.fromBTA(properties);
+    formats.add(muf);
+
+    properties =
+        new Properties().setMeasureUnit(MeasureUnit.HECTARE).setCompactStyle(CompactStyle.LONG);
+    Format cmf = Endpoint.fromBTA(properties);
+    formats.add(cmf);
+
+    properties = PatternString.parseToProperties("#,##0.00 \u00a4");
+    Format ptf = Endpoint.fromBTA(properties);
+    formats.add(ptf);
+
+    RangeFormat rf = new RangeFormat(cdf, cdf, " to ");
+    System.out.println(rf.format(new FormatQuantity2(1234), new FormatQuantity2(2345)));
+
+    String[] cases = {
+      "1.0",
+      "2.01",
+      "1234.56",
+      "3000.0",
+      //      "512.0000000000017",
+      //      "4096.000000000001",
+      //      "4096.000000000004",
+      //      "4096.000000000005",
+      //      "4096.000000000006",
+      //      "4096.000000000007",
+      "0.00026418",
+      "0.01789261",
+      "468160.0",
+      "999000.0",
+      "999900.0",
+      "999990.0",
+      "0.0",
+      "12345678901.0",
+      //      "789000000000000000000000.0",
+      //      "789123123567853156372158.0",
+      "-5193.48",
+    };
+
+    for (String str : cases) {
+      System.out.println("----------");
+      System.out.println(str);
+      System.out.println("  NDF: " + ndf.format(new FormatQuantity2(Double.parseDouble(str))));
+      System.out.println("  CDF: " + cdf.format(new FormatQuantity2(Double.parseDouble(str))));
+      System.out.println("  PWD: " + pdf.format(new FormatQuantity2(Double.parseDouble(str))));
+      System.out.println("  EXF: " + exf.format(new FormatQuantity2(Double.parseDouble(str))));
+      System.out.println("  RIF: " + rif.format(new FormatQuantity2(Double.parseDouble(str))));
+      System.out.println("  MUF: " + muf.format(new FormatQuantity2(Double.parseDouble(str))));
+      System.out.println("  CMF: " + cmf.format(new FormatQuantity2(Double.parseDouble(str))));
+      System.out.println("  PTF: " + ptf.format(new FormatQuantity2(Double.parseDouble(str))));
+    }
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/BigDecimalMultiplier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/BigDecimalMultiplier.java
new file mode 100644 (file)
index 0000000..a6a008a
--- /dev/null
@@ -0,0 +1,57 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import java.math.BigDecimal;
+
+import com.ibm.icu.impl.number.Format.BeforeFormat;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.ModifierHolder;
+import com.ibm.icu.impl.number.Properties;
+
+public class BigDecimalMultiplier extends BeforeFormat {
+  public static interface IProperties {
+
+    static BigDecimal DEFAULT_MULTIPLIER = null;
+
+    /** @see #setMultiplier */
+    public BigDecimal getMultiplier();
+
+    /**
+     * Multiply all numbers by this amount before formatting.
+     *
+     * @param multiplier The amount to multiply by.
+     * @return The property bag, for chaining.
+     * @see MagnitudeMultiplier
+     */
+    public IProperties setMultiplier(BigDecimal multiplier);
+  }
+
+  public static boolean useMultiplier(IProperties properties) {
+    return properties.getMultiplier() != IProperties.DEFAULT_MULTIPLIER;
+  }
+
+  private final BigDecimal multiplier;
+
+  public static BigDecimalMultiplier getInstance(IProperties properties) {
+    if (properties.getMultiplier() == null) {
+      throw new IllegalArgumentException("The multiplier must be present for MultiplierFormat");
+    }
+    // TODO: Intelligently fall back to a MagnitudeMultiplier if the multiplier is a power of ten?
+    return new BigDecimalMultiplier(properties);
+  }
+
+  private BigDecimalMultiplier(IProperties properties) {
+    this.multiplier = properties.getMultiplier();
+  }
+
+  @Override
+  public void before(FormatQuantity input, ModifierHolder mods) {
+    input.multiplyBy(multiplier);
+  }
+
+  @Override
+  public void export(Properties properties) {
+    properties.setMultiplier(multiplier);
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CompactDecimalFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CompactDecimalFormat.java
new file mode 100644 (file)
index 0000000..d3d8aef
--- /dev/null
@@ -0,0 +1,449 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.ibm.icu.impl.ICUData;
+import com.ibm.icu.impl.ICUResourceBundle;
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.UResource;
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.impl.number.Modifier.PositiveNegativeModifier;
+import com.ibm.icu.impl.number.ModifierHolder;
+import com.ibm.icu.impl.number.PNAffixGenerator;
+import com.ibm.icu.impl.number.PatternString;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.impl.number.Rounder;
+import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier;
+import com.ibm.icu.impl.number.modifiers.PositiveNegativeAffixModifier;
+import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder;
+import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder.SignificantDigitsMode;
+import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.NumberFormat;
+import com.ibm.icu.text.NumberingSystem;
+import com.ibm.icu.text.PluralRules;
+import com.ibm.icu.util.ULocale;
+import com.ibm.icu.util.UResourceBundle;
+
+public class CompactDecimalFormat extends Format.BeforeFormat {
+  public static interface IProperties
+      extends RoundingFormat.IProperties, CurrencyFormat.ICurrencyProperties {
+
+    static CompactStyle DEFAULT_COMPACT_STYLE = null;
+
+    /** @see #setCompactStyle */
+    public CompactStyle getCompactStyle();
+
+    /**
+     * Use compact decimal formatting with the specified {@link CompactStyle}. CompactStyle.SHORT
+     * produces output like "10K" in locale <em>en-US</em>, whereas CompactStyle.LONG produces
+     * output like "10 thousand" in that locale.
+     *
+     * @param compactStyle The style of prefixes/suffixes to append.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setCompactStyle(CompactStyle compactStyle);
+  }
+
+  public static boolean useCompactDecimalFormat(IProperties properties) {
+    return properties.getCompactStyle() != IProperties.DEFAULT_COMPACT_STYLE;
+  }
+
+  static final int MAX_DIGITS = 15;
+
+  // Properties
+  private final CompactDecimalData data;
+  private final Rounder rounder;
+  private final PositiveNegativeModifier defaultMod;
+  private final CompactStyle style; // retained for exporting only
+
+  public static CompactDecimalFormat getInstance(
+      DecimalFormatSymbols symbols, IProperties properties) {
+    return new CompactDecimalFormat(symbols, properties);
+  }
+
+  private static Rounder getRounder(IProperties properties) {
+    // Use rounding settings if they were specified, or else use the default CDF rounder.
+    Rounder rounder = RoundingFormat.getDefaultOrNull(properties);
+    if (rounder == null) {
+      rounder =
+          SignificantDigitsRounder.getInstance(
+              SignificantDigitsRounder.getThreadLocalProperties()
+                  .setMinimumSignificantDigits(1)
+                  .setMaximumSignificantDigits(2)
+                  .setSignificantDigitsMode(SignificantDigitsMode.OVERRIDE_MAXIMUM_FRACTION));
+    }
+    return rounder;
+  }
+
+  protected static final ThreadLocal<Map<CompactDecimalFingerprint, CompactDecimalData>>
+      threadLocalDataCache =
+          new ThreadLocal<Map<CompactDecimalFingerprint, CompactDecimalData>>() {
+            @Override
+            protected Map<CompactDecimalFingerprint, CompactDecimalData> initialValue() {
+              return new HashMap<CompactDecimalFingerprint, CompactDecimalData>();
+            }
+          };
+
+  private static CompactDecimalData getData(
+      DecimalFormatSymbols symbols, CompactDecimalFingerprint fingerprint) {
+    // See if we already have a data object based on the fingerprint
+    CompactDecimalData data = threadLocalDataCache.get().get(fingerprint);
+    if (data != null) return data;
+
+    // Make data bundle object
+    data = new CompactDecimalData();
+    ULocale ulocale = symbols.getULocale();
+    CompactDecimalDataSink sink = new CompactDecimalDataSink(data, symbols, fingerprint);
+    String nsName = NumberingSystem.getInstance(ulocale).getName();
+    ICUResourceBundle r =
+        (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, ulocale);
+    r.getAllItemsWithFallback("NumberElements/" + nsName, sink);
+    if (data.isEmpty() && !nsName.equals("latn")) {
+      r.getAllItemsWithFallback("NumberElements/latn", sink);
+    }
+    if (sink.exception != null) {
+      throw sink.exception;
+    }
+    threadLocalDataCache.get().put(fingerprint, data);
+    return data;
+  }
+
+  private static PositiveNegativeModifier getDefaultMod(
+      DecimalFormatSymbols symbols, CompactDecimalFingerprint fingerprint) {
+    ULocale uloc = symbols.getULocale();
+    String pattern;
+    if (fingerprint.compactType == CompactType.CURRENCY) {
+      pattern = NumberFormat.getPattern(uloc, NumberFormat.CURRENCYSTYLE);
+    } else {
+      pattern = NumberFormat.getPattern(uloc, NumberFormat.NUMBERSTYLE);
+    }
+    // TODO: Clean this up; avoid the extra object creations.
+    // TODO: Currency may also need to override grouping settings, not just affixes.
+    Properties properties = PatternString.parseToProperties(pattern);
+    PNAffixGenerator pnag = PNAffixGenerator.getThreadLocalInstance();
+    PNAffixGenerator.Result result =
+        pnag.getModifiers(symbols, fingerprint.currencySymbol, properties);
+    return new PositiveNegativeAffixModifier(result.positive, result.negative);
+  }
+
+  private CompactDecimalFormat(DecimalFormatSymbols symbols, IProperties properties) {
+    CompactDecimalFingerprint fingerprint = new CompactDecimalFingerprint(symbols, properties);
+    this.rounder = getRounder(properties);
+    this.data = getData(symbols, fingerprint);
+    this.defaultMod = getDefaultMod(symbols, fingerprint);
+    this.style = properties.getCompactStyle(); // for exporting only
+  }
+
+  @Override
+  public void before(FormatQuantity input, ModifierHolder mods, PluralRules rules) {
+    apply(input, mods, rules, rounder, data, defaultMod);
+  }
+
+  @Override
+  protected void before(FormatQuantity input, ModifierHolder mods) {
+    throw new UnsupportedOperationException();
+  }
+
+  public static void apply(
+      FormatQuantity input,
+      ModifierHolder mods,
+      PluralRules rules,
+      DecimalFormatSymbols symbols,
+      IProperties properties) {
+    CompactDecimalFingerprint fingerprint = new CompactDecimalFingerprint(symbols, properties);
+    Rounder rounder = getRounder(properties);
+    CompactDecimalData data = getData(symbols, fingerprint);
+    PositiveNegativeModifier defaultMod = getDefaultMod(symbols, fingerprint);
+    apply(input, mods, rules, rounder, data, defaultMod);
+  }
+
+  private static void apply(
+      FormatQuantity input,
+      ModifierHolder mods,
+      PluralRules rules,
+      Rounder rounder,
+      CompactDecimalData data,
+      PositiveNegativeModifier defaultMod) {
+
+    // Treat zero as if it had magnitude 0
+    int magnitude;
+    if (input.isZero()) {
+      magnitude = 0;
+      rounder.apply(input);
+    } else {
+      int multiplier = rounder.chooseMultiplierAndApply(input, data);
+      magnitude = input.getMagnitude() - multiplier;
+    }
+
+    StandardPlural plural = input.getStandardPlural(rules);
+    boolean isNegative = input.isNegative();
+    Modifier mod = data.getModifier(magnitude, plural, isNegative);
+    if (mod == null) {
+      // Use the default (non-compact) modifier.
+      mod = defaultMod.getModifier(isNegative);
+    }
+    mods.add(mod);
+  }
+
+  @Override
+  public void export(Properties properties) {
+    properties.setCompactStyle(style);
+    rounder.export(properties);
+  }
+
+  static class CompactDecimalData implements Rounder.MultiplierGenerator {
+
+    // A dummy object used when a "0" compact decimal entry is encountered.  This is necessary
+    // in order to prevent falling back to root.
+    private static final Modifier USE_FALLBACK = new ConstantAffixModifier();
+
+    final Modifier[] mods;
+    final byte[] multipliers;
+    boolean isEmpty;
+    int largestMagnitude;
+
+    CompactDecimalData() {
+      mods = new Modifier[(MAX_DIGITS + 1) * StandardPlural.COUNT * 2];
+      multipliers = new byte[MAX_DIGITS + 1];
+      isEmpty = true;
+      largestMagnitude = -1;
+    }
+
+    boolean isEmpty() {
+      return isEmpty;
+    }
+
+    @Override
+    public int getMultiplier(int magnitude) {
+      if (magnitude < 0) {
+        return 0;
+      }
+      if (magnitude > largestMagnitude) {
+        magnitude = largestMagnitude;
+      }
+      return multipliers[magnitude];
+    }
+
+    int setOrGetMultiplier(int magnitude, byte multiplier) {
+      if (multipliers[magnitude] != 0) {
+        return multipliers[magnitude];
+      }
+      multipliers[magnitude] = multiplier;
+      isEmpty = false;
+      if (magnitude > largestMagnitude) largestMagnitude = magnitude;
+      return multiplier;
+    }
+
+    Modifier getModifier(int magnitude, StandardPlural plural, boolean isNegative) {
+      if (magnitude < 0) {
+        return null;
+      }
+      if (magnitude > largestMagnitude) {
+        magnitude = largestMagnitude;
+      }
+      Modifier mod = mods[modIndex(magnitude, plural, isNegative)];
+      if (mod == null && plural != StandardPlural.OTHER) {
+        // Fall back to "other" plural variant
+        mod = mods[modIndex(magnitude, StandardPlural.OTHER, isNegative)];
+      }
+      if (mod == USE_FALLBACK) {
+        // Return null if USE_FALLBACK is present
+        mod = null;
+      }
+      return mod;
+    }
+
+    public boolean has(int magnitude, StandardPlural plural) {
+      // Return true if USE_FALLBACK is present
+      return mods[modIndex(magnitude, plural, false)] != null;
+    }
+
+    void setModifiers(Modifier positive, Modifier negative, int magnitude, StandardPlural plural) {
+      mods[modIndex(magnitude, plural, false)] = positive;
+      mods[modIndex(magnitude, plural, true)] = negative;
+      isEmpty = false;
+      if (magnitude > largestMagnitude) largestMagnitude = magnitude;
+    }
+
+    void setNoFallback(int magnitude, StandardPlural plural) {
+      setModifiers(USE_FALLBACK, USE_FALLBACK, magnitude, plural);
+    }
+
+    private static final int modIndex(int magnitude, StandardPlural plural, boolean isNegative) {
+      return magnitude * StandardPlural.COUNT * 2 + plural.ordinal() * 2 + (isNegative ? 1 : 0);
+    }
+  }
+
+  // Should this be public or internal?
+  static enum CompactType {
+    DECIMAL,
+    CURRENCY
+  }
+
+  static class CompactDecimalFingerprint {
+    // TODO: Add more stuff to the fingerprint, like the symbols used by PNAffixGenerator
+    final CompactStyle compactStyle;
+    final CompactType compactType;
+    final ULocale uloc;
+    final String currencySymbol;
+
+    CompactDecimalFingerprint(DecimalFormatSymbols symbols, IProperties properties) {
+      // CompactDecimalFormat does not need to worry about the same constraints as non-compact
+      // currency formatting needs to consider, like the currency rounding mode and the currency
+      // long names with plural forms.
+      if (properties.getCurrency() != CurrencyFormat.ICurrencyProperties.DEFAULT_CURRENCY) {
+        compactType = CompactType.CURRENCY;
+        currencySymbol = CurrencyFormat.getCurrencySymbol(symbols, properties);
+      } else {
+        compactType = CompactType.DECIMAL;
+        currencySymbol = symbols.getCurrencySymbol(); // fallback; should remain unused
+      }
+      compactStyle = properties.getCompactStyle();
+      uloc = symbols.getULocale();
+    }
+
+    @Override
+    public boolean equals(Object _other) {
+      if (_other == null) return false;
+      if (this == _other) return true;
+      CompactDecimalFingerprint other = (CompactDecimalFingerprint) _other;
+      if (compactStyle != other.compactStyle) return false;
+      if (compactType != other.compactType) return false;
+      if (currencySymbol != other.currencySymbol) {
+        // String comparison with null handling
+        if (currencySymbol == null || other.currencySymbol == null) return false;
+        if (!currencySymbol.equals(other.currencySymbol)) return false;
+      }
+      if (!uloc.equals(other.uloc)) return false;
+      return true;
+    }
+
+    @Override
+    public int hashCode() {
+      int hashCode = 0;
+      if (compactStyle != null) hashCode ^= compactStyle.hashCode();
+      if (compactType != null) hashCode ^= compactType.hashCode();
+      if (uloc != null) hashCode ^= uloc.hashCode();
+      if (currencySymbol != null) hashCode ^= currencySymbol.hashCode();
+      return hashCode;
+    }
+  }
+
+  private static final class CompactDecimalDataSink extends UResource.Sink {
+
+    final CompactDecimalData data;
+    final DecimalFormatSymbols symbols;
+    final CompactStyle compactStyle;
+    final CompactType compactType;
+    final String currencySymbol;
+    final PNAffixGenerator pnag;
+    IllegalArgumentException exception;
+
+    /*
+     * NumberElements{              <-- top (numbering system table)
+     *  latn{                       <-- patternsTable (one per numbering system)
+     *    patternsLong{             <-- formatsTable (one per pattern)
+     *      decimalFormat{          <-- powersOfTenTable (one per format)
+     *        1000{                 <-- pluralVariantsTable (one per power of ten)
+     *          one{"0 thousand"}   <-- plural variant and template
+     */
+
+    public CompactDecimalDataSink(
+        CompactDecimalData data,
+        DecimalFormatSymbols symbols,
+        CompactDecimalFingerprint fingerprint) {
+      this.data = data;
+      this.symbols = symbols;
+      compactType = fingerprint.compactType;
+      currencySymbol = fingerprint.currencySymbol;
+      compactStyle = fingerprint.compactStyle;
+      pnag = PNAffixGenerator.getThreadLocalInstance();
+    }
+
+    @Override
+    public void put(UResource.Key key, UResource.Value value, boolean isRoot) {
+      UResource.Table patternsTable = value.getTable();
+      for (int i1 = 0; patternsTable.getKeyAndValue(i1, key, value); ++i1) {
+        if (key.contentEquals("patternsShort") && compactStyle == CompactStyle.SHORT) {
+        } else if (key.contentEquals("patternsLong") && compactStyle == CompactStyle.LONG) {
+        } else {
+          continue;
+        }
+
+        // traverse into the table of formats
+        UResource.Table formatsTable = value.getTable();
+        for (int i2 = 0; formatsTable.getKeyAndValue(i2, key, value); ++i2) {
+          if (key.contentEquals("decimalFormat") && compactType == CompactType.DECIMAL) {
+          } else if (key.contentEquals("currencyFormat") && compactType == CompactType.CURRENCY) {
+          } else {
+            continue;
+          }
+
+          // traverse into the table of powers of ten
+          UResource.Table powersOfTenTable = value.getTable();
+          for (int i3 = 0; powersOfTenTable.getKeyAndValue(i3, key, value); ++i3) {
+            try {
+
+              // Assumes that the keys are always of the form "10000" where the magnitude is the
+              // length of the key minus one
+              byte magnitude = (byte) (key.length() - 1);
+
+              // Silently ignore divisors that are too big.
+              if (magnitude >= MAX_DIGITS) continue;
+
+              // Iterate over the plural variants ("one", "other", etc)
+              UResource.Table pluralVariantsTable = value.getTable();
+              for (int i4 = 0; pluralVariantsTable.getKeyAndValue(i4, key, value); ++i4) {
+
+                // Skip this magnitude/plural if we already have it from a child locale.
+                StandardPlural plural = StandardPlural.fromString(key.toString());
+                if (data.has(magnitude, plural)) {
+                  continue;
+                }
+
+                // The value "0" means that we need to use the default pattern and not fall back
+                // to parent locales.  Example locale where this is relevant: 'it'.
+                String patternString = value.toString();
+                if (patternString.equals("0")) {
+                  data.setNoFallback(magnitude, plural);
+                  continue;
+                }
+
+                // The magnitude multiplier is the difference between the magnitude and the number
+                // of zeros in the pattern, getMinimumIntegerDigits.
+                Properties properties = PatternString.parseToProperties(patternString);
+                byte _multiplier = (byte) -(magnitude - properties.getMinimumIntegerDigits() + 1);
+                if (_multiplier != data.setOrGetMultiplier(magnitude, _multiplier)) {
+                  throw new IllegalArgumentException(
+                      String.format(
+                          "Different number of zeros for same power of ten in compact decimal format data for locale '%s', style '%s', type '%s'",
+                          symbols.getULocale().toString(),
+                          compactStyle.toString(),
+                          compactType.toString()));
+                }
+
+                PNAffixGenerator.Result result =
+                    pnag.getModifiers(symbols, currencySymbol, properties);
+                data.setModifiers(result.positive, result.negative, magnitude, plural);
+              }
+
+            } catch (IllegalArgumentException e) {
+              exception = e;
+              continue;
+            }
+          }
+
+          // We want only one table of compact decimal formats, so if we get here, stop consuming.
+          // The data.isEmpty() check will prevent further bundles from being traversed.
+          return;
+        }
+      }
+    }
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CurrencyFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/CurrencyFormat.java
new file mode 100644 (file)
index 0000000..b7575cc
--- /dev/null
@@ -0,0 +1,299 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import java.math.BigDecimal;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.number.AffixPatternUtils;
+import com.ibm.icu.impl.number.PNAffixGenerator;
+import com.ibm.icu.impl.number.PatternString;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.impl.number.Rounder;
+import com.ibm.icu.impl.number.modifiers.GeneralPluralModifier;
+import com.ibm.icu.impl.number.rounders.IncrementRounder;
+import com.ibm.icu.impl.number.rounders.MagnitudeRounder;
+import com.ibm.icu.text.CurrencyPluralInfo;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyUsage;
+
+public class CurrencyFormat {
+
+  public enum CurrencyStyle {
+    SYMBOL,
+    ISO_CODE;
+  }
+
+  public static interface ICurrencyProperties {
+    static Currency DEFAULT_CURRENCY = null;
+
+    /** @see #setCurrency */
+    public Currency getCurrency();
+
+    /**
+     * Use the specified currency to substitute currency placeholders ('¤') in the pattern string.
+     *
+     * @param currency The currency.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setCurrency(Currency currency);
+
+    static CurrencyStyle DEFAULT_CURRENCY_STYLE = null;
+
+    /** @see #setCurrencyStyle */
+    public CurrencyStyle getCurrencyStyle();
+
+    /**
+     * Use the specified {@link CurrencyStyle} to replace currency placeholders ('¤').
+     * CurrencyStyle.SYMBOL will use the short currency symbol, like "$" or "€", whereas
+     * CurrencyStyle.ISO_CODE will use the ISO 4217 currency code, like "USD" or "EUR".
+     *
+     * <p>For long currency names, use {@link MeasureFormat.IProperties#setMeasureUnit}.
+     *
+     * @param currencyStyle The currency style. Defaults to CurrencyStyle.SYMBOL.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setCurrencyStyle(CurrencyStyle currencyStyle);
+
+    /**
+     * An old enum that specifies how currencies should be rounded. It contains a subset of the
+     * functionality supported by RoundingInterval.
+     */
+    static Currency.CurrencyUsage DEFAULT_CURRENCY_USAGE = null;
+
+    /** @see #setCurrencyUsage */
+    public Currency.CurrencyUsage getCurrencyUsage();
+
+    /**
+     * Use the specified {@link CurrencyUsage} instance, which provides default rounding rules for
+     * the currency in two styles, CurrencyUsage.CASH and CurrencyUsage.STANDARD.
+     *
+     * <p>The CurrencyUsage specified here will not be used unless there is a currency placeholder
+     * in the pattern.
+     *
+     * @param currencyUsage The currency usage. Defaults to CurrencyUsage.STANDARD.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setCurrencyUsage(Currency.CurrencyUsage currencyUsage);
+
+    static CurrencyPluralInfo DEFAULT_CURRENCY_PLURAL_INFO = null;
+
+    /** @see #setCurrencyPluralInfo */
+    @Deprecated
+    public CurrencyPluralInfo getCurrencyPluralInfo();
+
+    /**
+     * Use the specified {@link CurrencyPluralInfo} instance when formatting currency long names.
+     *
+     * @param currencyPluralInfo The currency plural info object.
+     * @return The property bag, for chaining.
+     * @deprecated Use {@link MeasureFormat.IProperties#setMeasureUnit} with a Currency instead.
+     */
+    @Deprecated
+    public IProperties setCurrencyPluralInfo(CurrencyPluralInfo currencyPluralInfo);
+
+    public IProperties clone();
+  }
+
+  public static interface IProperties
+      extends ICurrencyProperties,
+          RoundingFormat.IProperties,
+          PositiveNegativeAffixFormat.IProperties {}
+
+  /**
+   * Returns true if the currency is set in The property bag or if currency symbols are present in
+   * the prefix/suffix pattern.
+   */
+  public static boolean useCurrency(IProperties properties) {
+    return ((properties.getCurrency() != null)
+        || properties.getCurrencyPluralInfo() != null
+        || AffixPatternUtils.hasCurrencySymbols(properties.getPositivePrefixPattern())
+        || AffixPatternUtils.hasCurrencySymbols(properties.getPositiveSuffixPattern())
+        || AffixPatternUtils.hasCurrencySymbols(properties.getNegativePrefixPattern())
+        || AffixPatternUtils.hasCurrencySymbols(properties.getNegativeSuffixPattern()));
+  }
+
+  /**
+   * Returns the effective currency symbol based on the input. If {@link
+   * ICurrencyProperties#setCurrencyStyle} was set to {@link CurrencyStyle#ISO_CODE}, the ISO Code
+   * will be returned; otherwise, the currency symbol, like "$", will be returned.
+   *
+   * @param symbols The current {@link DecimalFormatSymbols} instance
+   * @param properties The current property bag
+   * @return The currency symbol string, e.g., to substitute '¤' in a decimal pattern string.
+   */
+  public static String getCurrencySymbol(
+      DecimalFormatSymbols symbols, ICurrencyProperties properties) {
+    // If the user asked for ISO Code, return the ISO Code instead of the symbol
+    CurrencyStyle style = properties.getCurrencyStyle();
+    if (style == CurrencyStyle.ISO_CODE) {
+      return getCurrencyIsoCode(symbols, properties);
+    }
+
+    // Get the currency symbol
+    Currency currency = properties.getCurrency();
+    if (currency == null) {
+      return symbols.getCurrencySymbol();
+    } else if (currency.equals(symbols.getCurrency())) {
+      // The user may have set a custom currency symbol in DecimalFormatSymbols.
+      return symbols.getCurrencySymbol();
+    } else {
+      // Use the canonical symbol.
+      return currency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null);
+    }
+  }
+
+  /**
+   * Returns the currency ISO code based on the input, like "USD".
+   *
+   * @param symbols The current {@link DecimalFormatSymbols} instance
+   * @param properties The current property bag
+   * @return The currency ISO code string, e.g., to substitute '¤¤' in a decimal pattern string.
+   */
+  public static String getCurrencyIsoCode(
+      DecimalFormatSymbols symbols, ICurrencyProperties properties) {
+    Currency currency = properties.getCurrency();
+    if (currency == null) {
+      // If a currency object was not provided, use the string from symbols
+      // Note: symbols.getCurrency().getCurrencyCode() won't work here because
+      // DecimalFormatSymbols#setInternationalCurrencySymbol() does not update the
+      // immutable internal currency instance.
+      return symbols.getInternationalCurrencySymbol();
+    } else if (currency.equals(symbols.getCurrency())) {
+      // The user may have set a custom currency symbol in DecimalFormatSymbols.
+      return symbols.getInternationalCurrencySymbol();
+    } else {
+      // Use the canonical currency code.
+      return currency.getCurrencyCode();
+    }
+  }
+
+  /**
+   * Returns the currency long name on the input, like "US dollars".
+   *
+   * @param symbols The current {@link DecimalFormatSymbols} instance
+   * @param properties The current property bag
+   * @param plural The plural form
+   * @return The currency long name string, e.g., to substitute '¤¤¤' in a decimal pattern string.
+   */
+  public static String getCurrencyLongName(
+      DecimalFormatSymbols symbols, ICurrencyProperties properties, StandardPlural plural) {
+    // Attempt to get a currency object first from properties then from symbols
+    Currency currency = properties.getCurrency();
+    if (currency == null) {
+      currency = symbols.getCurrency();
+    }
+
+    // If no currency object is available, fall back to the currency symbol
+    if (currency == null) {
+      return getCurrencySymbol(symbols, properties);
+    }
+
+    // Get the long name
+    return currency.getName(
+        symbols.getULocale(), Currency.PLURAL_LONG_NAME, plural.getKeyword(), null);
+  }
+
+  public static GeneralPluralModifier getCurrencyModifier(
+      DecimalFormatSymbols symbols, IProperties properties) {
+
+    PNAffixGenerator pnag = PNAffixGenerator.getThreadLocalInstance();
+    String sym = getCurrencySymbol(symbols, properties);
+    String iso = getCurrencyIsoCode(symbols, properties);
+
+    // Previously, the user was also able to specify '¤¤' and '¤¤¤' directly into the prefix or
+    // suffix, which is how the user specified whether they wanted the ISO code or long name.
+    // For backwards compatibility support, that feature is implemented here.
+
+    CurrencyPluralInfo info = properties.getCurrencyPluralInfo();
+    GeneralPluralModifier mod = new GeneralPluralModifier();
+    Properties temp = new Properties();
+    for (StandardPlural plural : StandardPlural.VALUES) {
+      String longName = getCurrencyLongName(symbols, properties, plural);
+
+      PNAffixGenerator.Result result;
+      if (info == null) {
+        // CurrencyPluralInfo is not available.
+        result = pnag.getModifiers(symbols, sym, iso, longName, properties);
+      } else {
+        // CurrencyPluralInfo is available. Use it to generate affixes for long name support.
+        String pluralPattern = info.getCurrencyPluralPattern(plural.getKeyword());
+        PatternString.parseToExistingProperties(pluralPattern, temp, true);
+        result = pnag.getModifiers(symbols, sym, iso, longName, temp);
+      }
+      mod.put(plural, result.positive, result.negative);
+    }
+    return mod;
+  }
+
+  private static final Currency DEFAULT_CURRENCY = Currency.getInstance("XXX");
+
+  public static void populateCurrencyRounderProperties(
+      Properties destination, DecimalFormatSymbols symbols, IProperties properties) {
+
+    Currency currency = properties.getCurrency();
+    if (currency == null) {
+      // Fall back to the DecimalFormatSymbols currency instance.
+      currency = symbols.getCurrency();
+    }
+    if (currency == null) {
+      // There is a currency symbol in the pattern, but we have no currency available to use.
+      // Use the default currency instead so that we can still apply currency usage rules.
+      currency = DEFAULT_CURRENCY;
+    }
+
+    Currency.CurrencyUsage currencyUsage = properties.getCurrencyUsage();
+    if (currencyUsage == null) {
+      currencyUsage = CurrencyUsage.STANDARD;
+    }
+
+    double incrementDouble = currency.getRoundingIncrement(currencyUsage);
+    int fractionDigits = currency.getDefaultFractionDigits(currencyUsage);
+
+    destination.setRoundingMode(properties.getRoundingMode());
+    destination.setMinimumIntegerDigits(properties.getMinimumIntegerDigits());
+    destination.setMaximumIntegerDigits(properties.getMaximumIntegerDigits());
+
+    int _minFrac = properties.getMinimumFractionDigits();
+    int _maxFrac = properties.getMaximumFractionDigits();
+    if (_minFrac >= 0 || _maxFrac >= 0) {
+      // User override
+      destination.setMinimumFractionDigits(_minFrac);
+      destination.setMaximumFractionDigits(_maxFrac);
+    } else {
+      destination.setMinimumFractionDigits(fractionDigits);
+      destination.setMaximumFractionDigits(fractionDigits);
+    }
+
+    if (incrementDouble > 0.0) {
+      BigDecimal incrementBigDecimal;
+      BigDecimal _roundingIncrement = properties.getRoundingIncrement();
+      if (_roundingIncrement != null) {
+        incrementBigDecimal = _roundingIncrement;
+      } else {
+        incrementBigDecimal = BigDecimal.valueOf(incrementDouble);
+      }
+      destination.setRoundingIncrement(incrementBigDecimal);
+    } else {
+    }
+  }
+
+  private static final ThreadLocal<Properties> threadLocalProperties =
+      new ThreadLocal<Properties>() {
+        @Override
+        protected Properties initialValue() {
+          return new Properties();
+        }
+      };
+
+  public static Rounder getCurrencyRounder(DecimalFormatSymbols symbols, IProperties properties) {
+    Properties cprops = threadLocalProperties.get().clear();
+    populateCurrencyRounderProperties(cprops, symbols, properties);
+    if (cprops.getRoundingIncrement() != null) {
+      return IncrementRounder.getInstance(cprops);
+    } else {
+      return MagnitudeRounder.getInstance(cprops);
+    }
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/MagnitudeMultiplier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/MagnitudeMultiplier.java
new file mode 100644 (file)
index 0000000..6e19c7d
--- /dev/null
@@ -0,0 +1,59 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.Format.BeforeFormat;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.ModifierHolder;
+import com.ibm.icu.impl.number.Properties;
+
+public class MagnitudeMultiplier extends Format.BeforeFormat {
+  private static final MagnitudeMultiplier DEFAULT = new MagnitudeMultiplier(0);
+
+  public static interface IProperties {
+
+    static int DEFAULT_MAGNITUDE_MULTIPLIER = 0;
+
+    /** @see #setMagnitudeMultiplier */
+    public int getMagnitudeMultiplier();
+
+    /**
+     * Multiply all numbers by this power of ten before formatting. Negative multipliers reduce the
+     * magnitude and make numbers smaller (closer to zero).
+     *
+     * @param magnitudeMultiplier The number of powers of ten to scale.
+     * @return The property bag, for chaining.
+     * @see BigDecimalMultiplier
+     */
+    public IProperties setMagnitudeMultiplier(int magnitudeMultiplier);
+  }
+
+  public static boolean useMagnitudeMultiplier(IProperties properties) {
+    return properties.getMagnitudeMultiplier() != IProperties.DEFAULT_MAGNITUDE_MULTIPLIER;
+  }
+
+  // Properties
+  final int delta;
+
+  public static BeforeFormat getInstance(Properties properties) {
+    if (properties.getMagnitudeMultiplier() == 0) {
+      return DEFAULT;
+    }
+    return new MagnitudeMultiplier(properties.getMagnitudeMultiplier());
+  }
+
+  private MagnitudeMultiplier(int delta) {
+    this.delta = delta;
+  }
+
+  @Override
+  public void before(FormatQuantity input, ModifierHolder mods) {
+    input.adjustMagnitude(delta);
+  }
+
+  @Override
+  public void export(Properties properties) {
+    properties.setMagnitudeMultiplier(delta);
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/MeasureFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/MeasureFormat.java
new file mode 100644 (file)
index 0000000..752dc0a
--- /dev/null
@@ -0,0 +1,73 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.number.modifiers.GeneralPluralModifier;
+import com.ibm.icu.impl.number.modifiers.SimpleModifier;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.MeasureFormat.FormatWidth;
+import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.ULocale;
+
+public class MeasureFormat {
+
+  public static interface IProperties {
+
+    static MeasureUnit DEFAULT_MEASURE_UNIT = null;
+
+    /** @see #setMeasureUnit */
+    public MeasureUnit getMeasureUnit();
+
+    /**
+     * Apply prefixes and suffixes for the specified {@link MeasureUnit} to the formatted number.
+     *
+     * @param measureUnit The measure unit.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setMeasureUnit(MeasureUnit measureUnit);
+
+    static FormatWidth DEFAULT_MEASURE_FORMAT_WIDTH = null;
+
+    /** @see #setMeasureFormatWidth */
+    public FormatWidth getMeasureFormatWidth();
+
+    /**
+     * Use the specified {@link FormatWidth} when choosing the style of measure unit prefix/suffix.
+     *
+     * <p>Must be used in conjunction with {@link #setMeasureUnit}.
+     *
+     * @param measureFormatWidth The width style. Defaults to FormatWidth.WIDE.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setMeasureFormatWidth(FormatWidth measureFormatWidth);
+  }
+
+  public static boolean useMeasureFormat(IProperties properties) {
+    return properties.getMeasureUnit() != IProperties.DEFAULT_MEASURE_UNIT;
+  }
+
+  public static GeneralPluralModifier getInstance(DecimalFormatSymbols symbols, IProperties properties) {
+    ULocale uloc = symbols.getULocale();
+    MeasureUnit unit = properties.getMeasureUnit();
+    FormatWidth width = properties.getMeasureFormatWidth();
+
+    if (unit == null) {
+      throw new IllegalArgumentException("A measure unit is required for MeasureFormat");
+    }
+    if (width == null) {
+      width = FormatWidth.WIDE;
+    }
+
+    // Temporarily, create a MeasureFormat instance for its data loading capability
+    // TODO: Move data loading directly into this class file
+    com.ibm.icu.text.MeasureFormat mf = com.ibm.icu.text.MeasureFormat.getInstance(uloc, width);
+    GeneralPluralModifier mod = new GeneralPluralModifier();
+    for (StandardPlural plural : StandardPlural.VALUES) {
+      String formatString = null;
+      mf.getPluralFormatter(unit, width, plural.ordinal());
+      mod.put(plural, new SimpleModifier(formatString, null, false));
+    }
+    return mod;
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PaddingFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PaddingFormat.java
new file mode 100644 (file)
index 0000000..5aa3c48
--- /dev/null
@@ -0,0 +1,173 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import com.ibm.icu.impl.number.Format.AfterFormat;
+import com.ibm.icu.impl.number.ModifierHolder;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.Properties;
+
+public class PaddingFormat implements AfterFormat {
+  public enum PadPosition {
+    BEFORE_PREFIX,
+    AFTER_PREFIX,
+    BEFORE_SUFFIX,
+    AFTER_SUFFIX;
+
+    public static PadPosition fromOld(int old) {
+      switch (old) {
+        case com.ibm.icu.text.DecimalFormat.PAD_BEFORE_PREFIX:
+          return PadPosition.BEFORE_PREFIX;
+        case com.ibm.icu.text.DecimalFormat.PAD_AFTER_PREFIX:
+          return PadPosition.AFTER_PREFIX;
+        case com.ibm.icu.text.DecimalFormat.PAD_BEFORE_SUFFIX:
+          return PadPosition.BEFORE_SUFFIX;
+        case com.ibm.icu.text.DecimalFormat.PAD_AFTER_SUFFIX:
+          return PadPosition.AFTER_SUFFIX;
+        default:
+          throw new IllegalArgumentException("Don't know how to map " + old);
+      }
+    }
+
+    public int toOld() {
+      switch (this) {
+        case BEFORE_PREFIX:
+          return com.ibm.icu.text.DecimalFormat.PAD_BEFORE_PREFIX;
+        case AFTER_PREFIX:
+          return com.ibm.icu.text.DecimalFormat.PAD_AFTER_PREFIX;
+        case BEFORE_SUFFIX:
+          return com.ibm.icu.text.DecimalFormat.PAD_BEFORE_SUFFIX;
+        case AFTER_SUFFIX:
+          return com.ibm.icu.text.DecimalFormat.PAD_AFTER_SUFFIX;
+        default:
+          return -1; // silence compiler errors
+      }
+    }
+  }
+
+  public static interface IProperties {
+
+    static int DEFAULT_FORMAT_WIDTH = 0;
+
+    /** @see #setFormatWidth */
+    public int getFormatWidth();
+
+    /**
+     * Sets the minimum width of the string output by the formatting pipeline. For example, if
+     * padding is enabled and paddingWidth is set to 6, formatting the number "3.14159" with the
+     * pattern "0.00" will result in "··3.14" if '·' is your padding string.
+     *
+     * <p>If the number is longer than your padding width, the number will display as if no padding
+     * width had been specified, which may result in strings longer than the padding width.
+     *
+     * <p>Width is counted in UTF-16 code units.
+     *
+     * @param formatWidth The output width.
+     * @return The property bag, for chaining.
+     * @see #setPadPosition
+     * @see #setPadString
+     */
+    public IProperties setFormatWidth(int formatWidth);
+
+    static String DEFAULT_PAD_STRING = null;
+
+    /** @see #setPadString */
+    public String getPadString();
+
+    /**
+     * Sets the string used for padding. The string should contain a single character or grapheme
+     * cluster.
+     *
+     * <p>Must be used in conjunction with {@link #setFormatWidth}.
+     *
+     * @param paddingString The padding string. Defaults to an ASCII space (U+0020).
+     * @return The property bag, for chaining.
+     * @see #setFormatWidth
+     */
+    public IProperties setPadString(String paddingString);
+
+    static PadPosition DEFAULT_PAD_POSITION = null;
+
+    /** @see #setPadPosition */
+    public PadPosition getPadPosition();
+
+    /**
+     * Sets the location where the padding string is to be inserted to maintain the padding width:
+     * one of BEFORE_PREFIX, AFTER_PREFIX, BEFORE_SUFFIX, or AFTER_SUFFIX.
+     *
+     * <p>Must be used in conjunction with {@link #setFormatWidth}.
+     *
+     * @param padPosition The output width.
+     * @return The property bag, for chaining.
+     * @see #setFormatWidth
+     */
+    public IProperties setPadPosition(PadPosition padPosition);
+  }
+
+  public static final String FALLBACK_PADDING_STRING = "\u0020"; // i.e. a space
+
+  public static boolean usePadding(IProperties properties) {
+    return properties.getFormatWidth() != IProperties.DEFAULT_FORMAT_WIDTH;
+  }
+
+  public static AfterFormat getInstance(IProperties properties) {
+    return new PaddingFormat(
+        properties.getFormatWidth(),
+        properties.getPadString(),
+        properties.getPadPosition());
+  }
+
+  // Properties
+  private final int paddingWidth;
+  private final String paddingString;
+  private final PadPosition paddingLocation;
+
+  private PaddingFormat(
+      int paddingWidth, String paddingString, PadPosition paddingLocation) {
+    this.paddingWidth = paddingWidth > 0 ? paddingWidth : 10; // TODO: Is this a sensible default?
+    this.paddingString = paddingString != null ? paddingString : FALLBACK_PADDING_STRING;
+    this.paddingLocation =
+        paddingLocation != null ? paddingLocation : PadPosition.BEFORE_PREFIX;
+  }
+
+  @Override
+  public int after(ModifierHolder mods, NumberStringBuilder string, int leftIndex, int rightIndex) {
+
+    // TODO: Count code points instead of code units?
+    int requiredPadding = paddingWidth - (rightIndex - leftIndex) - mods.totalLength();
+
+    if (requiredPadding <= 0) {
+      // Skip padding, but still apply modifiers to be consistent
+      return mods.applyAll(string, leftIndex, rightIndex);
+    }
+
+    int length = 0;
+    if (paddingLocation == PadPosition.AFTER_PREFIX) {
+      length += addPadding(requiredPadding, string, leftIndex);
+    } else if (paddingLocation == PadPosition.BEFORE_SUFFIX) {
+      length += addPadding(requiredPadding, string, rightIndex);
+    }
+    length += mods.applyAll(string, leftIndex, rightIndex + length);
+    if (paddingLocation == PadPosition.BEFORE_PREFIX) {
+      length += addPadding(requiredPadding, string, leftIndex);
+    } else if (paddingLocation == PadPosition.AFTER_SUFFIX) {
+      length += addPadding(requiredPadding, string, rightIndex + length);
+    }
+
+    return length;
+  }
+
+  private int addPadding(int requiredPadding, NumberStringBuilder string, int index) {
+    for (int i = 0; i < requiredPadding; i++) {
+      string.insert(index, paddingString, null);
+    }
+    return paddingString.length() * requiredPadding;
+  }
+
+  @Override
+  public void export(Properties properties) {
+    properties.setFormatWidth(paddingWidth);
+    properties.setPadString(paddingString);
+    properties.setPadPosition(paddingLocation);
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PositiveDecimalFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PositiveDecimalFormat.java
new file mode 100644 (file)
index 0000000..f791ca4
--- /dev/null
@@ -0,0 +1,227 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.NumberFormat;
+import com.ibm.icu.text.NumberFormat.Field;
+
+public class PositiveDecimalFormat implements Format.TargetFormat {
+
+  public static interface IProperties extends CurrencyFormat.IProperties {
+
+    static int DEFAULT_GROUPING_SIZE = -1;
+
+    /** @see #setGroupingSize */
+    public int getGroupingSize();
+
+    /**
+     * Sets the number of digits between grouping separators. For example, the <em>en-US</em> locale
+     * uses a grouping size of 3, so the number 1234567 would be formatted as "1,234,567". For
+     * locales whose grouping sizes vary with magnitude, see {@link #setSecondaryGroupingSize(int)}.
+     *
+     * @param groupingSize The primary grouping size.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setGroupingSize(int groupingSize);
+
+    static int DEFAULT_SECONDARY_GROUPING_SIZE = -1;
+
+    /** @see #setSecondaryGroupingSize */
+    public int getSecondaryGroupingSize();
+
+    /**
+     * Sets the number of digits between grouping separators higher than the least-significant
+     * grouping separator. For example, the locale <em>hi</em> uses a primary grouping size of 3 and
+     * a secondary grouping size of 2, so the number 1234567 would be formatted as "12,34,567".
+     *
+     * <p>The two levels of grouping separators can be specified in the pattern string. For example,
+     * the <em>hi</em> locale's default decimal format pattern is "#,##,##0.###".
+     *
+     * @param secondaryGroupingSize The secondary grouping size.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setSecondaryGroupingSize(int secondaryGroupingSize);
+
+    static boolean DEFAULT_DECIMAL_SEPARATOR_ALWAYS_SHOWN = false;
+
+    /** @see #setDecimalSeparatorAlwaysShown */
+    public boolean getDecimalSeparatorAlwaysShown();
+
+    /**
+     * Sets whether to always show the decimal point, even if the number doesn't require one. For
+     * example, if always show decimal is true, the number 123 would be formatted as "123." in
+     * locale <em>en-US</em>.
+     *
+     * @param decimalSeparatorAlwaysShown Whether to show the decimal point when it is optional.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setDecimalSeparatorAlwaysShown(boolean decimalSeparatorAlwaysShown);
+
+    static int DEFAULT_MINIMUM_GROUPING_DIGITS = 1;
+
+    /** @see #setMinimumGroupingDigits */
+    public int getMinimumGroupingDigits();
+
+    /**
+     * Sets the minimum number of digits required to be beyond the first grouping separator in order
+     * to enable grouping. For example, if the minimum grouping digits is 2, then 1234 would be
+     * formatted as "1234" but 12345 would be formatted as "12,345" in <em>en-US</em>. Note that
+     * 1234567 would still be formatted as "1,234,567", not "1234,567".
+     *
+     * @param minimumGroupingDigits How many digits must appear before a grouping separator before
+     *     enabling grouping.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setMinimumGroupingDigits(int minimumGroupingDigits);
+  }
+
+  public static boolean useGrouping(IProperties properties) {
+    return properties.getGroupingSize() != IProperties.DEFAULT_GROUPING_SIZE
+        || properties.getSecondaryGroupingSize() != IProperties.DEFAULT_SECONDARY_GROUPING_SIZE;
+  }
+
+  public static boolean allowsDecimalPoint(IProperties properties) {
+    return properties.getDecimalSeparatorAlwaysShown() || properties.getMaximumFractionDigits() != 0;
+  }
+
+  // Properties
+  private final boolean alwaysShowDecimal;
+  private final int groupingSize;
+  private final int secondaryGroupingSize;
+  private final int minimumGroupingDigits;
+
+  // Symbols
+  private final String infinityString;
+  private final String nanString;
+  private final String groupingSeparator;
+  private final String decimalSeparator;
+  private final String[] digitStrings;
+  private final int codePointZero;
+
+  public PositiveDecimalFormat(DecimalFormatSymbols symbols, IProperties properties) {
+    groupingSize =
+        (properties.getGroupingSize() < 0)
+            ? properties.getSecondaryGroupingSize()
+            : properties.getGroupingSize();
+    secondaryGroupingSize =
+        (properties.getSecondaryGroupingSize() < 0)
+            ? properties.getGroupingSize()
+            : properties.getSecondaryGroupingSize();
+
+    minimumGroupingDigits = properties.getMinimumGroupingDigits();
+    alwaysShowDecimal = properties.getDecimalSeparatorAlwaysShown();
+    infinityString = symbols.getInfinity();
+    nanString = symbols.getNaN();
+
+    if (CurrencyFormat.useCurrency(properties)) {
+      groupingSeparator = symbols.getMonetaryGroupingSeparatorString();
+      decimalSeparator = symbols.getMonetaryDecimalSeparatorString();
+    } else {
+      groupingSeparator = symbols.getGroupingSeparatorString();
+      decimalSeparator = symbols.getDecimalSeparatorString();
+    }
+
+    // Check to see if we can use code points instead of strings (~15% format performance boost)
+    int _codePointZero = -1;
+    String[] _digitStrings = symbols.getDigitStringsLocal();
+    for (int i = 0; i < _digitStrings.length; i++) {
+      int cp = Character.codePointAt(_digitStrings[i], 0);
+      int cc = Character.charCount(cp);
+      if (cc != _digitStrings[i].length()) {
+        _codePointZero = -1;
+        break;
+      } else if (i == 0) {
+        _codePointZero = cp;
+      } else if (cp != _codePointZero + i) {
+        _codePointZero = -1;
+        break;
+      }
+    }
+    if (_codePointZero != -1) {
+      digitStrings = null;
+      codePointZero = _codePointZero;
+    } else {
+      digitStrings = symbols.getDigitStrings(); // makes a copy
+      codePointZero = -1;
+    }
+  }
+
+  @Override
+  public int target(FormatQuantity input, NumberStringBuilder string, int startIndex) {
+    int length = 0;
+
+    if (input.isInfinite()) {
+      length += string.insert(startIndex, infinityString, NumberFormat.Field.INTEGER);
+
+    } else if (input.isNaN()) {
+      length += string.insert(startIndex, nanString, NumberFormat.Field.INTEGER);
+
+    } else {
+      // Add the integer digits
+      length += addIntegerDigits(input, string, startIndex);
+
+      // Add the decimal point
+      if (input.getLowerDisplayMagnitude() < 0 || alwaysShowDecimal) {
+        length += string.insert(startIndex + length, decimalSeparator, NumberFormat.Field.DECIMAL_SEPARATOR);
+      }
+
+      // Add the fraction digits
+      length += addFractionDigits(input, string, startIndex + length);
+    }
+
+    return length;
+  }
+
+  private int addIntegerDigits(FormatQuantity input, NumberStringBuilder string, int startIndex) {
+    int length = 0;
+    int integerCount = input.getUpperDisplayMagnitude() + 1;
+    for (int i = 0; i < integerCount; i++) {
+      // Add grouping separator
+      if (groupingSize > 0 && i == groupingSize && integerCount - i >= minimumGroupingDigits) {
+        length += string.insert(startIndex, groupingSeparator, NumberFormat.Field.GROUPING_SEPARATOR);
+      } else if (secondaryGroupingSize > 0
+          && i > groupingSize
+          && (i - groupingSize) % secondaryGroupingSize == 0) {
+        length += string.insert(startIndex, groupingSeparator, NumberFormat.Field.GROUPING_SEPARATOR);
+      }
+
+      // Get and append the next digit value
+      byte nextDigit = input.getDigit(i);
+      length += addDigit(nextDigit, string, startIndex, NumberFormat.Field.INTEGER);
+    }
+
+    return length;
+  }
+
+  private int addFractionDigits(FormatQuantity input, NumberStringBuilder string, int index) {
+    int length = 0;
+    int fractionCount = -input.getLowerDisplayMagnitude();
+    for (int i = 0; i < fractionCount; i++) {
+      // Get and append the next digit value
+      byte nextDigit = input.getDigit(-i - 1);
+      length += addDigit(nextDigit, string, index + length, NumberFormat.Field.FRACTION);
+    }
+    return length;
+  }
+
+  private int addDigit(byte digit, NumberStringBuilder outputString, int index, Field field) {
+    if (codePointZero != -1) {
+      return outputString.insertCodePoint(index, codePointZero + digit, field);
+    } else {
+      return outputString.insert(index, digitStrings[digit], field);
+    }
+  }
+
+  @Override
+  public void export(Properties properties) {
+    properties.setDecimalSeparatorAlwaysShown(alwaysShowDecimal);
+    properties.setGroupingSize(groupingSize);
+    properties.setSecondaryGroupingSize(secondaryGroupingSize);
+    properties.setMinimumGroupingDigits(minimumGroupingDigits);
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PositiveNegativeAffixFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/PositiveNegativeAffixFormat.java
new file mode 100644 (file)
index 0000000..f71374a
--- /dev/null
@@ -0,0 +1,256 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.ModifierHolder;
+import com.ibm.icu.impl.number.PNAffixGenerator;
+import com.ibm.icu.impl.number.modifiers.PositiveNegativeAffixModifier;
+import com.ibm.icu.text.DecimalFormatSymbols;
+
+/**
+ * The implementation of this class is a thin wrapper around {@link PNAffixGenerator}, a utility
+ * used by this and other classes, including {@link CompactDecimalFormat} and {@link Parse}, to
+ * efficiently convert from the abstract properties in the property bag to actual prefix and suffix
+ * strings.
+ */
+
+/**
+ * This class is responsible for adding the positive/negative prefixes and suffixes from the decimal
+ * format pattern. Properties are set using the following methods:
+ *
+ * <ul>
+ *   <li>{@link IProperties#setPositivePrefix(String)}
+ *   <li>{@link IProperties#setPositiveSuffix(String)}
+ *   <li>{@link IProperties#setNegativePrefix(String)}
+ *   <li>{@link IProperties#setNegativeSuffix(String)}
+ *   <li>{@link IProperties#setPositivePrefixPattern(String)}
+ *   <li>{@link IProperties#setPositiveSuffixPattern(String)}
+ *   <li>{@link IProperties#setNegativePrefixPattern(String)}
+ *   <li>{@link IProperties#setNegativeSuffixPattern(String)}
+ * </ul>
+ *
+ * If one of the first four methods is used (those of the form <code>setXxxYyy</code>), the value
+ * will be interpreted literally. If one of the second four methods is used (those of the form
+ * <code>setXxxYyyPattern</code>), locale-specific symbols for the plus sign, minus sign, percent
+ * sign, permille sign, and currency sign will be substituted into the string, according to Unicode
+ * Technical Standard #35 (LDML) section 3.2.
+ *
+ * <p>Literal characters can be used in the <code>setXxxYyyPattern</code> methods by using quotes;
+ * for example, to display a literal "%" sign, you can set the pattern <code>'%'</code>. To display
+ * a literal quote, use two quotes in a row, like <code>''</code>.
+ *
+ * <p>If a value is set in both a <code>setXxxYyy</code> method and in the corresponding <code>
+ * setXxxYyyPattern</code> method, the one set in <code>setXxxYyy</code> takes precedence.
+ *
+ * <p>For more information on formatting currencies, see {@link CurrencyFormat}.
+ *
+ * <p>The parameter is taken by reference by these methods into the property bag, meaning that if a
+ * mutable object like StringBuilder is passed, changes to the StringBuilder will be reflected in
+ * the property bag. However, upon creation of a finalized formatter object, all prefixes and
+ * suffixes will be converted to strings and will stop reflecting changes in the property bag.
+ */
+public class PositiveNegativeAffixFormat {
+
+  public static interface IProperties {
+
+    static String DEFAULT_POSITIVE_PREFIX = null;
+
+    /** @see #setPositivePrefix */
+    public String getPositivePrefix();
+
+    /**
+     * Sets the prefix to prepend to positive numbers. The prefix will be interpreted literally. For
+     * example, if you set a positive prefix of <code>p</code>, then the number 123 will be
+     * formatted as "p123" in the locale <em>en-US</em>.
+     *
+     * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+     *
+     * @param positivePrefix The CharSequence to prepend to positive numbers.
+     * @return The property bag, for chaining.
+     * @see PositiveNegativeAffixFormat
+     * @see #setPositivePrefixPattern
+     */
+    public IProperties setPositivePrefix(String positivePrefix);
+
+    static String DEFAULT_POSITIVE_SUFFIX = null;
+
+    /** @see #setPositiveSuffix */
+    public String getPositiveSuffix();
+
+    /**
+     * Sets the suffix to append to positive numbers. The suffix will be interpreted literally. For
+     * example, if you set a positive suffix of <code>p</code>, then the number 123 will be
+     * formatted as "123p" in the locale <em>en-US</em>.
+     *
+     * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+     *
+     * @param positiveSuffix The CharSequence to append to positive numbers.
+     * @return The property bag, for chaining.
+     * @see PositiveNegativeAffixFormat
+     * @see #setPositiveSuffixPattern
+     */
+    public IProperties setPositiveSuffix(String positiveSuffix);
+
+    static String DEFAULT_NEGATIVE_PREFIX = null;
+
+    /** @see #setNegativePrefix */
+    public String getNegativePrefix();
+
+    /**
+     * Sets the prefix to prepend to negative numbers. The prefix will be interpreted literally. For
+     * example, if you set a negative prefix of <code>n</code>, then the number -123 will be
+     * formatted as "n123" in the locale <em>en-US</em>. Note that if the negative prefix is left unset,
+     * the locale's minus sign is used.
+     *
+     * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+     *
+     * @param negativePrefix The CharSequence to prepend to negative numbers.
+     * @return The property bag, for chaining.
+     * @see PositiveNegativeAffixFormat
+     * @see #setNegativePrefixPattern
+     */
+    public IProperties setNegativePrefix(String negativePrefix);
+
+    static String DEFAULT_NEGATIVE_SUFFIX = null;
+
+    /** @see #setNegativeSuffix */
+    public String getNegativeSuffix();
+
+    /**
+     * Sets the suffix to append to negative numbers. The suffix will be interpreted literally. For
+     * example, if you set a suffix prefix of <code>n</code>, then the number -123 will be formatted
+     * as "-123n" in the locale <em>en-US</em>. Note that the minus sign is prepended by default unless
+     * otherwise specified in either the pattern string or in one of the {@link #setNegativePrefix}
+     * methods.
+     *
+     * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+     *
+     * @param negativeSuffix The CharSequence to append to negative numbers.
+     * @return The property bag, for chaining.
+     * @see PositiveNegativeAffixFormat
+     * @see #setNegativeSuffixPattern
+     */
+    public IProperties setNegativeSuffix(String negativeSuffix);
+
+    static String DEFAULT_POSITIVE_PREFIX_PATTERN = null;
+
+    /** @see #setPositivePrefixPattern */
+    public String getPositivePrefixPattern();
+
+    /**
+     * Sets the prefix to prepend to positive numbers. Locale-specific symbols will be substituted
+     * into the string according to Unicode Technical Standard #35 (LDML).
+     *
+     * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+     *
+     * @param positivePrefixPattern The CharSequence to prepend to positive numbers after locale
+     *     symbol substitutions take place.
+     * @return The property bag, for chaining.
+     * @see PositiveNegativeAffixFormat
+     * @see #setPositivePrefix
+     */
+    public IProperties setPositivePrefixPattern(String positivePrefixPattern);
+
+    static String DEFAULT_POSITIVE_SUFFIX_PATTERN = null;
+
+    /** @see #setPositiveSuffixPattern */
+    public String getPositiveSuffixPattern();
+
+    /**
+     * Sets the suffix to append to positive numbers. Locale-specific symbols will be substituted
+     * into the string according to Unicode Technical Standard #35 (LDML).
+     *
+     * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+     *
+     * @param positiveSuffixPattern The CharSequence to append to positive numbers after locale
+     *     symbol substitutions take place.
+     * @return The property bag, for chaining.
+     * @see PositiveNegativeAffixFormat
+     * @see #setPositiveSuffix
+     */
+    public IProperties setPositiveSuffixPattern(String positiveSuffixPattern);
+
+    static String DEFAULT_NEGATIVE_PREFIX_PATTERN = null;
+
+    /** @see #setNegativePrefixPattern */
+    public String getNegativePrefixPattern();
+
+    /**
+     * Sets the prefix to prepend to negative numbers. Locale-specific symbols will be substituted
+     * into the string according to Unicode Technical Standard #35 (LDML).
+     *
+     * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+     *
+     * @param negativePrefixPattern The CharSequence to prepend to negative numbers after locale
+     *     symbol substitutions take place.
+     * @return The property bag, for chaining.
+     * @see PositiveNegativeAffixFormat
+     * @see #setNegativePrefix
+     */
+    public IProperties setNegativePrefixPattern(String negativePrefixPattern);
+
+    static String DEFAULT_NEGATIVE_SUFFIX_PATTERN = null;
+
+    /** @see #setNegativeSuffixPattern */
+    public String getNegativeSuffixPattern();
+
+    /**
+     * Sets the suffix to append to negative numbers. Locale-specific symbols will be substituted
+     * into the string according to Unicode Technical Standard #35 (LDML).
+     *
+     * <p>For more information on prefixes and suffixes, see {@link PositiveNegativeAffixFormat}.
+     *
+     * @param negativeSuffixPattern The CharSequence to append to negative numbers after locale
+     *     symbol substitutions take place.
+     * @return The property bag, for chaining.
+     * @see PositiveNegativeAffixFormat
+     * @see #setNegativeSuffix
+     */
+    public IProperties setNegativeSuffixPattern(String negativeSuffixPattern);
+
+    static boolean DEFAULT_PLUS_SIGN_ALWAYS_SHOWN = false;
+
+    /** @see #setPlusSignAlwaysShown */
+    public boolean getPlusSignAlwaysShown();
+
+    /**
+     * Sets whether to always display of a plus sign on positive numbers.
+     *
+     * <p>If the location of the negative sign is specified by the decimal format pattern (or by the
+     * negative prefix/suffix pattern methods), a plus sign is substituted into that location, in
+     * accordance with Unicode Technical Standard #35 (LDML) section 3.2.1. Otherwise, the plus sign
+     * is prepended to the number. For example, if the decimal format pattern <code>#;#-</code> is
+     * used, then formatting 123 would result in "123+" in the locale <em>en-US</em>.
+     *
+     * <p>This method should be used <em>instead of</em> setting the positive prefix/suffix. The
+     * behavior is undefined if alwaysShowPlusSign is set but the positive prefix/suffix already
+     * contains a plus sign.
+     *
+     * @param plusSignAlwaysShown Whether positive numbers should display a plus sign.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setPlusSignAlwaysShown(boolean plusSignAlwaysShown);
+  }
+
+  public static PositiveNegativeAffixModifier getInstance(DecimalFormatSymbols symbols, IProperties properties) {
+    PNAffixGenerator pnag = PNAffixGenerator.getThreadLocalInstance();
+    PNAffixGenerator.Result result = pnag.getModifiers(symbols, properties);
+    return new PositiveNegativeAffixModifier(result.positive, result.negative);
+  }
+
+  // TODO: Investigate static interface methods (Java 8 only?)
+  public static void apply(
+      FormatQuantity input,
+      ModifierHolder mods,
+      DecimalFormatSymbols symbols,
+      IProperties properties) {
+    PNAffixGenerator pnag = PNAffixGenerator.getThreadLocalInstance();
+    PNAffixGenerator.Result result = pnag.getModifiers(symbols, properties);
+    if (input.isNegative()) {
+      mods.add(result.negative);
+    } else {
+      mods.add(result.positive);
+    }
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/RangeFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/RangeFormat.java
new file mode 100644 (file)
index 0000000..7c72e62
--- /dev/null
@@ -0,0 +1,58 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+// THIS CLASS IS A PROOF OF CONCEPT ONLY.
+// IT REQUIRES ADDITIONAL DISCUSION ABOUT ITS DESIGN AND IMPLEMENTATION.
+
+package com.ibm.icu.impl.number.formatters;
+
+import java.util.Deque;
+
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.ModifierHolder;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+
+public class RangeFormat extends Format {
+  // Primary settings
+  private final String separator;
+
+  // Child formatters
+  private final Format left;
+  private final Format right;
+
+  public RangeFormat(Format left, Format right, String separator) {
+    this.separator = separator; // TODO: This would be loaded from locale data.
+    this.left = left;
+    this.right = right;
+
+    if (left == null || right == null) {
+      throw new IllegalArgumentException("Both child formatters are required for RangeFormat");
+    }
+  }
+
+  @Override
+  public int process(
+      Deque<FormatQuantity> inputs,
+      ModifierHolder mods,
+      NumberStringBuilder string,
+      int startIndex) {
+    ModifierHolder lMods = new ModifierHolder();
+    ModifierHolder rMods = new ModifierHolder();
+    int lLen = left.process(inputs, lMods, string, startIndex);
+    int rLen = right.process(inputs, rMods, string, startIndex + lLen);
+
+    // Bubble up any modifiers that are shared between the two sides
+    while (lMods.peekLast() != null && lMods.peekLast() == rMods.peekLast()) {
+      mods.add(lMods.removeLast());
+      rMods.removeLast();
+    }
+
+    // Apply the remaining modifiers
+    lLen += lMods.applyAll(string, startIndex, startIndex + lLen);
+    rLen += rMods.applyAll(string, startIndex + lLen, startIndex + lLen + rLen);
+
+    int sLen = string.insert(startIndex + lLen, separator, null);
+
+    return lLen + sLen + rLen;
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/RoundingFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/RoundingFormat.java
new file mode 100644 (file)
index 0000000..a57caf4
--- /dev/null
@@ -0,0 +1,41 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import com.ibm.icu.impl.number.Rounder;
+import com.ibm.icu.impl.number.Rounder.IBasicRoundingProperties;
+import com.ibm.icu.impl.number.rounders.IncrementRounder;
+import com.ibm.icu.impl.number.rounders.MagnitudeRounder;
+import com.ibm.icu.impl.number.rounders.NoRounder;
+import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder;
+
+// TODO: Figure out a better place to put these methods.
+
+public class RoundingFormat {
+
+  public static interface IProperties
+      extends IBasicRoundingProperties,
+          IncrementRounder.IProperties,
+          MagnitudeRounder.IProperties,
+          SignificantDigitsRounder.IProperties {}
+
+  public static Rounder getDefaultOrNoRounder(IProperties properties) {
+    Rounder candidate = getDefaultOrNull(properties);
+    if (candidate == null) {
+      candidate = NoRounder.getInstance(properties);
+    }
+    return candidate;
+  }
+
+  public static Rounder getDefaultOrNull(IProperties properties) {
+    if (SignificantDigitsRounder.useSignificantDigits(properties)) {
+      return SignificantDigitsRounder.getInstance(properties);
+    } else if (IncrementRounder.useRoundingIncrement(properties)) {
+      return IncrementRounder.getInstance(properties);
+    } else if (MagnitudeRounder.useFractionFormat(properties)) {
+      return MagnitudeRounder.getInstance(properties);
+    } else {
+      return null;
+    }
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/ScientificFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/ScientificFormat.java
new file mode 100644 (file)
index 0000000..4f67b4e
--- /dev/null
@@ -0,0 +1,233 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.FormatQuantitySelector;
+import com.ibm.icu.impl.number.ModifierHolder;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.impl.number.Rounder;
+import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier;
+import com.ibm.icu.impl.number.modifiers.PositiveNegativeAffixModifier;
+import com.ibm.icu.impl.number.rounders.IncrementRounder;
+import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.NumberFormat;
+
+public class ScientificFormat extends Format.BeforeFormat implements Rounder.MultiplierGenerator {
+
+  public static interface IProperties
+      extends RoundingFormat.IProperties, CurrencyFormat.IProperties {
+
+    static boolean DEFAULT_EXPONENT_SIGN_ALWAYS_SHOWN = false;
+
+    /** @see #setExponentSignAlwaysShown */
+    public boolean getExponentSignAlwaysShown();
+
+    /**
+     * Sets whether to show the plus sign in the exponent part of numbers with a zero or positive
+     * exponent. For example, the number "1200" with the pattern "0.0E0" would be formatted as
+     * "1.2E+3" instead of "1.2E3" in <em>en-US</em>.
+     *
+     * @param exponentSignAlwaysShown Whether to show the plus sign in positive exponents.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setExponentSignAlwaysShown(boolean exponentSignAlwaysShown);
+
+    static int DEFAULT_MINIMUM_EXPONENT_DIGITS = -1;
+
+    /** @see #setMinimumExponentDigits */
+    public int getMinimumExponentDigits();
+
+    /**
+     * Sets the minimum number of digits to display in the exponent. For example, the number "1200"
+     * with the pattern "0.0E00", which has 2 exponent digits, would be formatted as "1.2E03" in
+     * <em>en-US</em>.
+     *
+     * @param minimumExponentDigits The minimum number of digits to display in the exponent field.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setMinimumExponentDigits(int minimumExponentDigits);
+
+    @Override
+    public IProperties clone();
+  }
+
+  public static boolean useScientificNotation(IProperties properties) {
+    return properties.getMinimumExponentDigits() != IProperties.DEFAULT_MINIMUM_EXPONENT_DIGITS;
+  }
+
+  private static final ThreadLocal<Properties> threadLocalProperties =
+      new ThreadLocal<Properties>() {
+        @Override
+        protected Properties initialValue() {
+          return new Properties();
+        }
+      };
+
+  public static ScientificFormat getInstance(DecimalFormatSymbols symbols, IProperties properties) {
+    // If significant digits or rounding interval are specified through normal means, we use those.
+    // Otherwise, we use the special significant digit rules for scientific notation.
+    Rounder rounder;
+    if (IncrementRounder.useRoundingIncrement(properties)) {
+      rounder = IncrementRounder.getInstance(properties);
+    } else if (SignificantDigitsRounder.useSignificantDigits(properties)) {
+      rounder = SignificantDigitsRounder.getInstance(properties);
+    } else {
+      Properties rprops = threadLocalProperties.get().clear();
+
+      int minInt = properties.getMinimumIntegerDigits();
+      int maxInt = properties.getMaximumIntegerDigits();
+      int minFrac = properties.getMinimumFractionDigits();
+      int maxFrac = properties.getMaximumFractionDigits();
+
+      // If currency is in use, pull information from CurrencyUsage.
+      if (CurrencyFormat.useCurrency(properties)) {
+        // Use rprops as the vehicle (it is still clean)
+        CurrencyFormat.populateCurrencyRounderProperties(rprops, symbols, properties);
+        minFrac = rprops.getMinimumFractionDigits();
+        maxFrac = rprops.getMaximumFractionDigits();
+        rprops.clear();
+      }
+
+      // TODO: Mark/Andy, take a look at this logic and see if it makes sense to you.
+      // I fiddled with the settings and fallbacks to make the unit tests pass, but I
+      // don't feel that it's the "right way" to do things.
+
+      if (minInt < 0) minInt = 0;
+      if (maxInt < minInt) maxInt = minInt;
+      if (minFrac < 0) minFrac = 0;
+      if (maxFrac < minFrac) maxFrac = minFrac;
+
+      rprops.setRoundingMode(properties.getRoundingMode());
+
+      if (minInt == 0 && maxFrac == 0) {
+        // Special case for the pattern "#E0" with no significant digits specified.
+        rprops.setMinimumSignificantDigits(1);
+        rprops.setMaximumSignificantDigits(Integer.MAX_VALUE);
+      } else if (minInt == 0 && minFrac == 0) {
+        // Special case for patterns like "#.##E0" with no significant digits specified.
+        rprops.setMinimumSignificantDigits(1);
+        rprops.setMaximumSignificantDigits(1 + maxFrac);
+      } else {
+        rprops.setMinimumSignificantDigits(minInt + minFrac);
+        rprops.setMaximumSignificantDigits(minInt + maxFrac);
+      }
+      rprops.setMinimumIntegerDigits(maxInt == 0 ? 0 : Math.max(1, minInt + minFrac - maxFrac));
+      rprops.setMaximumIntegerDigits(maxInt);
+      rprops.setMinimumFractionDigits(Math.max(0, minFrac + minInt - maxInt));
+      rprops.setMaximumFractionDigits(maxFrac);
+      rounder = SignificantDigitsRounder.getInstance(rprops);
+    }
+
+    return new ScientificFormat(symbols, properties, rounder);
+  }
+
+  public static ScientificFormat getInstance(
+      DecimalFormatSymbols symbols, IProperties properties, Rounder rounder) {
+    return new ScientificFormat(symbols, properties, rounder);
+  }
+
+  // Properties
+  private final boolean exponentShowPlusSign;
+  private final int exponentDigits;
+  private final int minInt;
+  private final int maxInt;
+  private final int interval;
+  private final Rounder rounder;
+  private final ConstantAffixModifier separatorMod;
+  private final PositiveNegativeAffixModifier signMod;
+
+  // Symbols
+  private final String[] digitStrings;
+
+  private ScientificFormat(DecimalFormatSymbols symbols, IProperties properties, Rounder rounder) {
+    exponentShowPlusSign = properties.getExponentSignAlwaysShown();
+    exponentDigits = Math.max(1, properties.getMinimumExponentDigits());
+    int _maxInt = properties.getMaximumIntegerDigits();
+    int _minInt = properties.getMinimumIntegerDigits();
+    // Special behavior:
+    if (_maxInt > 8) {
+      _maxInt = _minInt;
+    }
+    maxInt = _maxInt < 0 ? Integer.MAX_VALUE : _maxInt;
+    minInt = _minInt < 0 ? 0 : _minInt < maxInt ? _minInt : maxInt;
+    interval = Math.max(1, maxInt);
+    this.rounder = rounder;
+    digitStrings = symbols.getDigitStrings(); // makes a copy
+
+    separatorMod =
+        new ConstantAffixModifier(
+            "", symbols.getExponentSeparator(), NumberFormat.Field.EXPONENT_SYMBOL, true);
+    signMod =
+        new PositiveNegativeAffixModifier(
+            new ConstantAffixModifier(
+                "",
+                exponentShowPlusSign ? symbols.getPlusSignString() : "",
+                NumberFormat.Field.EXPONENT_SIGN,
+                true),
+            new ConstantAffixModifier(
+                "", symbols.getMinusSignString(), NumberFormat.Field.EXPONENT_SIGN, true));
+  }
+
+  private static final ThreadLocal<StringBuilder> threadLocalStringBuilder =
+      new ThreadLocal<StringBuilder>() {
+        @Override
+        protected StringBuilder initialValue() {
+          return new StringBuilder();
+        }
+      };
+
+  @Override
+  public void before(FormatQuantity input, ModifierHolder mods) {
+
+    // Treat zero as if it had magnitude 0
+    int exponent;
+    if (input.isZero()) {
+      rounder.apply(input);
+      exponent = 0;
+    } else {
+      exponent = -rounder.chooseMultiplierAndApply(input, this);
+    }
+
+    // Format the exponent part of the scientific format.
+    // Insert digits starting from the left so that append can be used.
+    // TODO: Use thread locals here.
+    FormatQuantity exponentQ = FormatQuantitySelector.from(exponent);
+    StringBuilder exponentSB = threadLocalStringBuilder.get();
+    exponentSB.setLength(0);
+    exponentQ.setIntegerFractionLength(exponentDigits, Integer.MAX_VALUE, 0, 0);
+    for (int i = exponentQ.getUpperDisplayMagnitude(); i >= 0; i--) {
+      exponentSB.append(digitStrings[exponentQ.getDigit(i)]);
+    }
+
+    // Add modifiers from the outside in.
+    mods.add(
+        new ConstantAffixModifier("", exponentSB.toString(), NumberFormat.Field.EXPONENT, true));
+    mods.add(signMod.getModifier(exponent < 0));
+    mods.add(separatorMod);
+  }
+
+  @Override
+  public int getMultiplier(int magnitude) {
+    int digitsShown = ((magnitude % interval + interval) % interval) + 1;
+    if (digitsShown < minInt) {
+      digitsShown = minInt;
+    } else if (digitsShown > maxInt) {
+      digitsShown = maxInt;
+    }
+    int retval = digitsShown - magnitude - 1;
+    return retval;
+  }
+
+  @Override
+  public void export(Properties properties) {
+    properties.setMinimumExponentDigits(exponentDigits);
+    properties.setExponentSignAlwaysShown(exponentShowPlusSign);
+
+    // Set the transformed object into the property bag.  This may result in a pattern string that
+    // uses different syntax from the original, but it will be functionally equivalent.
+    rounder.export(properties);
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/StrongAffixFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/StrongAffixFormat.java
new file mode 100644 (file)
index 0000000..c70352d
--- /dev/null
@@ -0,0 +1,48 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.formatters;
+
+import java.util.Deque;
+
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.ModifierHolder;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.Properties;
+
+// TODO: This class isn't currently being used anywhere.  Consider removing it.
+
+/** Attaches all prefixes and suffixes at this point in the render tree without bubbling up. */
+public class StrongAffixFormat extends Format implements Format.AfterFormat {
+  private final Format child;
+
+  public StrongAffixFormat(Format child) {
+    this.child = child;
+
+    if (child == null) {
+      throw new IllegalArgumentException("A child formatter is required for StrongAffixFormat");
+    }
+  }
+
+  @Override
+  public int process(
+      Deque<FormatQuantity> inputs,
+      ModifierHolder mods,
+      NumberStringBuilder string,
+      int startIndex) {
+    int length = child.process(inputs, mods, string, startIndex);
+    length += mods.applyAll(string, startIndex, startIndex + length);
+    return length;
+  }
+
+  @Override
+  public int after(
+      ModifierHolder mods, NumberStringBuilder string, int leftIndex, int rightIndex) {
+    return mods.applyAll(string, leftIndex, rightIndex);
+  }
+
+  @Override
+  public void export(Properties properties) {
+    // Nothing to do.
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/ConstantAffixModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/ConstantAffixModifier.java
new file mode 100644 (file)
index 0000000..b133bc4
--- /dev/null
@@ -0,0 +1,105 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.modifiers;
+
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.impl.number.Modifier.AffixModifier;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.text.NumberFormat.Field;
+
+/** The canonical implementation of {@link Modifier}, containing a prefix and suffix string. */
+public class ConstantAffixModifier extends Modifier.BaseModifier implements AffixModifier {
+
+  // TODO: Avoid making a new instance by default if prefix and suffix are empty
+  public static final AffixModifier EMPTY = new ConstantAffixModifier();
+
+  private final String prefix;
+  private final String suffix;
+  private final Field field;
+  private final boolean strong;
+
+  /**
+   * Constructs an instance with the given strings.
+   *
+   * <p>The arguments need to be Strings, not CharSequences, because Strings are immutable but
+   * CharSequences are not.
+   *
+   * @param prefix The prefix string.
+   * @param suffix The suffix string.
+   * @param field The field type to be associated with this modifier. Can be null.
+   * @param strong Whether this modifier should be strongly applied.
+   * @see Field
+   */
+  public ConstantAffixModifier(String prefix, String suffix, Field field, boolean strong) {
+    // Use an empty string instead of null if we are given null
+    // TODO: Consider returning a null modifier if both prefix and suffix are empty.
+    this.prefix = (prefix == null ? "" : prefix);
+    this.suffix = (suffix == null ? "" : suffix);
+    this.field = field;
+    this.strong = strong;
+  }
+
+  /**
+   * Constructs a new instance with an empty prefix, suffix, and field.
+   */
+  public ConstantAffixModifier() {
+    prefix = "";
+    suffix = "";
+    field = null;
+    strong = false;
+  }
+
+  @Override
+  public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
+    // Insert the suffix first since inserting the prefix will change the rightIndex
+    int length = output.insert(rightIndex, suffix, field);
+    length += output.insert(leftIndex, prefix, field);
+    return length;
+  }
+
+  @Override
+  public int length() {
+    return prefix.length() + suffix.length();
+  }
+
+  @Override
+  public boolean isStrong() {
+    return strong;
+  }
+
+  @Override
+  public String getPrefix() {
+    return prefix;
+  }
+
+  @Override
+  public String getSuffix() {
+    return suffix;
+  }
+
+  public boolean contentEquals(CharSequence _prefix, CharSequence _suffix) {
+    if (_prefix == null && !prefix.isEmpty()) return false;
+    if (_suffix == null && !suffix.isEmpty()) return false;
+    if (prefix.length() != _prefix.length()) return false;
+    if (suffix.length() != _suffix.length()) return false;
+    for (int i = 0; i < prefix.length(); i++) {
+      if (prefix.charAt(i) != _prefix.charAt(i)) return false;
+    }
+    for (int i = 0; i < suffix.length(); i++) {
+      if (suffix.charAt(i) != _suffix.charAt(i)) return false;
+    }
+    return true;
+  }
+
+  @Override
+  public String toString() {
+    return String.format(
+        "<ConstantAffixModifier(%d) prefix:'%s' suffix:'%s'>", length(), prefix, suffix);
+  }
+
+  @Override
+  public void export(Properties properties) {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/ConstantMultiFieldModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/ConstantMultiFieldModifier.java
new file mode 100644 (file)
index 0000000..e7ed0a6
--- /dev/null
@@ -0,0 +1,93 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.modifiers;
+
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.impl.number.Modifier.AffixModifier;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.text.NumberFormat.Field;
+
+/**
+ * An implementation of {@link Modifier} that allows for multiple types of fields in the same
+ * modifier. Constructed based on the contents of two {@link NumberStringBuilder} instances (one for
+ * the prefix, one for the suffix).
+ */
+public class ConstantMultiFieldModifier extends Modifier.BaseModifier implements AffixModifier {
+
+  // TODO: Avoid making a new instance by default if prefix and suffix are empty
+  public static final ConstantMultiFieldModifier EMPTY = new ConstantMultiFieldModifier();
+
+  private final char[] prefixChars;
+  private final char[] suffixChars;
+  private final Field[] prefixFields;
+  private final Field[] suffixFields;
+  private final String prefix;
+  private final String suffix;
+  private final boolean strong;
+
+  public ConstantMultiFieldModifier(
+      NumberStringBuilder prefix, NumberStringBuilder suffix, boolean strong) {
+    prefixChars = prefix.toCharArray();
+    suffixChars = suffix.toCharArray();
+    prefixFields = prefix.toFieldArray();
+    suffixFields = suffix.toFieldArray();
+    this.prefix = new String(prefixChars);
+    this.suffix = new String(suffixChars);
+    this.strong = strong;
+  }
+
+  private ConstantMultiFieldModifier() {
+    prefixChars = new char[0];
+    suffixChars = new char[0];
+    prefixFields = new Field[0];
+    suffixFields = new Field[0];
+    prefix = "";
+    suffix = "";
+    strong = false;
+  }
+
+  @Override
+  public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
+    // Insert the suffix first since inserting the prefix will change the rightIndex
+    int length = output.insert(rightIndex, suffixChars, suffixFields);
+    length += output.insert(leftIndex, prefixChars, prefixFields);
+    return length;
+  }
+
+  @Override
+  public int length() {
+    return prefixChars.length + suffixChars.length;
+  }
+
+  @Override
+  public boolean isStrong() {
+    return strong;
+  }
+
+  @Override
+  public String getPrefix() {
+    return prefix;
+  }
+
+  @Override
+  public String getSuffix() {
+    return suffix;
+  }
+
+  public boolean contentEquals(NumberStringBuilder prefix, NumberStringBuilder suffix) {
+    return prefix.contentEquals(prefixChars, prefixFields)
+        && suffix.contentEquals(suffixChars, suffixFields);
+  }
+
+  @Override
+  public String toString() {
+    return String.format(
+        "<ConstantMultiFieldModifier(%d) prefix:'%s' suffix:'%s'>", length(), prefix, suffix);
+  }
+
+  @Override
+  public void export(Properties properties) {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/GeneralPluralModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/GeneralPluralModifier.java
new file mode 100644 (file)
index 0000000..3dfeefa
--- /dev/null
@@ -0,0 +1,76 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.modifiers;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.impl.number.ModifierHolder;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.text.PluralRules;
+
+// TODO: Is it okay that this class is not completely immutable? Right now it is internal-only.
+// Freezable or Builder could be used if necessary.
+
+/**
+ * A basic implementation of {@link com.ibm.icu.impl.number.Modifier.PositiveNegativePluralModifier}
+ * that is built on the fly using its <code>put</code> methods.
+ */
+public class GeneralPluralModifier extends Format.BeforeFormat
+    implements Modifier.PositiveNegativePluralModifier {
+  /**
+   * A single array for modifiers. Even elements are positive; odd elements are negative. The
+   * elements 2i and 2i+1 belong to the StandardPlural with ordinal i.
+   */
+  private final Modifier[] mods;
+
+  public GeneralPluralModifier() {
+    this.mods = new Modifier[StandardPlural.COUNT * 2];
+  }
+
+  /** Adds a positive/negative-agnostic modifier for the specified plural form. */
+  public void put(StandardPlural plural, Modifier modifier) {
+    put(plural, modifier, modifier);
+  }
+
+  /** Adds a positive and a negative modifier for the specified plural form. */
+  public void put(StandardPlural plural, Modifier positive, Modifier negative) {
+    assert mods[plural.ordinal() * 2] == null;
+    assert mods[plural.ordinal() * 2 + 1] == null;
+    assert positive != null;
+    assert negative != null;
+    mods[plural.ordinal() * 2] = positive;
+    mods[plural.ordinal() * 2 + 1] = negative;
+  }
+
+  @Override
+  public Modifier getModifier(StandardPlural plural, boolean isNegative) {
+    Modifier mod = mods[plural.ordinal() * 2 + (isNegative ? 1 : 0)];
+    if (mod == null) {
+      mod = mods[StandardPlural.OTHER.ordinal()*2 + (isNegative ? 1 : 0)];
+    }
+    if (mod == null) {
+      throw new UnsupportedOperationException();
+    }
+    return mod;
+  }
+
+  @Override
+  public void before(FormatQuantity input, ModifierHolder mods, PluralRules rules) {
+    mods.add(getModifier(input.getStandardPlural(rules), input.isNegative()));
+  }
+
+  @Override
+  public void before(FormatQuantity input, ModifierHolder mods) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void export(Properties properties) {
+    // Since we can export only one affix pair, do the one for "other".
+    Modifier positive = getModifier(StandardPlural.OTHER, false);
+    Modifier negative = getModifier(StandardPlural.OTHER, true);
+    PositiveNegativeAffixModifier.exportPositiveNegative(properties, positive, negative);
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/PositiveNegativeAffixModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/PositiveNegativeAffixModifier.java
new file mode 100644 (file)
index 0000000..1384b7b
--- /dev/null
@@ -0,0 +1,53 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.modifiers;
+
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.impl.number.Modifier.AffixModifier;
+import com.ibm.icu.impl.number.ModifierHolder;
+import com.ibm.icu.impl.number.Properties;
+
+/** A class containing a positive form and a negative form of {@link ConstantAffixModifier}. */
+public class PositiveNegativeAffixModifier extends Format.BeforeFormat
+    implements Modifier.PositiveNegativeModifier {
+  private final AffixModifier positive;
+  private final AffixModifier negative;
+
+  /**
+   * Constructs an instance using the two {@link ConstantMultiFieldModifier} classes for positive
+   * and negative.
+   *
+   * @param positive The positive-form Modifier.
+   * @param negative The negative-form Modifier.
+   */
+  public PositiveNegativeAffixModifier(AffixModifier positive, AffixModifier negative) {
+    this.positive = positive;
+    this.negative = negative;
+  }
+
+  @Override
+  public Modifier getModifier(boolean isNegative) {
+    return isNegative ? negative : positive;
+  }
+
+  @Override
+  public void before(FormatQuantity input, ModifierHolder mods) {
+    Modifier mod = getModifier(input.isNegative());
+    mods.add(mod);
+  }
+
+  @Override
+  public void export(Properties properties) {
+    exportPositiveNegative(properties, positive, negative);
+  }
+
+  /** Internal method used to export a positive and negative modifier to a property bag. */
+  static void exportPositiveNegative(Properties properties, Modifier positive, Modifier negative) {
+    properties.setPositivePrefix(positive.getPrefix().isEmpty() ? null : positive.getPrefix());
+    properties.setPositiveSuffix(positive.getSuffix().isEmpty() ? null : positive.getSuffix());
+    properties.setNegativePrefix(negative.getPrefix().isEmpty() ? null : negative.getPrefix());
+    properties.setNegativeSuffix(negative.getSuffix().isEmpty() ? null : negative.getSuffix());
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/SimpleModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/SimpleModifier.java
new file mode 100644 (file)
index 0000000..23a15f4
--- /dev/null
@@ -0,0 +1,130 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.modifiers;
+
+import com.ibm.icu.impl.SimpleFormatterImpl;
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.text.NumberFormat.Field;
+
+/**
+ * The second primary implementation of {@link Modifier}, this one consuming a {@link
+ * com.ibm.icu.text.SimpleFormatter} pattern.
+ */
+public class SimpleModifier extends Modifier.BaseModifier {
+  private final String compiledPattern;
+  private final Field field;
+  private final boolean strong;
+
+  /** Creates a modifier that uses the SimpleFormatter string formats. */
+  public SimpleModifier(String compiledPattern, Field field, boolean strong) {
+    this.compiledPattern = (compiledPattern == null) ? "\u0001\u0000" : compiledPattern;
+    this.field = field;
+    this.strong = strong;
+  }
+
+  @Override
+  public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
+    return formatAsPrefixSuffix(compiledPattern, output, leftIndex, rightIndex, field);
+  }
+
+  @Override
+  public int length() {
+    // TODO: Make a separate method for computing the length only?
+    return formatAsPrefixSuffix(compiledPattern, null, -1, -1, field);
+  }
+
+  @Override
+  public boolean isStrong() {
+    return strong;
+  }
+
+  @Override
+  public String getPrefix() {
+    // TODO: Implement this when MeasureFormat is ready.
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public String getSuffix() {
+    // TODO: Implement this when MeasureFormat is ready.
+    throw new UnsupportedOperationException();
+  }
+
+  /**
+   * TODO: This belongs in SimpleFormatterImpl. The only reason I haven't moved it there yet is
+   * because DoubleSidedStringBuilder is an internal class and SimpleFormatterImpl feels like it
+   * should not depend on it.
+   *
+   * <p>Formats a value that is already stored inside the StringBuilder <code>result</code> between
+   * the indices <code>startIndex</code> and <code>endIndex</code> by inserting characters before
+   * the start index and after the end index.
+   *
+   * <p>This is well-defined only for patterns with exactly one argument.
+   *
+   * @param compiledPattern Compiled form of a pattern string.
+   * @param result The StringBuilder containing the value argument.
+   * @param startIndex The left index of the value within the string builder.
+   * @param endIndex The right index of the value within the string builder.
+   * @return The number of characters (UTF-16 code points) that were added to the StringBuilder.
+   */
+  public static int formatAsPrefixSuffix(
+      String compiledPattern,
+      NumberStringBuilder result,
+      int startIndex,
+      int endIndex,
+      Field field) {
+    assert SimpleFormatterImpl.getArgumentLimit(compiledPattern) == 1;
+    int ARG_NUM_LIMIT = 0x100;
+    int length = 0, offset = 2;
+    if (compiledPattern.charAt(1) != '\u0000') {
+      int prefixLength = compiledPattern.charAt(1) - ARG_NUM_LIMIT;
+      if (result != null) {
+        result.insert(startIndex, compiledPattern, 2, 2 + prefixLength, field);
+      }
+      length += prefixLength;
+      offset = 3 + prefixLength;
+    }
+    if (offset < compiledPattern.length()) {
+      int suffixLength = compiledPattern.charAt(offset) - ARG_NUM_LIMIT;
+      if (result != null) {
+        result.insert(
+            endIndex + length, compiledPattern, offset + 1, offset + suffixLength + 1, field);
+      }
+      length += suffixLength;
+    }
+    return length;
+  }
+
+  /** TODO: Move this to a test file somewhere, once we figure out what to do with the method. */
+  public static void testFormatAsPrefixSuffix() {
+    String[] patterns = {"{0}", "X{0}Y", "XX{0}YYY", "{0}YY", "XXXX{0}"};
+    Object[][] outputs = {{"", 0, 0}, {"abcde", 0, 0}, {"abcde", 2, 2}, {"abcde", 1, 3}};
+    String[][] expecteds = {
+      {"", "XY", "XXYYY", "YY", "XXXX"},
+      {"abcde", "XYabcde", "XXYYYabcde", "YYabcde", "XXXXabcde"},
+      {"abcde", "abXYcde", "abXXYYYcde", "abYYcde", "abXXXXcde"},
+      {"abcde", "aXbcYde", "aXXbcYYYde", "abcYYde", "aXXXXbcde"}
+    };
+    for (int i = 0; i < patterns.length; i++) {
+      for (int j = 0; j < outputs.length; j++) {
+        String pattern = patterns[i];
+        String compiledPattern =
+            SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, new StringBuilder(), 1, 1);
+        NumberStringBuilder output = new NumberStringBuilder();
+        output.append((String) outputs[j][0], null);
+        formatAsPrefixSuffix(
+            compiledPattern, output, (Integer) outputs[j][1], (Integer) outputs[j][2], null);
+        String expected = expecteds[j][i];
+        String actual = output.toString();
+        assert expected.equals(actual);
+      }
+    }
+  }
+
+  @Override
+  public void export(Properties properties) {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/IncrementRounder.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/IncrementRounder.java
new file mode 100644 (file)
index 0000000..01ba69c
--- /dev/null
@@ -0,0 +1,67 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.rounders;
+
+import java.math.BigDecimal;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.impl.number.Rounder;
+
+public class IncrementRounder extends Rounder {
+
+  public static interface IProperties extends IBasicRoundingProperties {
+
+    static BigDecimal DEFAULT_ROUNDING_INCREMENT = null;
+
+    /** @see #setRoundingIncrement */
+    public BigDecimal getRoundingIncrement();
+
+    /**
+     * Sets the increment to which to round numbers. For example, with a rounding interval of 0.05,
+     * the number 11.17 would be formatted as "11.15" in locale <em>en-US</em> with the default
+     * rounding mode.
+     *
+     * <p>You can use either a rounding increment or significant digits, but not both at the same
+     * time.
+     *
+     * <p>The rounding increment can be specified in a pattern string. For example, the pattern
+     * "#,##0.05" corresponds to a rounding interval of 0.05 with 1 minimum integer digit and a
+     * grouping size of 3.
+     *
+     * @param roundingIncrement The interval to which to round.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setRoundingIncrement(BigDecimal roundingIncrement);
+  }
+
+  public static boolean useRoundingIncrement(IProperties properties) {
+    return properties.getRoundingIncrement() != IProperties.DEFAULT_ROUNDING_INCREMENT;
+  }
+
+  private final BigDecimal roundingIncrement;
+
+  public static IncrementRounder getInstance(IProperties properties) {
+    return new IncrementRounder(properties);
+  }
+
+  private IncrementRounder(IProperties properties) {
+    super(properties);
+    if (properties.getRoundingIncrement().compareTo(BigDecimal.ZERO) <= 0) {
+      throw new IllegalArgumentException("Rounding interval must be greater than zero");
+    }
+    roundingIncrement = properties.getRoundingIncrement();
+  }
+
+  @Override
+  public void apply(FormatQuantity input) {
+    input.roundToIncrement(roundingIncrement, mathContext);
+    applyDefaults(input);
+  }
+
+  @Override
+  public void export(Properties properties) {
+    super.export(properties);
+    properties.setRoundingIncrement(roundingIncrement);
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/MagnitudeRounder.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/MagnitudeRounder.java
new file mode 100644 (file)
index 0000000..d53f966
--- /dev/null
@@ -0,0 +1,30 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.rounders;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.Rounder;
+
+public class MagnitudeRounder extends Rounder {
+
+  public static interface IProperties extends IBasicRoundingProperties {}
+
+  public static boolean useFractionFormat(IProperties properties) {
+    return properties.getMinimumFractionDigits() != IProperties.DEFAULT_MINIMUM_FRACTION_DIGITS
+        || properties.getMaximumFractionDigits() != IProperties.DEFAULT_MAXIMUM_FRACTION_DIGITS;
+  }
+
+  public static MagnitudeRounder getInstance(IBasicRoundingProperties properties) {
+    return new MagnitudeRounder(properties);
+  }
+
+  private MagnitudeRounder(IBasicRoundingProperties properties) {
+    super(properties);
+  }
+
+  @Override
+  public void apply(FormatQuantity input) {
+    input.roundToMagnitude(-maxFrac, mathContext);
+    applyDefaults(input);
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/NoRounder.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/NoRounder.java
new file mode 100644 (file)
index 0000000..814e11e
--- /dev/null
@@ -0,0 +1,24 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.rounders;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.Rounder;
+
+/** Sets the integer and fraction length based on the properties, but does not perform rounding. */
+public final class NoRounder extends Rounder {
+
+  public static NoRounder getInstance(IBasicRoundingProperties properties) {
+    return new NoRounder(properties);
+  }
+
+  private NoRounder(IBasicRoundingProperties properties) {
+    super(properties);
+  }
+
+  @Override
+  public void apply(FormatQuantity input) {
+    applyDefaults(input);
+    input.roundToInfinity();
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/SignificantDigitsRounder.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/SignificantDigitsRounder.java
new file mode 100644 (file)
index 0000000..e11bf46
--- /dev/null
@@ -0,0 +1,223 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.impl.number.rounders;
+
+import java.math.RoundingMode;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.impl.number.Rounder;
+
+public class SignificantDigitsRounder extends Rounder {
+
+  /**
+   * Sets whether the minimum significant digits should override the maximum integer and fraction
+   * digits. This affects both display and rounding. Default is true.
+   *
+   * <p>For example, if this option is enabled, formatting the number 4.567 with 3 min/max
+   * significant digits against the pattern "0.0" (1 min/max fraction digits) will result in "4.57"
+   * in locale <em>en-US</em> with the default rounding mode. If this option is disabled, the max
+   * fraction digits take priority instead, and the output will be "4.6".
+   *
+   * @param significantDigitsOverride true to ensure that the minimum significant digits are always
+   *     shown; false to ensure that the maximum integer and fraction digits are obeyed.
+   * @return The property bag, for chaining.
+   */
+  public static enum SignificantDigitsMode {
+    OVERRIDE_MAXIMUM_FRACTION,
+    RESPECT_MAXIMUM_FRACTION,
+    ENSURE_MINIMUM_SIGNIFICANT
+  };
+
+  public static interface IProperties extends IBasicRoundingProperties {
+
+    static int DEFAULT_MINIMUM_SIGNIFICANT_DIGITS = -1;
+
+    /** @see #setMinimumSignificantDigits */
+    public int getMinimumSignificantDigits();
+
+    /**
+     * Sets the minimum number of significant digits to display. If, after rounding to the number of
+     * significant digits specified by {@link #setMaximumSignificantDigits}, the number of remaining
+     * significant digits is less than the minimum, the number will be padded with zeros. For
+     * example, if minimum significant digits is 3, the number 5.8 will be formatted as "5.80" in
+     * locale <em>en-US</em>. Note that minimum significant digits is relevant only when numbers
+     * have digits after the decimal point.
+     *
+     * <p>If both minimum significant digits and minimum integer/fraction digits are set at the same
+     * time, both values will be respected, and the one that results in the greater number of
+     * padding zeros will be used. For example, formatting the number 73 with 3 minimum significant
+     * digits and 2 minimum fraction digits will produce "73.00".
+     *
+     * <p>The number of significant digits can be specified in a pattern string using the '@'
+     * character. For example, the pattern "@@#" corresponds to a minimum of 2 and a maximum of 3
+     * significant digits.
+     *
+     * @param minimumSignificantDigits The minimum number of significant digits to display.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setMinimumSignificantDigits(int minimumSignificantDigits);
+
+    static int DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS = -1;
+
+    /** @see #setMaximumSignificantDigits */
+    public int getMaximumSignificantDigits();
+
+    /**
+     * Sets the maximum number of significant digits to display. The number of significant digits is
+     * equal to the number of digits counted from the leftmost nonzero digit through the rightmost
+     * nonzero digit; for example, the number "2010" has 3 significant digits. If the number has
+     * more significant digits than specified here, the extra significant digits will be rounded off
+     * using the rounding mode specified by {@link #setRoundingMode(RoundingMode)}. For example, if
+     * maximum significant digits is 3, the number 1234.56 will be formatted as "1230" in locale
+     * <em>en-US</em> with the default rounding mode.
+     *
+     * <p>If both maximum significant digits and maximum integer/fraction digits are set at the same
+     * time, the behavior is undefined.
+     *
+     * <p>The number of significant digits can be specified in a pattern string using the '@'
+     * character. For example, the pattern "@@#" corresponds to a minimum of 2 and a maximum of 3
+     * significant digits.
+     *
+     * @param maximumSignificantDigits The maximum number of significant digits to display.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setMaximumSignificantDigits(int maximumSignificantDigits);
+
+    static SignificantDigitsMode DEFAULT_SIGNIFICANT_DIGITS_MODE = null;
+
+    /** @see #setSignificantDigitsMode */
+    public SignificantDigitsMode getSignificantDigitsMode();
+
+    /**
+     * Sets the strategy used when reconciling significant digits versus integer and fraction
+     * lengths.
+     *
+     * @param significantDigitsMode One of the options from {@link SignificantDigitsMode}.
+     * @return The property bag, for chaining.
+     */
+    public IProperties setSignificantDigitsMode(SignificantDigitsMode significantDigitsMode);
+  }
+
+  public static boolean useSignificantDigits(IProperties properties) {
+    return properties.getMinimumSignificantDigits()
+            != IProperties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS
+        || properties.getMaximumSignificantDigits()
+            != IProperties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS
+        || properties.getSignificantDigitsMode() != IProperties.DEFAULT_SIGNIFICANT_DIGITS_MODE;
+  }
+
+  public static SignificantDigitsRounder getInstance(IProperties properties) {
+    return new SignificantDigitsRounder(properties);
+  }
+
+  private final int minSig;
+  private final int maxSig;
+  private final SignificantDigitsMode mode;
+
+  private SignificantDigitsRounder(IProperties properties) {
+    super(properties);
+    int _minSig = properties.getMinimumSignificantDigits();
+    int _maxSig = properties.getMaximumSignificantDigits();
+    minSig = _minSig < 1 ? 1 : _minSig > 1000 ? 1000 : _minSig;
+    maxSig = _maxSig < 0 ? 1000 : _maxSig < minSig ? minSig : _maxSig > 1000 ? 1000 : _maxSig;
+    SignificantDigitsMode _mode = properties.getSignificantDigitsMode();
+    mode = _mode == null ? SignificantDigitsMode.OVERRIDE_MAXIMUM_FRACTION : _mode;
+  }
+
+  @Override
+  public void apply(FormatQuantity input) {
+
+    int magnitude, effectiveMag, magMinSig, magMaxSig;
+
+    if (input.isZero()) {
+      // Treat zero as if magnitude corresponded to the minimum number of zeros
+      magnitude = minInt - 1;
+    } else {
+      magnitude = input.getMagnitude();
+    }
+    effectiveMag = Math.min(magnitude + 1, maxInt);
+    magMinSig = effectiveMag - minSig;
+    magMaxSig = effectiveMag - maxSig;
+
+    // Step 1: pick the rounding magnitude and apply.
+    int roundingMagnitude;
+    switch (mode) {
+      case OVERRIDE_MAXIMUM_FRACTION:
+        // Always round to maxSig.
+        // Of the six possible orders:
+        //    Case 1: minSig, maxSig, minFrac, maxFrac -- maxSig wins
+        //    Case 2: minSig, minFrac, maxSig, maxFrac -- maxSig wins
+        //    Case 3: minSig, minFrac, maxFrac, maxSig -- maxSig wins
+        //    Case 4: minFrac, minSig, maxSig, maxFrac -- maxSig wins
+        //    Case 5: minFrac, minSig, maxFrac, maxSig -- maxSig wins
+        //    Case 6: minFrac, maxFrac, minSig, maxSig -- maxSig wins
+        roundingMagnitude = magMaxSig;
+        break;
+      case RESPECT_MAXIMUM_FRACTION:
+        // Round to the strongest of maxFrac, maxInt, and maxSig.
+        // Of the six possible orders:
+        //    Case 1: minSig, maxSig, minFrac, maxFrac -- maxSig wins
+        //    Case 2: minSig, minFrac, maxSig, maxFrac -- maxSig wins
+        //    Case 3: minSig, minFrac, maxFrac, maxSig -- maxFrac wins --> differs from default
+        //    Case 4: minFrac, minSig, maxSig, maxFrac -- maxSig wins
+        //    Case 5: minFrac, minSig, maxFrac, maxSig -- maxFrac wins --> differs from default
+        //    Case 6: minFrac, maxFrac, minSig, maxSig -- maxFrac wins --> differs from default
+        //
+        // Math.max() picks the rounding magnitude farthest to the left (most significant).
+        // Math.min() picks the rounding magnitude farthest to the right (least significant).
+        roundingMagnitude = Math.max(-maxFrac, magMaxSig);
+        break;
+      case ENSURE_MINIMUM_SIGNIFICANT:
+        // Round to the strongest of maxFrac and maxSig, and always ensure minSig.
+        // Of the six possible orders:
+        //    Case 1: minSig, maxSig, minFrac, maxFrac -- maxSig wins
+        //    Case 2: minSig, minFrac, maxSig, maxFrac -- maxSig wins
+        //    Case 3: minSig, minFrac, maxFrac, maxSig -- maxFrac wins --> differs from default
+        //    Case 4: minFrac, minSig, maxSig, maxFrac -- maxSig wins
+        //    Case 5: minFrac, minSig, maxFrac, maxSig -- maxFrac wins --> differs from default
+        //    Case 6: minFrac, maxFrac, minSig, maxSig -- minSig wins --> differs from default
+        roundingMagnitude = Math.min(magMinSig, Math.max(-maxFrac, magMaxSig));
+        break;
+      default:
+        throw new AssertionError();
+    }
+    input.roundToMagnitude(roundingMagnitude, mathContext);
+
+    // In case magnitude changed:
+    if (input.isZero()) {
+      magnitude = minInt - 1;
+    } else {
+      magnitude = input.getMagnitude();
+    }
+    effectiveMag = Math.min(magnitude + 1, maxInt);
+    magMinSig = effectiveMag - minSig;
+    magMaxSig = effectiveMag - maxSig;
+
+    // Step 2: pick the number of visible digits.
+    switch (mode) {
+      case OVERRIDE_MAXIMUM_FRACTION:
+        // Ensure minSig is always displayed.
+        input.setIntegerFractionLength(
+            minInt, maxInt, Math.max(minFrac, -magMinSig), Integer.MAX_VALUE);
+        break;
+      case RESPECT_MAXIMUM_FRACTION:
+        // Ensure minSig is displayed, unless doing so is in violation of maxFrac.
+        input.setIntegerFractionLength(
+            minInt, maxInt, Math.min(maxFrac, Math.max(minFrac, -magMinSig)), maxFrac);
+        break;
+      case ENSURE_MINIMUM_SIGNIFICANT:
+        // Follow minInt/minFrac, but ensure all digits are allowed to be visible.
+        input.setIntegerFractionLength(minInt, maxInt, minFrac, Integer.MAX_VALUE);
+        break;
+    }
+  }
+
+  @Override
+  public void export(Properties properties) {
+    super.export(properties);
+    properties.setMinimumSignificantDigits(minSig);
+    properties.setMaximumSignificantDigits(maxSig);
+    properties.setSignificantDigitsMode(mode);
+  }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalDataCache.java b/icu4j/main/classes/core/src/com/ibm/icu/text/CompactDecimalDataCache.java
deleted file mode 100644 (file)
index 8d302a5..0000000
+++ /dev/null
@@ -1,524 +0,0 @@
-// © 2016 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html#License
-/*
- *******************************************************************************
- * Copyright (C) 2012-2016, International Business Machines Corporation and
- * others. All Rights Reserved.
- *******************************************************************************
- */
-package com.ibm.icu.text;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.MissingResourceException;
-
-import com.ibm.icu.impl.ICUCache;
-import com.ibm.icu.impl.ICUData;
-import com.ibm.icu.impl.ICUResourceBundle;
-import com.ibm.icu.impl.SimpleCache;
-import com.ibm.icu.impl.UResource;
-import com.ibm.icu.text.DecimalFormat.Unit;
-import com.ibm.icu.util.ULocale;
-import com.ibm.icu.util.UResourceBundle;
-
-/**
- * A cache containing data by locale for {@link CompactDecimalFormat}
- *
- * @author Travis Keep
- */
-class CompactDecimalDataCache {
-
-    private static final String SHORT_STYLE = "short";
-    private static final String LONG_STYLE = "long";
-    private static final String SHORT_CURRENCY_STYLE = "shortCurrency";
-    private static final String NUMBER_ELEMENTS = "NumberElements";
-    private static final String PATTERNS_LONG = "patternsLong";
-    private static final String PATTERNS_SHORT = "patternsShort";
-    private static final String DECIMAL_FORMAT = "decimalFormat";
-    private static final String CURRENCY_FORMAT = "currencyFormat";
-    private static final String LATIN_NUMBERING_SYSTEM = "latn";
-
-    private static enum PatternsTableKey { PATTERNS_LONG, PATTERNS_SHORT };
-    private static enum FormatsTableKey { DECIMAL_FORMAT, CURRENCY_FORMAT };
-
-    public static final String OTHER = "other";
-
-    /**
-     * We can specify prefixes or suffixes for values with up to 15 digits,
-     * less than 10^15.
-     */
-    static final int MAX_DIGITS = 15;
-
-    private final ICUCache<ULocale, DataBundle> cache =
-            new SimpleCache<ULocale, DataBundle>();
-
-    /**
-     * Data contains the compact decimal data for a particular locale. Data consists
-     * of one array and two hashmaps. The index of the divisors array as well
-     * as the arrays stored in the values of the two hashmaps correspond
-     * to log10 of the number being formatted, so when formatting 12,345, the 4th
-     * index of the arrays should be used. Divisors contain the number to divide
-     * by before doing formatting. In the case of english, <code>divisors[4]</code>
-     * is 1000.  So to format 12,345, divide by 1000 to get 12. Then use
-     * PluralRules with the current locale to figure out which of the 6 plural variants
-     * 12 matches: "zero", "one", "two", "few", "many", or "other." Prefixes and
-     * suffixes are maps whose key is the plural variant and whose values are
-     * arrays of strings with indexes corresponding to log10 of the original number.
-     * these arrays contain the prefix or suffix to use.
-     *
-     * Each array in data is 15 in length, and every index is filled.
-     *
-     * @author Travis Keep
-     *
-     */
-    static class Data {
-        long[] divisors;
-        Map<String, DecimalFormat.Unit[]> units;
-        boolean fromFallback;
-
-        Data(long[] divisors, Map<String, DecimalFormat.Unit[]> units)
-        {
-            this.divisors = divisors;
-            this.units = units;
-        }
-
-        public boolean isEmpty() {
-            return units == null || units.isEmpty();
-        }
-    }
-
-    /**
-     * DataBundle contains compact decimal data for all the styles in a particular
-     * locale. Currently available styles are short and long for decimals, and
-     * short only for currencies.
-     *
-     * @author Travis Keep
-     */
-    static class DataBundle {
-        Data shortData;
-        Data longData;
-        Data shortCurrencyData;
-
-        private DataBundle(Data shortData, Data longData, Data shortCurrencyData) {
-            this.shortData = shortData;
-            this.longData = longData;
-            this.shortCurrencyData = shortCurrencyData;
-        }
-
-        private static DataBundle createEmpty() {
-            return new DataBundle(
-                new Data(new long[MAX_DIGITS], new HashMap<String, DecimalFormat.Unit[]>()),
-                new Data(new long[MAX_DIGITS], new HashMap<String, DecimalFormat.Unit[]>()),
-                new Data(new long[MAX_DIGITS], new HashMap<String, DecimalFormat.Unit[]>())
-            );
-        }
-    }
-
-    /**
-     * Sink for enumerating all of the compact decimal format patterns.
-     *
-     * More specific bundles (en_GB) are enumerated before their parents (en_001, en, root):
-     * Only store a value if it is still missing, that is, it has not been overridden.
-     */
-    private static final class CompactDecimalDataSink extends UResource.Sink {
-
-        private DataBundle dataBundle; // Where to save values when they are read
-        private ULocale locale; // The locale we are traversing (for exception messages)
-        private boolean isLatin; // Whether or not we are traversing the Latin table
-        private boolean isFallback; // Whether or not we are traversing the Latin table as fallback
-
-        /*
-         * NumberElements{              <-- top (numbering system table)
-         *  latn{                       <-- patternsTable (one per numbering system)
-         *    patternsLong{             <-- formatsTable (one per pattern)
-         *      decimalFormat{          <-- powersOfTenTable (one per format)
-         *        1000{                 <-- pluralVariantsTable (one per power of ten)
-         *          one{"0 thousand"}   <-- plural variant and template
-         */
-
-        public CompactDecimalDataSink(DataBundle dataBundle, ULocale locale) {
-            this.dataBundle = dataBundle;
-            this.locale = locale;
-        }
-
-        @Override
-        public void put(UResource.Key key, UResource.Value value, boolean isRoot) {
-            // SPECIAL CASE: Don't consume root in the non-Latin numbering system
-            if (isRoot && !isLatin) { return; }
-
-            UResource.Table patternsTable = value.getTable();
-            for (int i1 = 0; patternsTable.getKeyAndValue(i1, key, value); ++i1) {
-
-                // patterns table: check for patternsShort or patternsLong
-                PatternsTableKey patternsTableKey;
-                if (key.contentEquals(PATTERNS_SHORT)) {
-                    patternsTableKey = PatternsTableKey.PATTERNS_SHORT;
-                } else if (key.contentEquals(PATTERNS_LONG)) {
-                    patternsTableKey = PatternsTableKey.PATTERNS_LONG;
-                } else {
-                    continue;
-                }
-
-                // traverse into the table of formats
-                UResource.Table formatsTable = value.getTable();
-                for (int i2 = 0; formatsTable.getKeyAndValue(i2, key, value); ++i2) {
-
-                    // formats table: check for decimalFormat or currencyFormat
-                    FormatsTableKey formatsTableKey;
-                    if (key.contentEquals(DECIMAL_FORMAT)) {
-                        formatsTableKey = FormatsTableKey.DECIMAL_FORMAT;
-                    } else if (key.contentEquals(CURRENCY_FORMAT)) {
-                        formatsTableKey = FormatsTableKey.CURRENCY_FORMAT;
-                    } else {
-                        continue;
-                    }
-
-                    // Set the current style and destination based on the lvl1 and lvl2 keys
-                    String style = null;
-                    Data destination = null;
-                    if (patternsTableKey == PatternsTableKey.PATTERNS_LONG
-                            && formatsTableKey == FormatsTableKey.DECIMAL_FORMAT) {
-                        style = LONG_STYLE;
-                        destination = dataBundle.longData;
-                    } else if (patternsTableKey == PatternsTableKey.PATTERNS_SHORT
-                            && formatsTableKey == FormatsTableKey.DECIMAL_FORMAT) {
-                        style = SHORT_STYLE;
-                        destination = dataBundle.shortData;
-                    } else if (patternsTableKey == PatternsTableKey.PATTERNS_SHORT
-                            && formatsTableKey == FormatsTableKey.CURRENCY_FORMAT) {
-                        style = SHORT_CURRENCY_STYLE;
-                        destination = dataBundle.shortCurrencyData;
-                    } else {
-                        // Silently ignore this case
-                        continue;
-                    }
-
-                    // SPECIAL CASE: RULES FOR WHETHER OR NOT TO CONSUME THIS TABLE:
-                    //   1) Don't consume longData if shortData was consumed from the non-Latin
-                    //      locale numbering system
-                    //   2) Don't consume longData for the first time if this is the root bundle and
-                    //      shortData is already populated from a more specific locale. Note that if
-                    //      both longData and shortData are both only in root, longData will be
-                    //      consumed since it is alphabetically before shortData in the bundle.
-                    if (isFallback
-                            && style == LONG_STYLE
-                            && !dataBundle.shortData.isEmpty()
-                            && !dataBundle.shortData.fromFallback) {
-                        continue;
-                    }
-                    if (isRoot
-                            && style == LONG_STYLE
-                            && dataBundle.longData.isEmpty()
-                            && !dataBundle.shortData.isEmpty()) {
-                        continue;
-                    }
-
-                    // Set the "fromFallback" flag on the data object
-                    destination.fromFallback = isFallback;
-
-                    // traverse into the table of powers of ten
-                    UResource.Table powersOfTenTable = value.getTable();
-                    for (int i3 = 0; powersOfTenTable.getKeyAndValue(i3, key, value); ++i3) {
-
-                        // This value will always be some even power of 10. e.g 10000.
-                        long power10 = Long.parseLong(key.toString());
-                        int log10Value = (int) Math.log10(power10);
-
-                        // Silently ignore divisors that are too big.
-                        if (log10Value >= MAX_DIGITS) continue;
-
-                        // Iterate over the plural variants ("one", "other", etc)
-                        UResource.Table pluralVariantsTable = value.getTable();
-                        for (int i4 = 0; pluralVariantsTable.getKeyAndValue(i4, key, value); ++i4) {
-                            // TODO: Use StandardPlural rather than String.
-                            String pluralVariant = key.toString();
-                            String template = value.toString();
-
-                            // Copy the data into the in-memory data bundle (do not overwrite
-                            // existing values)
-                            int numZeros = populatePrefixSuffix(
-                                    pluralVariant, log10Value, template, locale, style, destination, false);
-
-                            // If populatePrefixSuffix returns -1, it means that this key has been
-                            // encountered already.
-                            if (numZeros < 0) {
-                                continue;
-                            }
-
-                            // Set the divisor, which is based on the number of zeros in the template
-                            // string.  If the divisor from here is different from the one previously
-                            // stored, it means that the number of zeros in different plural variants
-                            // differs; throw an exception.
-                            long divisor = calculateDivisor(power10, numZeros);
-                            if (destination.divisors[log10Value] != 0L
-                                    && destination.divisors[log10Value] != divisor) {
-                                throw new IllegalArgumentException("Plural variant '" + pluralVariant
-                                        + "' template '" + template
-                                        + "' for 10^" + log10Value
-                                        + " has wrong number of zeros in " + localeAndStyle(locale, style));
-                            }
-                            destination.divisors[log10Value] = divisor;
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Fetch data for a particular locale. Clients must not modify any part of the returned data. Portions of returned
-     * data may be shared so modifying it will have unpredictable results.
-     */
-    DataBundle get(ULocale locale) {
-        DataBundle result = cache.get(locale);
-        if (result == null) {
-            result = load(locale);
-            cache.put(locale, result);
-        }
-        return result;
-    }
-
-    private static DataBundle load(ULocale ulocale) throws MissingResourceException {
-        DataBundle dataBundle = DataBundle.createEmpty();
-        String nsName = NumberingSystem.getInstance(ulocale).getName();
-        ICUResourceBundle r = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME,
-                ulocale);
-        CompactDecimalDataSink sink = new CompactDecimalDataSink(dataBundle, ulocale);
-        sink.isFallback = false;
-
-        // First load the number elements data from nsName if nsName is not Latin.
-        if (!nsName.equals(LATIN_NUMBERING_SYSTEM)) {
-            sink.isLatin = false;
-
-            try {
-                r.getAllItemsWithFallback(NUMBER_ELEMENTS + "/" + nsName, sink);
-            } catch (MissingResourceException e) {
-                // Silently ignore and use Latin
-            }
-
-            // Set the "isFallback" flag for when we read Latin
-            sink.isFallback = true;
-        }
-
-        // Now load Latin, which will fill in things that were left out from above.
-        sink.isLatin = true;
-        r.getAllItemsWithFallback(NUMBER_ELEMENTS + "/" + LATIN_NUMBERING_SYSTEM, sink);
-
-        // If longData is empty, default it to be equal to shortData
-        if (dataBundle.longData.isEmpty()) {
-            dataBundle.longData = dataBundle.shortData;
-        }
-
-        // Check for "other" variants in each of the three data classes
-        checkForOtherVariants(dataBundle.longData, ulocale, LONG_STYLE);
-        checkForOtherVariants(dataBundle.shortData, ulocale, SHORT_STYLE);
-        checkForOtherVariants(dataBundle.shortCurrencyData, ulocale, SHORT_CURRENCY_STYLE);
-
-        // Resolve missing elements
-        fillInMissing(dataBundle.longData);
-        fillInMissing(dataBundle.shortData);
-        fillInMissing(dataBundle.shortCurrencyData);
-
-        // Return the data bundle
-        return dataBundle;
-    }
-
-
-    /**
-     * Populates prefix and suffix information for a particular plural variant
-     * and index (log10 value).
-     * @param pluralVariant e.g "one", "other"
-     * @param idx the index (log10 value of the number) 0 <= idx < MAX_DIGITS
-     * @param template e.g "00K"
-     * @param locale the locale
-     * @param style the style
-     * @param destination Extracted prefix and suffix stored here.
-     * @return number of zeros found before any decimal point in template, or -1 if it was not saved.
-     */
-    private static int populatePrefixSuffix(
-            String pluralVariant, int idx, String template, ULocale locale, String style,
-            Data destination, boolean overwrite) {
-        int firstIdx = template.indexOf("0");
-        int lastIdx = template.lastIndexOf("0");
-        if (firstIdx == -1) {
-            throw new IllegalArgumentException(
-                "Expect at least one zero in template '" + template +
-                "' for variant '" +pluralVariant + "' for 10^" + idx +
-                " in " + localeAndStyle(locale, style));
-        }
-        String prefix = template.substring(0, firstIdx);
-        String suffix = template.substring(lastIdx + 1);
-
-        // Save the unit, and return -1 if it was not saved
-        boolean saved = saveUnit(new DecimalFormat.Unit(prefix, suffix), pluralVariant, idx, destination.units, overwrite);
-        if (!saved) {
-            return -1;
-        }
-
-        // If there is effectively no prefix or suffix, ignore the actual
-        // number of 0's and act as if the number of 0's matches the size
-        // of the number
-        if (prefix.trim().length() == 0 && suffix.trim().length() == 0) {
-          return idx + 1;
-        }
-
-        // Calculate number of zeros before decimal point.
-        int i = firstIdx + 1;
-        while (i <= lastIdx && template.charAt(i) == '0') {
-            i++;
-        }
-        return i - firstIdx;
-    }
-
-    /**
-     * Calculate a divisor based on the magnitude and number of zeros in the
-     * template string.
-     * @param power10
-     * @param numZeros
-     * @return
-     */
-    private static long calculateDivisor(long power10, int numZeros) {
-        // We craft our divisor such that when we divide by it, we get a
-        // number with the same number of digits as zeros found in the
-        // plural variant templates. If our magnitude is 10000 and we have
-        // two 0's in our plural variants, then we want a divisor of 1000.
-        // Note that if we have 43560 which is of same magnitude as 10000.
-        // When we divide by 1000 we a quotient which rounds to 44 (2 digits)
-        long divisor = power10;
-        for (int i = 1; i < numZeros; i++) {
-            divisor /= 10;
-        }
-        return divisor;
-    }
-
-
-    /**
-     * Returns locale and style. Used to form useful messages in thrown exceptions.
-     *
-     * Note: This is not covered by unit tests since no exceptions are thrown on the default CLDR data.  It is too
-     * cumbersome to cover via reflection.
-     *
-     * @param locale the locale
-     * @param style the style
-     */
-    private static String localeAndStyle(ULocale locale, String style) {
-        return "locale '" + locale + "' style '" + style + "'";
-    }
-
-    /**
-     * Checks to make sure that an "other" variant is present in all powers of 10.
-     * @param data
-     */
-    private static void checkForOtherVariants(Data data, ULocale locale, String style) {
-        DecimalFormat.Unit[] otherByBase = data.units.get(OTHER);
-
-        if (otherByBase == null) {
-            throw new IllegalArgumentException("No 'other' plural variants defined in "
-                    + localeAndStyle(locale, style));
-        }
-
-        // Check all other plural variants, and make sure that if any of them are populated, then
-        // other is also populated
-        for (Map.Entry<String, Unit[]> entry : data.units.entrySet()) {
-            if (entry.getKey() == OTHER) continue;
-            DecimalFormat.Unit[] variantByBase = entry.getValue();
-            for (int log10Value = 0; log10Value < MAX_DIGITS; log10Value++) {
-                if (variantByBase[log10Value] != null && otherByBase[log10Value] == null) {
-                    throw new IllegalArgumentException(
-                            "No 'other' plural variant defined for 10^" + log10Value
-                            + " but a '" + entry.getKey() + "' variant is defined"
-                            + " in " +localeAndStyle(locale, style));
-                }
-            }
-        }
-    }
-
-    /**
-     * After reading information from resource bundle into a Data object, there
-     * is guarantee that it is complete.
-     *
-     * This method fixes any incomplete data it finds within <code>result</code>.
-     * It looks at each log10 value applying the two rules.
-     *   <p>
-     *   If no prefix is defined for the "other" variant, use the divisor, prefixes and
-     *   suffixes for all defined variants from the previous log10. For log10 = 0,
-     *   use all empty prefixes and suffixes and a divisor of 1.
-     *   </p><p>
-     *   Otherwise, examine each plural variant defined for the given log10 value.
-     *   If it has no prefix and suffix for a particular variant, use the one from the
-     *   "other" variant.
-     *   </p>
-     *
-     * @param result this instance is fixed in-place.
-     */
-    private static void fillInMissing(Data result) {
-        // Initially we assume that previous divisor is 1 with no prefix or suffix.
-        long lastDivisor = 1L;
-        for (int i = 0; i < result.divisors.length; i++) {
-            if (result.units.get(OTHER)[i] == null) {
-                result.divisors[i] = lastDivisor;
-                copyFromPreviousIndex(i, result.units);
-            } else {
-                lastDivisor = result.divisors[i];
-                propagateOtherToMissing(i, result.units);
-            }
-        }
-    }
-
-    private static void propagateOtherToMissing(
-            int idx, Map<String, DecimalFormat.Unit[]> units) {
-        DecimalFormat.Unit otherVariantValue = units.get(OTHER)[idx];
-        for (DecimalFormat.Unit[] byBase : units.values()) {
-            if (byBase[idx] == null) {
-                byBase[idx] = otherVariantValue;
-            }
-        }
-    }
-
-    private static void copyFromPreviousIndex(int idx, Map<String, DecimalFormat.Unit[]> units) {
-        for (DecimalFormat.Unit[] byBase : units.values()) {
-            if (idx == 0) {
-                byBase[idx] = DecimalFormat.NULL_UNIT;
-            } else {
-                byBase[idx] = byBase[idx - 1];
-            }
-        }
-    }
-
-    private static boolean saveUnit(
-            DecimalFormat.Unit unit, String pluralVariant, int idx,
-            Map<String, DecimalFormat.Unit[]> units,
-            boolean overwrite) {
-        DecimalFormat.Unit[] byBase = units.get(pluralVariant);
-        if (byBase == null) {
-            byBase = new DecimalFormat.Unit[MAX_DIGITS];
-            units.put(pluralVariant, byBase);
-        }
-
-        // Don't overwrite a pre-existing value unless the "overwrite" flag is true.
-        if (!overwrite && byBase[idx] != null) {
-            return false;
-        }
-
-        // Save the value and return
-        byBase[idx] = unit;
-        return true;
-    }
-
-    /**
-     * Fetches a prefix or suffix given a plural variant and log10 value. If it
-     * can't find the given variant, it falls back to "other".
-     * @param prefixOrSuffix the prefix or suffix map
-     * @param variant the plural variant
-     * @param base log10 value. 0 <= base < MAX_DIGITS.
-     * @return the prefix or suffix.
-     */
-    static DecimalFormat.Unit getUnit(
-            Map<String, DecimalFormat.Unit[]> units, String variant, int base) {
-        DecimalFormat.Unit[] byBase = units.get(variant);
-        if (byBase == null) {
-            byBase = units.get(CompactDecimalDataCache.OTHER);
-        }
-        return byBase[base];
-    }
-}
index 078f10db57a523a875d9baeca8e0690f276f9086..1d034b1901c46909b65ffe8ffc1e4a505faf2998 100644 (file)
@@ -18,559 +18,228 @@ import java.math.BigInteger;
 import java.text.AttributedCharacterIterator;
 import java.text.FieldPosition;
 import java.text.ParsePosition;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashMap;
 import java.util.Locale;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.regex.Pattern;
 
-import com.ibm.icu.text.CompactDecimalDataCache.Data;
-import com.ibm.icu.text.PluralRules.FixedDecimal;
-import com.ibm.icu.util.Currency;
-import com.ibm.icu.util.CurrencyAmount;
-import com.ibm.icu.util.Output;
+import com.ibm.icu.impl.number.FormatQuantity4;
+import com.ibm.icu.impl.number.Properties;
 import com.ibm.icu.util.ULocale;
 
 /**
- * The CompactDecimalFormat produces abbreviated numbers, suitable for display in environments will limited real estate.
- * For example, 'Hits: 1.2B' instead of 'Hits: 1,200,000,000'. The format will be appropriate for the given language,
- * such as "1,2 Mrd." for German.
- * <p>
- * For numbers under 1000 trillion (under 10^15, such as 123,456,789,012,345), the result will be short for supported
- * languages. However, the result may sometimes exceed 7 characters, such as when there are combining marks or thin
- * characters. In such cases, the visual width in fonts should still be short.
- * <p>
- * By default, there are 2 significant digits. After creation, if more than three significant digits are set (with
- * setMaximumSignificantDigits), or if a fixed number of digits are set (with setMaximumIntegerDigits or
- * setMaximumFractionDigits), then result may be wider.
- * <p>
- * The "short" style is also capable of formatting currency amounts, such as "$1.2M" instead of "$1,200,000.00" (English) or
- * "5,3 Mio. €" instead of "5.300.000,00 €" (German).  Localized data concerning longer formats is not available yet in
- * the Unicode CLDR. Because of this, attempting to format a currency amount using the "long" style will produce
- * an UnsupportedOperationException.
+ * The CompactDecimalFormat produces abbreviated numbers, suitable for display in environments will
+ * limited real estate. For example, 'Hits: 1.2B' instead of 'Hits: 1,200,000,000'. The format will
+ * be appropriate for the given language, such as "1,2 Mrd." for German.
  *
- * At this time, negative numbers and parsing are not supported, and will produce an UnsupportedOperationException.
- * Resetting the pattern prefixes or suffixes is not supported; the method calls are ignored.
- * <p>
- * Note that important methods, like setting the number of decimals, will be moved up from DecimalFormat to
- * NumberFormat.
+ * <p>For numbers under 1000 trillion (under 10^15, such as 123,456,789,012,345), the result will be
+ * short for supported languages. However, the result may sometimes exceed 7 characters, such as
+ * when there are combining marks or thin characters. In such cases, the visual width in fonts
+ * should still be short.
+ *
+ * <p>By default, there are 2 significant digits. After creation, if more than three significant
+ * digits are set (with setMaximumSignificantDigits), or if a fixed number of digits are set (with
+ * setMaximumIntegerDigits or setMaximumFractionDigits), then result may be wider.
+ *
+ * <p>The "short" style is also capable of formatting currency amounts, such as "$1.2M" instead of
+ * "$1,200,000.00" (English) or "5,3 Mio. €" instead of "5.300.000,00 €" (German). Localized data
+ * concerning longer formats is not available yet in the Unicode CLDR. Because of this, attempting
+ * to format a currency amount using the "long" style will produce an UnsupportedOperationException.
+ *
+ * <p>At this time, negative numbers and parsing are not supported, and will produce an
+ * UnsupportedOperationException. Resetting the pattern prefixes or suffixes is not supported; the
+ * method calls are ignored.
+ *
+ * <p>Note that important methods, like setting the number of decimals, will be moved up from
+ * DecimalFormat to NumberFormat.
  *
  * @author markdavis
  * @stable ICU 49
  */
 public class CompactDecimalFormat extends DecimalFormat {
 
-    private static final long serialVersionUID = 4716293295276629682L;
-
-//    private static final int POSITIVE_PREFIX = 0, POSITIVE_SUFFIX = 1, AFFIX_SIZE = 2;
-    private static final CompactDecimalDataCache cache = new CompactDecimalDataCache();
-
-    private final Map<String, DecimalFormat.Unit[]> units;
-    private final Map<String, DecimalFormat.Unit[]> currencyUnits;
-    private final long[] divisor;
-    private final long[] currencyDivisor;
-    private final Map<String, Unit> pluralToCurrencyAffixes;
-    private CompactStyle style;
-
-    // null if created internally using explicit prefixes and suffixes.
-    private final PluralRules pluralRules;
-
-    /**
-     * Style parameter for CompactDecimalFormat.
-     * @stable ICU 50
-     */
-    public enum CompactStyle {
-        /**
-         * Short version, like "1.2T"
-         * @stable ICU 50
-         */
-        SHORT,
-        /**
-         * Longer version, like "1.2 trillion", if available. May return same result as SHORT if not.
-         * @stable ICU 50
-         */
-        LONG
-    }
-
-    /**
-     * Create a CompactDecimalFormat appropriate for a locale. The result may
-     * be affected by the number system in the locale, such as ar-u-nu-latn.
-     *
-     * @param locale the desired locale
-     * @param style the compact style
-     * @stable ICU 50
-     */
-    public static CompactDecimalFormat getInstance(ULocale locale, CompactStyle style) {
-        return new CompactDecimalFormat(locale, style);
-    }
-
+  /**
+   * Style parameter for CompactDecimalFormat.
+   *
+   * @stable ICU 50
+   */
+  public enum CompactStyle {
     /**
-     * Create a CompactDecimalFormat appropriate for a locale. The result may
-     * be affected by the number system in the locale, such as ar-u-nu-latn.
+     * Short version, like "1.2T"
      *
-     * @param locale the desired locale
-     * @param style the compact style
      * @stable ICU 50
      */
-    public static CompactDecimalFormat getInstance(Locale locale, CompactStyle style) {
-        return new CompactDecimalFormat(ULocale.forLocale(locale), style);
-    }
-
+    SHORT,
     /**
-     * The public mechanism is CompactDecimalFormat.getInstance().
+     * Longer version, like "1.2 trillion", if available. May return same result as SHORT if not.
      *
-     * @param locale
-     *            the desired locale
-     * @param style
-     *            the compact style
-     */
-    CompactDecimalFormat(ULocale locale, CompactStyle style) {
-        this.pluralRules = PluralRules.forLocale(locale);
-        DecimalFormat format = (DecimalFormat) NumberFormat.getInstance(locale);
-        CompactDecimalDataCache.Data data = getData(locale, style);
-        CompactDecimalDataCache.Data currencyData = getCurrencyData(locale);
-        this.units = data.units;
-        this.divisor = data.divisors;
-        this.currencyUnits = currencyData.units;
-        this.currencyDivisor = currencyData.divisors;
-        this.style = style;
-        pluralToCurrencyAffixes = null;
-
-//        DecimalFormat currencyFormat = (DecimalFormat) NumberFormat.getCurrencyInstance(locale);
-//        // TODO fix to use plural-dependent affixes
-//        Unit currency = new Unit(currencyFormat.getPositivePrefix(), currencyFormat.getPositiveSuffix());
-//        pluralToCurrencyAffixes = new HashMap<String,Unit>();
-//        for (String key : pluralRules.getKeywords()) {
-//            pluralToCurrencyAffixes.put(key, currency);
-//        }
-//        // TODO fix to get right symbol for the count
-
-        finishInit(style, format.toPattern(), format.getDecimalFormatSymbols());
-    }
-
-    /**
-     * Create a short number "from scratch". Intended for internal use. The prefix, suffix, and divisor arrays are
-     * parallel, and provide the information for each power of 10. When formatting a value, the correct power of 10 is
-     * found, then the value is divided by the divisor, and the prefix and suffix are set (using
-     * setPositivePrefix/Suffix).
-     *
-     * @param pattern
-     *            A number format pattern. Note that the prefix and suffix are discarded, and the decimals are
-     *            overridden by default.
-     * @param formatSymbols
-     *            Decimal format symbols, typically from a locale.
-     * @param style
-     *            compact style.
-     * @param divisor
-     *            An array of prefix values, one for each power of 10 from 0 to 14
-     * @param pluralAffixes
-     *            A map from plural categories to affixes.
-     * @param currencyAffixes
-     *            A map from plural categories to currency affixes.
-     * @param debugCreationErrors
-     *            A collection of strings for debugging. If null on input, then any errors found will be added to that
-     *            collection instead of throwing exceptions.
-     * @internal
-     * @deprecated This API is ICU internal only.
-     */
-    @Deprecated
-    public CompactDecimalFormat(String pattern, DecimalFormatSymbols formatSymbols,
-            CompactStyle style, PluralRules pluralRules,
-            long[] divisor, Map<String,String[][]> pluralAffixes, Map<String, String[]> currencyAffixes,
-            Collection<String> debugCreationErrors) {
-
-        this.pluralRules = pluralRules;
-        this.units = otherPluralVariant(pluralAffixes, divisor, debugCreationErrors);
-        this.currencyUnits = otherPluralVariant(pluralAffixes, divisor, debugCreationErrors);
-        if (!pluralRules.getKeywords().equals(this.units.keySet())) {
-            debugCreationErrors.add("Missmatch in pluralCategories, should be: " + pluralRules.getKeywords() + ", was actually " + this.units.keySet());
-        }
-        this.divisor = divisor.clone();
-        this.currencyDivisor = divisor.clone();
-        if (currencyAffixes == null) {
-            pluralToCurrencyAffixes = null;
-        } else {
-            pluralToCurrencyAffixes = new HashMap<String,Unit>();
-            for (Entry<String, String[]> s : currencyAffixes.entrySet()) {
-                String[] pair = s.getValue();
-                pluralToCurrencyAffixes.put(s.getKey(), new Unit(pair[0], pair[1]));
-            }
-        }
-        finishInit(style, pattern, formatSymbols);
-    }
-
-    private void finishInit(CompactStyle style, String pattern, DecimalFormatSymbols formatSymbols) {
-        applyPattern(pattern);
-        setDecimalFormatSymbols(formatSymbols);
-        setMaximumSignificantDigits(2); // default significant digits
-        setSignificantDigitsUsed(true);
-        if (style == CompactStyle.SHORT) {
-            setGroupingUsed(false);
-        }
-        setCurrency(null);
-    }
-
-    /**
-     * {@inheritDoc}
-     * @stable ICU 49
-     */
-    @Override
-    public boolean equals(Object obj) {
-        if (obj == null)
-            return false;
-        if (!super.equals(obj))
-            return false; // super does class check
-        CompactDecimalFormat other = (CompactDecimalFormat) obj;
-        return mapsAreEqual(units, other.units)
-                && Arrays.equals(divisor, other.divisor)
-                && (pluralToCurrencyAffixes == other.pluralToCurrencyAffixes
-                || pluralToCurrencyAffixes != null && pluralToCurrencyAffixes.equals(other.pluralToCurrencyAffixes))
-                && pluralRules.equals(other.pluralRules);
-    }
-
-    private boolean mapsAreEqual(
-            Map<String, DecimalFormat.Unit[]> lhs, Map<String, DecimalFormat.Unit[]> rhs) {
-        if (lhs.size() != rhs.size()) {
-            return false;
-        }
-        // For each MapEntry in lhs, see if there is a matching one in rhs.
-        for (Map.Entry<String, DecimalFormat.Unit[]> entry : lhs.entrySet()) {
-            DecimalFormat.Unit[] value = rhs.get(entry.getKey());
-            if (value == null || !Arrays.equals(entry.getValue(), value)) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /**
-     * {@inheritDoc}
-     * @stable ICU 49
-     */
-    @Override
-    public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
-        return format(number, null, toAppendTo, pos);
-    }
-
-    /**
-     * {@inheritDoc}
      * @stable ICU 50
      */
-    @Override
-    public AttributedCharacterIterator formatToCharacterIterator(Object obj) {
-        if (!(obj instanceof Number)) {
-            throw new IllegalArgumentException();
-        }
-        Number number = (Number) obj;
-        Amount amount = toAmount(number.doubleValue(), null, null);
-        return super.formatToCharacterIterator(amount.getQty(), amount.getUnit());
-    }
-
-    /**
-     * {@inheritDoc}
-     * @stable ICU 49
-     */
-    @Override
-    public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) {
-        return format((double) number, toAppendTo, pos);
-    }
-
-    /**
-     * {@inheritDoc}
-     * @stable ICU 49
-     */
-    @Override
-    public StringBuffer format(BigInteger number, StringBuffer toAppendTo, FieldPosition pos) {
-        return format(number.doubleValue(), toAppendTo, pos);
-    }
-
-    /**
-     * {@inheritDoc}
-     * @stable ICU 49
-     */
-    @Override
-    public StringBuffer format(BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
-        return format(number.doubleValue(), toAppendTo, pos);
-    }
-
-    /**
-     * {@inheritDoc}
-     * @stable ICU 49
-     */
-    @Override
-    public StringBuffer format(com.ibm.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
-        return format(number.doubleValue(), toAppendTo, pos);
-    }
-    /**
-     * {@inheritDoc}
-     * @internal ICU 57 technology preview
-     * @deprecated This API might change or be removed in a future release.
-     */
-    @Override
-    @Deprecated
-    public StringBuffer format(CurrencyAmount currAmt, StringBuffer toAppendTo, FieldPosition pos) {
-        return format(currAmt.getNumber().doubleValue(), currAmt.getCurrency(), toAppendTo, pos);
-    }
-
-    /**
-     * Parsing is currently unsupported, and throws an UnsupportedOperationException.
-     * @stable ICU 49
-     */
-    @Override
-    public Number parse(String text, ParsePosition parsePosition) {
-        throw new UnsupportedOperationException();
-    }
-
-    // DISALLOW Serialization, at least while draft
-
-    private void writeObject(ObjectOutputStream out) throws IOException {
-        throw new NotSerializableException();
-    }
-
-    private void readObject(ObjectInputStream in) throws IOException {
-        throw new NotSerializableException();
-    }
-
-    /* INTERNALS */
-    private StringBuffer format(double number, Currency curr, StringBuffer toAppendTo, FieldPosition pos) {
-        if (curr != null && style == CompactStyle.LONG) {
-            throw new UnsupportedOperationException("CompactDecimalFormat does not support LONG style for currency.");
-        }
-
-        // Compute the scaled amount, prefix, and suffix appropriate for the number's magnitude.
-        Output<Unit> currencyUnit = new Output<Unit>();
-        Amount amount = toAmount(number, curr, currencyUnit);
-        Unit unit = amount.getUnit();
-
-        // Note that currencyUnit is a remnant.  In almost all cases, it will be null.
-        StringBuffer prefix = new StringBuffer();
-        StringBuffer suffix = new StringBuffer();
-        if (currencyUnit.value != null) {
-            currencyUnit.value.writePrefix(prefix);
-        }
-        unit.writePrefix(prefix);
-        unit.writeSuffix(suffix);
-        if (currencyUnit.value != null) {
-            currencyUnit.value.writeSuffix(suffix);
-        }
-
-        if (curr == null) {
-            // Prevent locking when not formatting a currency number.
-            toAppendTo.append(escape(prefix.toString()));
-            super.format(amount.getQty(), toAppendTo, pos);
-            toAppendTo.append(escape(suffix.toString()));
-
-        } else {
-            // To perform the formatting, we set this DecimalFormat's pattern to have the correct prefix, suffix,
-            // and currency, and then reset it back to what it was before.
-            // This has to be synchronized since this information is held in the state of the DecimalFormat object.
-            synchronized(this) {
-
-                String originalPattern = this.toPattern();
-                Currency originalCurrency = this.getCurrency();
-                StringBuffer newPattern = new StringBuffer();
-
-                // Write prefixes and suffixes to the pattern.  Note that we have to apply it to both halves of a
-                // positive/negative format (separated by ';')
-                int semicolonPos = originalPattern.indexOf(';');
-                newPattern.append(prefix);
-                if (semicolonPos != -1) {
-                    newPattern.append(originalPattern, 0, semicolonPos);
-                    newPattern.append(suffix);
-                    newPattern.append(';');
-                    newPattern.append(prefix);
-                }
-                newPattern.append(originalPattern, semicolonPos + 1, originalPattern.length());
-                newPattern.append(suffix);
-
-                // Overwrite the pattern and currency.
-                setCurrency(curr);
-                applyPattern(newPattern.toString());
-
-                // Actually perform the formatting.
-                super.format(amount.getQty(), toAppendTo, pos);
-
-                // Reset the pattern and currency.
-                setCurrency(originalCurrency);
-                applyPattern(originalPattern);
-            }
-        }
-        return toAppendTo;
-    }
-
-    private static final Pattern UNESCAPE_QUOTE = Pattern.compile("((?<!'))'");
-
-    private static String escape(String string) {
-        if (string.indexOf('\'') >= 0) {
-            return UNESCAPE_QUOTE.matcher(string).replaceAll("$1");
-        }
-        return string;
-    }
-
-    private Amount toAmount(double number, Currency curr, Output<Unit> currencyUnit) {
-        // We do this here so that the prefix or suffix we choose is always consistent
-        // with the rounding we do. This way, 999999 -> 1M instead of 1000K.
-        boolean negative = isNumberNegative(number);
-        number = adjustNumberAsInFormatting(number);
-        int base = number <= 1.0d ? 0 : (int) Math.log10(number);
-        if (base >= CompactDecimalDataCache.MAX_DIGITS) {
-            base = CompactDecimalDataCache.MAX_DIGITS - 1;
-        }
-        if (curr != null) {
-            number /= currencyDivisor[base];
-        } else {
-            number /= divisor[base];
-        }
-        String pluralVariant = getPluralForm(getFixedDecimal(number, toDigitList(number)));
-        if (pluralToCurrencyAffixes != null && currencyUnit != null) {
-            currencyUnit.value = pluralToCurrencyAffixes.get(pluralVariant);
-        }
-        if (negative) {
-            number = -number;
-        }
-        if ( curr != null ) {
-            return new Amount(number, CompactDecimalDataCache.getUnit(currencyUnits, pluralVariant, base));
-        } else {
-            return new Amount(number, CompactDecimalDataCache.getUnit(units, pluralVariant, base));
-        }
-    }
-
-    private void recordError(Collection<String> creationErrors, String errorMessage) {
-        if (creationErrors == null) {
-            throw new IllegalArgumentException(errorMessage);
-        }
-        creationErrors.add(errorMessage);
-    }
-
-    /**
-     * Manufacture the unit list from arrays
-     */
-    private Map<String, DecimalFormat.Unit[]> otherPluralVariant(Map<String, String[][]> pluralCategoryToPower10ToAffix,
-            long[] divisor, Collection<String> debugCreationErrors) {
-
-        // check for bad divisors
-        if (divisor.length < CompactDecimalDataCache.MAX_DIGITS) {
-            recordError(debugCreationErrors, "Must have at least " + CompactDecimalDataCache.MAX_DIGITS + " prefix items.");
-        }
-        long oldDivisor = 0;
-        for (int i = 0; i < divisor.length; ++i) {
-
-            // divisor must be a power of 10, and must be less than or equal to 10^i
-            int log = (int) Math.log10(divisor[i]);
-            if (log > i) {
-                recordError(debugCreationErrors, "Divisor[" + i + "] must be less than or equal to 10^" + i
-                        + ", but is: " + divisor[i]);
-            }
-            long roundTrip = (long) Math.pow(10.0d, log);
-            if (roundTrip != divisor[i]) {
-                recordError(debugCreationErrors, "Divisor[" + i + "] must be a power of 10, but is: " + divisor[i]);
-            }
-
-            if (divisor[i] < oldDivisor) {
-                recordError(debugCreationErrors, "Bad divisor, the divisor for 10E" + i + "(" + divisor[i]
-                        + ") is less than the divisor for the divisor for 10E" + (i - 1) + "(" + oldDivisor + ")");
-            }
-            oldDivisor = divisor[i];
-        }
-
-        Map<String, DecimalFormat.Unit[]> result = new HashMap<String, DecimalFormat.Unit[]>();
-        Map<String,Integer> seen = new HashMap<String,Integer>();
-
-        String[][] defaultPower10ToAffix = pluralCategoryToPower10ToAffix.get("other");
-
-        for (Entry<String, String[][]> pluralCategoryAndPower10ToAffix : pluralCategoryToPower10ToAffix.entrySet()) {
-            String pluralCategory = pluralCategoryAndPower10ToAffix.getKey();
-            String[][] power10ToAffix = pluralCategoryAndPower10ToAffix.getValue();
-
-            // we can't have one of the arrays be of different length
-            if (power10ToAffix.length != divisor.length) {
-                recordError(debugCreationErrors, "Prefixes & suffixes must be present for all divisors " + pluralCategory);
-            }
-            DecimalFormat.Unit[] units = new DecimalFormat.Unit[power10ToAffix.length];
-            for (int i = 0; i < power10ToAffix.length; i++) {
-                String[] pair = power10ToAffix[i];
-                if (pair == null) {
-                    pair = defaultPower10ToAffix[i];
-                }
-
-                // we can't have bad pair
-                if (pair.length != 2 || pair[0] == null || pair[1] == null) {
-                    recordError(debugCreationErrors, "Prefix or suffix is null for " + pluralCategory + ", " + i + ", " + Arrays.asList(pair));
-                    continue;
-                }
-
-                // we can't have two different indexes with the same display
-                int log = (int) Math.log10(divisor[i]);
-                String key = pair[0] + "\uFFFF" + pair[1] + "\uFFFF" + (i - log);
-                Integer old = seen.get(key);
-                if (old == null) {
-                    seen.put(key, i);
-                } else if (old != i) {
-                    recordError(debugCreationErrors, "Collision between values for " + i + " and " + old
-                            + " for [prefix/suffix/index-log(divisor)" + key.replace('\uFFFF', ';'));
-                }
-
-                units[i] = new Unit(pair[0], pair[1]);
-            }
-            result.put(pluralCategory, units);
-        }
-        return result;
-    }
-
-    private String getPluralForm(FixedDecimal fixedDecimal) {
-        if (pluralRules == null) {
-            return CompactDecimalDataCache.OTHER;
-        }
-        return pluralRules.select(fixedDecimal);
-    }
-
-    /**
-     * Gets the data for a particular locale and style. If style is unrecognized,
-     * we just return data for CompactStyle.SHORT.
-     * @param locale The locale.
-     * @param style The style.
-     * @return The data which must not be modified.
-     */
-    private Data getData(ULocale locale, CompactStyle style) {
-        CompactDecimalDataCache.DataBundle bundle = cache.get(locale);
-        switch (style) {
-        case SHORT:
-            return bundle.shortData;
-        case LONG:
-            return bundle.longData;
-        default:
-            return bundle.shortData;
-        }
-    }
-    /**
-     * Gets the currency data for a particular locale.
-     * Currently only short currency format is supported, since that is
-     * the only form in CLDR.
-     * @param locale The locale.
-     * @return The data which must not be modified.
-     */
-    private Data getCurrencyData(ULocale locale) {
-        CompactDecimalDataCache.DataBundle bundle = cache.get(locale);
-            return bundle.shortCurrencyData;
-    }
-
-    private static class Amount {
-        private final double qty;
-        private final Unit unit;
-
-        public Amount(double qty, Unit unit) {
-            this.qty = qty;
-            this.unit = unit;
-        }
-
-        public double getQty() {
-            return qty;
-        }
-
-        public Unit getUnit() {
-            return unit;
-        }
-    }
+    LONG
+  }
+
+  /**
+   * Create a CompactDecimalFormat appropriate for a locale. The result may be affected by the
+   * number system in the locale, such as ar-u-nu-latn.
+   *
+   * @param locale the desired locale
+   * @param style the compact style
+   * @stable ICU 50
+   */
+  public static CompactDecimalFormat getInstance(ULocale locale, CompactStyle style) {
+    return new CompactDecimalFormat(locale, style);
+  }
+
+  /**
+   * Create a CompactDecimalFormat appropriate for a locale. The result may be affected by the
+   * number system in the locale, such as ar-u-nu-latn.
+   *
+   * @param locale the desired locale
+   * @param style the compact style
+   * @stable ICU 50
+   */
+  public static CompactDecimalFormat getInstance(Locale locale, CompactStyle style) {
+    return new CompactDecimalFormat(ULocale.forLocale(locale), style);
+  }
+
+  /**
+   * The public mechanism is CompactDecimalFormat.getInstance().
+   *
+   * @param locale the desired locale
+   * @param style the compact style
+   */
+  CompactDecimalFormat(ULocale locale, CompactStyle style) {
+    // Use the locale's default pattern
+    String pattern = getPattern(locale, 0);
+    symbols = DecimalFormatSymbols.getInstance(locale);
+    properties = new Properties();
+    properties.setCompactStyle(style);
+    exportedProperties = new Properties();
+    setPropertiesFromPattern(pattern, true);
+    if (style == CompactStyle.SHORT) {
+      // TODO: This was setGroupingUsed(false) in ICU 58. Is it okay that I changed it for ICU 59?
+      properties.setMinimumGroupingDigits(2);
+    }
+    refreshFormatter();
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @stable ICU 49
+   */
+  @Override
+  public boolean equals(Object obj) {
+    return super.equals(obj);
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @stable ICU 49
+   */
+  @Override
+  public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
+    FormatQuantity4 fq = new FormatQuantity4(number);
+    formatter.format(fq, toAppendTo, pos);
+    fq.populateUFieldPosition(pos);
+    return toAppendTo;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @stable ICU 50
+   */
+  @Override
+  public AttributedCharacterIterator formatToCharacterIterator(Object obj) {
+    if (!(obj instanceof Number)) throw new IllegalArgumentException();
+    Number number = (Number) obj;
+    FormatQuantity4 fq = new FormatQuantity4(number);
+    AttributedCharacterIterator result = formatter.formatToCharacterIterator(fq);
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @stable ICU 49
+   */
+  @Override
+  public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) {
+    FormatQuantity4 fq = new FormatQuantity4(number);
+    formatter.format(fq, toAppendTo, pos);
+    fq.populateUFieldPosition(pos);
+    return toAppendTo;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @stable ICU 49
+   */
+  @Override
+  public StringBuffer format(BigInteger number, StringBuffer toAppendTo, FieldPosition pos) {
+    FormatQuantity4 fq = new FormatQuantity4(number);
+    formatter.format(fq, toAppendTo, pos);
+    fq.populateUFieldPosition(pos);
+    return toAppendTo;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @stable ICU 49
+   */
+  @Override
+  public StringBuffer format(BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
+    FormatQuantity4 fq = new FormatQuantity4(number);
+    formatter.format(fq, toAppendTo, pos);
+    fq.populateUFieldPosition(pos);
+    return toAppendTo;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @stable ICU 49
+   */
+  @Override
+  public StringBuffer format(
+      com.ibm.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
+    FormatQuantity4 fq = new FormatQuantity4(number.toBigDecimal());
+    formatter.format(fq, toAppendTo, pos);
+    fq.populateUFieldPosition(pos);
+    return toAppendTo;
+  }
+
+//  /**
+//   * {@inheritDoc}
+//   *
+//   * @internal ICU 57 technology preview
+//   * @deprecated This API might change or be removed in a future release.
+//   */
+//  @Override
+//  @Deprecated
+//  public StringBuffer format(CurrencyAmount currAmt, StringBuffer toAppendTo, FieldPosition pos) {
+//    // TODO(sffc)
+//    throw new UnsupportedOperationException();
+//  }
+
+  /**
+   * Parsing is currently unsupported, and throws an UnsupportedOperationException.
+   *
+   * @stable ICU 49
+   */
+  @Override
+  public Number parse(String text, ParsePosition parsePosition) {
+    throw new UnsupportedOperationException();
+  }
+
+  // DISALLOW Serialization, at least while draft
+
+  private void writeObject(ObjectOutputStream out) throws IOException {
+    throw new NotSerializableException();
+  }
+
+  private void readObject(ObjectInputStream in) throws IOException {
+    throw new NotSerializableException();
+  }
 }
index 055fafe0d0b33e2cbad21616968f4c7257f7010d..3e89cdb3f3b75d6e7a7f9330a0728ac21f1c74df 100644 (file)
@@ -148,7 +148,7 @@ public class CurrencyPluralInfo implements Cloneable, Serializable {
     }
 
     /**
-     * Set plural rules.  These are initially set in the constructor based on the locale, 
+     * Set plural rules.  These are initially set in the constructor based on the locale,
      * and usually do not need to be changed.
      *
      * @param ruleDescription new plural rule description
@@ -162,6 +162,10 @@ public class CurrencyPluralInfo implements Cloneable, Serializable {
      * Set currency plural patterns.  These are initially set in the constructor based on the
      * locale, and usually do not need to be changed.
      *
+     * The decimal digits part of the pattern cannot be specified via this method.  All plural
+     * forms will use the same decimal pattern as set in the constructor of DecimalFormat.  For
+     * example, you can't set "0.0" for plural "few" but "0.00" for plural "many".
+     *
      * @param pluralCount the plural count for which the currency pattern will
      *                    be overridden.
      * @param pattern     the new currency plural pattern
@@ -188,6 +192,7 @@ public class CurrencyPluralInfo implements Cloneable, Serializable {
      *
      * @stable ICU 4.2
      */
+    @Override
     public Object clone() {
         try {
             CurrencyPluralInfo other = (CurrencyPluralInfo) super.clone();
@@ -213,6 +218,7 @@ public class CurrencyPluralInfo implements Cloneable, Serializable {
      *
      * @stable ICU 4.2
      */
+    @Override
     public boolean equals(Object a) {
         if (a instanceof CurrencyPluralInfo) {
             CurrencyPluralInfo other = (CurrencyPluralInfo)a;
@@ -221,17 +227,19 @@ public class CurrencyPluralInfo implements Cloneable, Serializable {
         }
         return false;
     }
-    
+
     /**
-     * Mock implementation of hashCode(). This implementation always returns a constant
-     * value. When Java assertion is enabled, this method triggers an assertion failure.
+     * Override hashCode
+     *
      * @internal
      * @deprecated This API is ICU internal only.
      */
+    @Override
     @Deprecated
     public int hashCode() {
-        assert false : "hashCode not designed";
-        return 42;
+      return pluralCountToCurrencyUnitPattern.hashCode()
+          ^ pluralRules.hashCode()
+          ^ ulocale.hashCode();
     }
 
     /**
@@ -273,7 +281,7 @@ public class CurrencyPluralInfo implements Cloneable, Serializable {
 
     private void setupCurrencyPluralPattern(ULocale uloc) {
         pluralCountToCurrencyUnitPattern = new HashMap<String, String>();
-        
+
         String numberStylePattern = NumberFormat.getPattern(uloc, NumberFormat.NUMBERSTYLE);
         // Split the number style pattern into pos and neg if applicable
         int separatorIndex = numberStylePattern.indexOf(";");
@@ -286,7 +294,7 @@ public class CurrencyPluralInfo implements Cloneable, Serializable {
         for (Map.Entry<String, String> e : map.entrySet()) {
             String pluralCount = e.getKey();
             String pattern = e.getValue();
-            
+
             // replace {0} with numberStylePattern
             // and {1} with triple currency sign
             String patternWithNumber = pattern.replace("{0}", numberStylePattern);
index 0e3ec41ebe960f39c6fdbe92ca1aadbb6722bf18..07e812bcd2a8555aa5aa337826f9802ae3e5a31b 100644 (file)
@@ -232,8 +232,11 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
      * Returns the array of strings used as digits, in order from 0 through 9
      * Package private method - doesn't create a defensively copy.
      * @return the array of digit strings
+     * @internal
+     * @deprecated This API is ICU internal only.
      */
-    String[] getDigitStringsLocal() {
+    @Deprecated
+    public String[] getDigitStringsLocal() {
         return digitStrings;
     }
 
@@ -1318,9 +1321,9 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
         setMonetaryGroupingSeparatorString(numberElements[11]);
         setExponentMultiplicationSign(numberElements[12]);
 
-        digit = DecimalFormat.PATTERN_DIGIT;  // Localized pattern character no longer in CLDR
-        padEscape = DecimalFormat.PATTERN_PAD_ESCAPE;
-        sigDigit  = DecimalFormat.PATTERN_SIGNIFICANT_DIGIT;
+        digit = '#';  // Localized pattern character no longer in CLDR
+        padEscape = '*';
+        sigDigit  = '@';
 
 
         CurrencyDisplayInfo info = CurrencyData.provider.getInstance(locale, true);
@@ -1448,8 +1451,8 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
             exponential = 'E';
         }
         if (serialVersionOnStream < 2) {
-            padEscape = DecimalFormat.PATTERN_PAD_ESCAPE;
-            plusSign = DecimalFormat.PATTERN_PLUS_SIGN;
+            padEscape = '*';
+            plusSign = '+';
             exponentSeparator = String.valueOf(exponential);
             // Although we read the exponential field on stream to create the
             // exponentSeparator, we don't do the reverse, since scientific
@@ -1527,7 +1530,7 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
                 groupingSeparatorString = String.valueOf(groupingSeparator);
             }
             if (percentString == null) {
-                percentString = String.valueOf(percentString);
+                percentString = String.valueOf(percent);
             }
             if (perMillString == null) {
                 perMillString = String.valueOf(perMill);
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat_ICU58.java b/icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat_ICU58.java
new file mode 100644 (file)
index 0000000..1476274
--- /dev/null
@@ -0,0 +1,6277 @@
+// © 2016 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+/*
+ *******************************************************************************
+ * Copyright (C) 1996-2016, International Business Machines Corporation and
+ * others. All Rights Reserved.
+ *******************************************************************************
+ */
+package com.ibm.icu.text;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.math.BigInteger;
+import java.text.AttributedCharacterIterator;
+import java.text.AttributedString;
+import java.text.ChoiceFormat;
+import java.text.FieldPosition;
+import java.text.Format;
+import java.text.ParsePosition;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import com.ibm.icu.impl.ICUConfig;
+import com.ibm.icu.impl.PatternProps;
+import com.ibm.icu.impl.Utility;
+import com.ibm.icu.lang.UCharacter;
+import com.ibm.icu.math.BigDecimal;
+import com.ibm.icu.math.MathContext;
+import com.ibm.icu.text.PluralRules.FixedDecimal;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyUsage;
+import com.ibm.icu.util.CurrencyAmount;
+import com.ibm.icu.util.ULocale;
+import com.ibm.icu.util.ULocale.Category;
+
+/**
+ * {@icuenhanced java.text.DecimalFormat}.{@icu _usage_}
+ *
+ * <code>DecimalFormat</code> is a concrete subclass of {@link NumberFormat} that formats
+ * decimal numbers. It has a variety of features designed to make it possible to parse and
+ * format numbers in any locale, including support for Western, Arabic, or Indic digits.
+ * It also supports different flavors of numbers, including integers ("123"), fixed-point
+ * numbers ("123.4"), scientific notation ("1.23E4"), percentages ("12%"), and currency
+ * amounts ("$123.00", "USD123.00", "123.00 US dollars").  All of these flavors can be
+ * easily localized.
+ *
+ * <p>To obtain a {@link NumberFormat} for a specific locale (including the default
+ * locale) call one of <code>NumberFormat</code>'s factory methods such as {@link
+ * NumberFormat#getInstance}. Do not call the <code>DecimalFormat</code> constructors
+ * directly, unless you know what you are doing, since the {@link NumberFormat} factory
+ * methods may return subclasses other than <code>DecimalFormat</code>. If you need to
+ * customize the format object, do something like this:
+ *
+ * <blockquote><pre>
+ * NumberFormat f = NumberFormat.getInstance(loc);
+ * if (f instanceof DecimalFormat) {
+ *     ((DecimalFormat) f).setDecimalSeparatorAlwaysShown(true);
+ * }</pre></blockquote>
+ *
+ * <p><strong>Example Usage</strong>
+ *
+ * Print out a number using the localized number, currency, and percent
+ * format for each locale.
+ *
+ * <blockquote><pre>
+ * Locale[] locales = NumberFormat.getAvailableLocales();
+ * double myNumber = -1234.56;
+ * NumberFormat format;
+ * for (int j=0; j&lt;3; ++j) {
+ *     System.out.println("FORMAT");
+ *     for (int i = 0; i &lt; locales.length; ++i) {
+ *         if (locales[i].getCountry().length() == 0) {
+ *            // Skip language-only locales
+ *            continue;
+ *         }
+ *         System.out.print(locales[i].getDisplayName());
+ *         switch (j) {
+ *         case 0:
+ *             format = NumberFormat.getInstance(locales[i]); break;
+ *         case 1:
+ *             format = NumberFormat.getCurrencyInstance(locales[i]); break;
+ *         default:
+ *             format = NumberFormat.getPercentInstance(locales[i]); break;
+ *         }
+ *         try {
+ *             // Assume format is a DecimalFormat
+ *             System.out.print(": " + ((DecimalFormat) format).toPattern()
+ *                              + " -&gt; " + form.format(myNumber));
+ *         } catch (Exception e) {}
+ *         try {
+ *             System.out.println(" -&gt; " + format.parse(form.format(myNumber)));
+ *         } catch (ParseException e) {}
+ *     }
+ * }</pre></blockquote>
+ *
+ * <p>Another example use getInstance(style).<br>
+ * Print out a number using the localized number, currency, percent,
+ * scientific, integer, iso currency, and plural currency format for each locale.
+ *
+ * <blockquote><pre>
+ * ULocale locale = new ULocale("en_US");
+ * double myNumber = 1234.56;
+ * for (int j=NumberFormat.NUMBERSTYLE; j&lt;=NumberFormat.PLURALCURRENCYSTYLE; ++j) {
+ *     NumberFormat format = NumberFormat.getInstance(locale, j);
+ *     try {
+ *         // Assume format is a DecimalFormat
+ *         System.out.print(": " + ((DecimalFormat) format).toPattern()
+ *                          + " -&gt; " + form.format(myNumber));
+ *     } catch (Exception e) {}
+ *     try {
+ *         System.out.println(" -&gt; " + format.parse(form.format(myNumber)));
+ *     } catch (ParseException e) {}
+ * }</pre></blockquote>
+ *
+ * <h3>Patterns</h3>
+ *
+ * <p>A <code>DecimalFormat</code> consists of a <em>pattern</em> and a set of
+ * <em>symbols</em>.  The pattern may be set directly using {@link #applyPattern}, or
+ * indirectly using other API methods which manipulate aspects of the pattern, such as the
+ * minimum number of integer digits.  The symbols are stored in a {@link
+ * DecimalFormatSymbols} object.  When using the {@link NumberFormat} factory methods, the
+ * pattern and symbols are read from ICU's locale data.
+ *
+ * <h4>Special Pattern Characters</h4>
+ *
+ * <p>Many characters in a pattern are taken literally; they are matched during parsing
+ * and output unchanged during formatting.  Special characters, on the other hand, stand
+ * for other characters, strings, or classes of characters.  For example, the '#'
+ * character is replaced by a localized digit.  Often the replacement character is the
+ * same as the pattern character; in the U.S. locale, the ',' grouping character is
+ * replaced by ','.  However, the replacement is still happening, and if the symbols are
+ * modified, the grouping character changes.  Some special characters affect the behavior
+ * of the formatter by their presence; for example, if the percent character is seen, then
+ * the value is multiplied by 100 before being displayed.
+ *
+ * <p>To insert a special character in a pattern as a literal, that is, without any
+ * special meaning, the character must be quoted.  There are some exceptions to this which
+ * are noted below.
+ *
+ * <p>The characters listed here are used in non-localized patterns.  Localized patterns
+ * use the corresponding characters taken from this formatter's {@link
+ * DecimalFormatSymbols} object instead, and these characters lose their special status.
+ * Two exceptions are the currency sign and quote, which are not localized.
+ *
+ * <blockquote>
+ * <table border=0 cellspacing=3 cellpadding=0 summary="Chart showing symbol,
+ *  location, localized, and meaning.">
+ *   <tr style="background-color: #ccccff">
+ *     <th align=left>Symbol
+ *     <th align=left>Location
+ *     <th align=left>Localized?
+ *     <th align=left>Meaning
+ *   <tr style="vertical-align: top;">
+ *     <td><code>0</code>
+ *     <td>Number
+ *     <td>Yes
+ *     <td>Digit
+ *   <tr style="vertical-align: top; background-color: #eeeeff;">
+ *     <td><code>1-9</code>
+ *     <td>Number
+ *     <td>Yes
+ *     <td>'1' through '9' indicate rounding.
+ *   <tr style="vertical-align: top;">
+ *     <td><code>@</code>
+ *     <td>Number
+ *     <td>No
+ *     <td>Significant digit
+ *   <tr style="vertical-align: top; background-color: #eeeeff;">
+ *     <td><code>#</code>
+ *     <td>Number
+ *     <td>Yes
+ *     <td>Digit, zero shows as absent
+ *   <tr style="vertical-align: top;">
+ *     <td><code>.</code>
+ *     <td>Number
+ *     <td>Yes
+ *     <td>Decimal separator or monetary decimal separator
+ *   <tr style="vertical-align: top; background-color: #eeeeff;">
+ *     <td><code>-</code>
+ *     <td>Number
+ *     <td>Yes
+ *     <td>Minus sign
+ *   <tr style="vertical-align: top;">
+ *     <td><code>,</code>
+ *     <td>Number
+ *     <td>Yes
+ *     <td>Grouping separator
+ *   <tr style="vertical-align: top; background-color: #eeeeff;">
+ *     <td><code>E</code>
+ *     <td>Number
+ *     <td>Yes
+ *     <td>Separates mantissa and exponent in scientific notation.
+ *         <em>Need not be quoted in prefix or suffix.</em>
+ *   <tr style="vertical-align: top;">
+ *     <td><code>+</code>
+ *     <td>Exponent
+ *     <td>Yes
+ *     <td>Prefix positive exponents with localized plus sign.
+ *         <em>Need not be quoted in prefix or suffix.</em>
+ *   <tr style="vertical-align: top; background-color: #eeeeff;">
+ *     <td><code>;</code>
+ *     <td>Subpattern boundary
+ *     <td>Yes
+ *     <td>Separates positive and negative subpatterns
+ *   <tr style="vertical-align: top;">
+ *     <td><code>%</code>
+ *     <td>Prefix or suffix
+ *     <td>Yes
+ *     <td>Multiply by 100 and show as percentage
+ *   <tr style="vertical-align: top; background-color: #eeeeff;">
+ *     <td><code>&#92;u2030</code>
+ *     <td>Prefix or suffix
+ *     <td>Yes
+ *     <td>Multiply by 1000 and show as per mille
+ *   <tr style="vertical-align: top;">
+ *     <td><code>&#164;</code> (<code>&#92;u00A4</code>)
+ *     <td>Prefix or suffix
+ *     <td>No
+ *     <td>Currency sign, replaced by currency symbol.  If
+ *         doubled, replaced by international currency symbol.
+ *         If tripled, replaced by currency plural names, for example,
+ *         "US dollar" or "US dollars" for America.
+ *         If present in a pattern, the monetary decimal separator
+ *         is used instead of the decimal separator.
+ *   <tr style="vertical-align: top; background-color: #eeeeff;">
+ *     <td><code>'</code>
+ *     <td>Prefix or suffix
+ *     <td>No
+ *     <td>Used to quote special characters in a prefix or suffix,
+ *         for example, <code>"'#'#"</code> formats 123 to
+ *         <code>"#123"</code>.  To create a single quote
+ *         itself, use two in a row: <code>"# o''clock"</code>.
+ *   <tr style="vertical-align: top;">
+ *     <td><code>*</code>
+ *     <td>Prefix or suffix boundary
+ *     <td>Yes
+ *     <td>Pad escape, precedes pad character
+ * </table>
+ * </blockquote>
+ *
+ * <p>A <code>DecimalFormat</code> pattern contains a postive and negative subpattern, for
+ * example, "#,##0.00;(#,##0.00)".  Each subpattern has a prefix, a numeric part, and a
+ * suffix.  If there is no explicit negative subpattern, the negative subpattern is the
+ * localized minus sign prefixed to the positive subpattern. That is, "0.00" alone is
+ * equivalent to "0.00;-0.00".  If there is an explicit negative subpattern, it serves
+ * only to specify the negative prefix and suffix; the number of digits, minimal digits,
+ * and other characteristics are ignored in the negative subpattern. That means that
+ * "#,##0.0#;(#)" has precisely the same result as "#,##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 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 {@link #parse} to
+ * be able to distinguish positive from negative values.  Another example is that the
+ * decimal separator and thousands separator should be distinct characters, or parsing
+ * will be impossible.
+ *
+ * <p>The <em>grouping separator</em> is a character that separates clusters of integer
+ * digits to make large numbers more legible.  It commonly used for thousands, but in some
+ * locales it separates ten-thousands.  The <em>grouping size</em> is the number of digits
+ * between the grouping separators, such as 3 for "100,000,000" or 4 for "1 0000
+ * 0000". There are actually two different grouping sizes: One used for the least
+ * significant integer digits, the <em>primary grouping size</em>, and one used for all
+ * others, the <em>secondary grouping size</em>.  In most locales these are the same, but
+ * sometimes they are different. For example, if the primary grouping interval is 3, and
+ * the secondary is 2, then this corresponds to the pattern "#,##,##0", and the number
+ * 123456789 is formatted as "12,34,56,789".  If a pattern contains multiple grouping
+ * separators, the interval between the last one and the end of the integer defines the
+ * primary grouping size, and the interval between the last two defines the secondary
+ * grouping size. All others are ignored, so "#,##,###,####" == "###,###,####" ==
+ * "##,#,###,####".
+ *
+ * <p>Illegal patterns, such as "#.#.#" or "#.###,###", will cause
+ * <code>DecimalFormat</code> to throw an {@link IllegalArgumentException} with a message
+ * that describes the problem.
+ *
+ * <h4>Pattern BNF</h4>
+ *
+ * <pre>
+ * pattern    := subpattern (';' subpattern)?
+ * subpattern := prefix? number exponent? suffix?
+ * number     := (integer ('.' fraction)?) | sigDigits
+ * prefix     := '&#92;u0000'..'&#92;uFFFD' - specialCharacters
+ * suffix     := '&#92;u0000'..'&#92;uFFFD' - specialCharacters
+ * integer    := '#'* '0'* '0'
+ * fraction   := '0'* '#'*
+ * sigDigits  := '#'* '@' '@'* '#'*
+ * exponent   := 'E' '+'? '0'* '0'
+ * padSpec    := '*' padChar
+ * padChar    := '&#92;u0000'..'&#92;uFFFD' - quote
+ * &#32;
+ * Notation:
+ *   X*       0 or more instances of X
+ *   X?       0 or 1 instances of X
+ *   X|Y      either X or Y
+ *   C..D     any character from C up to D, inclusive
+ *   S-T      characters in S, except those in T
+ * </pre>
+ * The first subpattern is for positive numbers. The second (optional)
+ * subpattern is for negative numbers.
+ *
+ * <p>Not indicated in the BNF syntax above:
+ *
+ * <ul>
+ *
+ * <li>The grouping separator ',' can occur inside the integer and sigDigits
+ * elements, between any two pattern characters of that element, as long as the integer or
+ * sigDigits element is not followed by the exponent element.
+ *
+ * <li>Two grouping intervals are recognized: That between the decimal point and the first
+ * grouping symbol, and that between the first and second grouping symbols. These
+ * intervals are identical in most locales, but in some locales they differ. For example,
+ * the pattern &quot;#,##,###&quot; formats the number 123456789 as
+ * &quot;12,34,56,789&quot;.
+ *
+ * <li>The pad specifier <code>padSpec</code> may appear before the prefix, after the
+ * prefix, before the suffix, after the suffix, or not at all.
+ *
+ * <li>In place of '0', the digits '1' through '9' may be used to indicate a rounding
+ * increment.
+ *
+ * </ul>
+ *
+ * <h4>Parsing</h4>
+ *
+ * <p><code>DecimalFormat</code> parses all Unicode characters that represent decimal
+ * digits, as defined by {@link UCharacter#digit}.  In addition,
+ * <code>DecimalFormat</code> also recognizes as digits the ten consecutive characters
+ * starting with the localized zero digit defined in the {@link DecimalFormatSymbols}
+ * object.  During formatting, the {@link DecimalFormatSymbols}-based digits are output.
+ *
+ * <p>During parsing, grouping separators are ignored.
+ *
+ * <p>For currency parsing, the formatter is able to parse every currency style formats no
+ * matter which style the formatter is constructed with.  For example, a formatter
+ * instance gotten from NumberFormat.getInstance(ULocale, NumberFormat.CURRENCYSTYLE) can
+ * parse formats such as "USD1.00" and "3.00 US dollars".
+ *
+ * <p>If {@link #parse(String, ParsePosition)} fails to parse a string, it returns
+ * <code>null</code> and leaves the parse position unchanged.  The convenience method
+ * {@link #parse(String)} indicates parse failure by throwing a {@link
+ * java.text.ParseException}.
+ *
+ * <p>Parsing an extremely large or small absolute value (such as 1.0E10000 or 1.0E-10000)
+ * requires huge memory allocation for representing the parsed number. Such input may expose
+ * a risk of DoS attacks. To prevent huge memory allocation triggered by such inputs,
+ * <code>DecimalFormat</code> internally limits of maximum decimal digits to be 1000. Thus,
+ * an input string resulting more than 1000 digits in plain decimal representation (non-exponent)
+ * will be treated as either overflow (positive/negative infinite) or underflow (+0.0/-0.0).
+ *
+ * <h4>Formatting</h4>
+ *
+ * <p>Formatting is guided by several parameters, all of which can be specified either
+ * using a pattern or using the API.  The following description applies to formats that do
+ * not use <a href="#sci">scientific notation</a> or <a href="#sigdig">significant
+ * digits</a>.
+ *
+ * <ul><li>If the number of actual integer digits exceeds the <em>maximum integer
+ * digits</em>, then only the least significant digits are shown.  For example, 1997 is
+ * formatted as "97" if the maximum integer digits is set to 2.
+ *
+ * <li>If the number of actual integer digits is less than the <em>minimum integer
+ * digits</em>, then leading zeros are added.  For example, 1997 is formatted as "01997"
+ * if the minimum integer digits is set to 5.
+ *
+ * <li>If the number of actual fraction digits exceeds the <em>maximum fraction
+ * digits</em>, then half-even rounding it performed to the maximum fraction digits.  For
+ * example, 0.125 is formatted as "0.12" if the maximum fraction digits is 2.  This
+ * behavior can be changed by specifying a rounding increment and a rounding mode.
+ *
+ * <li>If the number of actual fraction digits is less than the <em>minimum fraction
+ * digits</em>, then trailing zeros are added.  For example, 0.125 is formatted as
+ * "0.1250" if the mimimum fraction digits is set to 4.
+ *
+ * <li>Trailing fractional zeros are not displayed if they occur <em>j</em> positions
+ * after the decimal, where <em>j</em> is less than the maximum fraction digits. For
+ * example, 0.10004 is formatted as "0.1" if the maximum fraction digits is four or less.
+ * </ul>
+ *
+ * <p><strong>Special Values</strong>
+ *
+ * <p><code>NaN</code> is represented as a single character, typically
+ * <code>&#92;uFFFD</code>.  This character is determined by the {@link
+ * DecimalFormatSymbols} object.  This is the only value for which the prefixes and
+ * suffixes are not used.
+ *
+ * <p>Infinity is represented as a single character, typically <code>&#92;u221E</code>,
+ * with the positive or negative prefixes and suffixes applied.  The infinity character is
+ * determined by the {@link DecimalFormatSymbols} object.
+ *
+ * <h4><a name="sci">Scientific Notation</a></h4>
+ *
+ * <p>Numbers in scientific notation are expressed as the product of a mantissa and a
+ * power of ten, for example, 1234 can be expressed as 1.234 x 10<sup>3</sup>. The
+ * mantissa is typically in the half-open interval [1.0, 10.0) or sometimes [0.0, 1.0),
+ * but it need not be.  <code>DecimalFormat</code> supports arbitrary mantissas.
+ * <code>DecimalFormat</code> can be instructed to use scientific notation through the API
+ * or through the pattern.  In a pattern, the exponent character immediately followed by
+ * one or more digit characters indicates scientific notation.  Example: "0.###E0" formats
+ * the number 1234 as "1.234E3".
+ *
+ * <ul>
+ *
+ * <li>The number of digit characters after the exponent character gives the minimum
+ * exponent digit count.  There is no maximum.  Negative exponents are formatted using the
+ * localized minus sign, <em>not</em> the prefix and suffix from the pattern.  This allows
+ * patterns such as "0.###E0 m/s".  To prefix positive exponents with a localized plus
+ * sign, specify '+' between the exponent and the digits: "0.###E+0" will produce formats
+ * "1E+1", "1E+0", "1E-1", etc.  (In localized patterns, use the localized plus sign
+ * rather than '+'.)
+ *
+ * <li>The minimum number of integer digits is achieved by adjusting the exponent.
+ * Example: 0.00123 formatted with "00.###E0" yields "12.3E-4".  This only happens if
+ * there is no maximum number of integer digits.  If there is a maximum, then the minimum
+ * number of integer digits is fixed at one.
+ *
+ * <li>The maximum number of integer digits, if present, specifies the exponent grouping.
+ * The most common use of this is to generate <em>engineering notation</em>, in which the
+ * exponent is a multiple of three, e.g., "##0.###E0".  The number 12345 is formatted
+ * using "##0.####E0" as "12.345E3".
+ *
+ * <li>When using scientific notation, the formatter controls the digit counts using
+ * significant digits logic.  The maximum number of significant digits limits the total
+ * number of integer and fraction digits that will be shown in the mantissa; it does not
+ * affect parsing.  For example, 12345 formatted with "##0.##E0" is "12.3E3".  See the
+ * section on significant digits for more details.
+ *
+ * <li>The number of significant digits shown is determined as follows: If
+ * areSignificantDigitsUsed() returns false, then the minimum number of significant digits
+ * shown is one, and the maximum number of significant digits shown is the sum of the
+ * <em>minimum integer</em> and <em>maximum fraction</em> digits, and is unaffected by the
+ * maximum integer digits.  If this sum is zero, then all significant digits are shown.
+ * If areSignificantDigitsUsed() returns true, then the significant digit counts are
+ * specified by getMinimumSignificantDigits() and getMaximumSignificantDigits().  In this
+ * case, the number of integer digits is fixed at one, and there is no exponent grouping.
+ *
+ * <li>Exponential patterns may not contain grouping separators.
+ *
+ * </ul>
+ *
+ * <h4><a name="sigdig">Significant Digits</a></h4>
+ *
+ * <code>DecimalFormat</code> has two ways of controlling how many digits are shows: (a)
+ * significant digits counts, or (b) integer and fraction digit counts.  Integer and
+ * fraction digit counts are described above.  When a formatter is using significant
+ * digits counts, the number of integer and fraction digits is not specified directly, and
+ * the formatter settings for these counts are ignored.  Instead, the formatter uses
+ * however many integer and fraction digits are required to display the specified number
+ * of significant digits.  Examples:
+ *
+ * <blockquote>
+ * <table border=0 cellspacing=3 cellpadding=0>
+ *   <tr style="background-color: #ccccff">
+ *     <th align=left>Pattern
+ *     <th align=left>Minimum significant digits
+ *     <th align=left>Maximum significant digits
+ *     <th align=left>Number
+ *     <th align=left>Output of format()
+ *   <tr style="vertical-align: top;">
+ *     <td><code>@@@</code>
+ *     <td>3
+ *     <td>3
+ *     <td>12345
+ *     <td><code>12300</code>
+ *   <tr style="vertical-align: top; background-color: #eeeeff;">
+ *     <td><code>@@@</code>
+ *     <td>3
+ *     <td>3
+ *     <td>0.12345
+ *     <td><code>0.123</code>
+ *   <tr style="vertical-align: top;">
+ *     <td><code>@@##</code>
+ *     <td>2
+ *     <td>4
+ *     <td>3.14159
+ *     <td><code>3.142</code>
+ *   <tr style="vertical-align: top; background-color: #eeeeff;">
+ *     <td><code>@@##</code>
+ *     <td>2
+ *     <td>4
+ *     <td>1.23004
+ *     <td><code>1.23</code>
+ * </table>
+ * </blockquote>
+ *
+ * <ul>
+ *
+ * <li>Significant digit counts may be expressed using patterns that specify a minimum and
+ * maximum number of significant digits.  These are indicated by the <code>'@'</code> and
+ * <code>'#'</code> characters.  The minimum number of significant digits is the number of
+ * <code>'@'</code> characters.  The maximum number of significant digits is the number of
+ * <code>'@'</code> characters plus the number of <code>'#'</code> characters following on
+ * the right.  For example, the pattern <code>"@@@"</code> indicates exactly 3 significant
+ * digits.  The pattern <code>"@##"</code> indicates from 1 to 3 significant digits.
+ * Trailing zero digits to the right of the decimal separator are suppressed after the
+ * minimum number of significant digits have been shown.  For example, the pattern
+ * <code>"@##"</code> formats the number 0.1203 as <code>"0.12"</code>.
+ *
+ * <li>If a pattern uses significant digits, it may not contain a decimal separator, nor
+ * the <code>'0'</code> pattern character.  Patterns such as <code>"@00"</code> or
+ * <code>"@.###"</code> are disallowed.
+ *
+ * <li>Any number of <code>'#'</code> characters may be prepended to the left of the
+ * leftmost <code>'@'</code> character.  These have no effect on the minimum and maximum
+ * significant digits counts, but may be used to position grouping separators.  For
+ * example, <code>"#,#@#"</code> indicates a minimum of one significant digits, a maximum
+ * of two significant digits, and a grouping size of three.
+ *
+ * <li>In order to enable significant digits formatting, use a pattern containing the
+ * <code>'@'</code> pattern character.  Alternatively, call {@link
+ * #setSignificantDigitsUsed setSignificantDigitsUsed(true)}.
+ *
+ * <li>In order to disable significant digits formatting, use a pattern that does not
+ * contain the <code>'@'</code> pattern character. Alternatively, call {@link
+ * #setSignificantDigitsUsed setSignificantDigitsUsed(false)}.
+ *
+ * <li>The number of significant digits has no effect on parsing.
+ *
+ * <li>Significant digits may be used together with exponential notation. Such patterns
+ * are equivalent to a normal exponential pattern with a minimum and maximum integer digit
+ * count of one, a minimum fraction digit count of <code>getMinimumSignificantDigits() -
+ * 1</code>, and a maximum fraction digit count of <code>getMaximumSignificantDigits() -
+ * 1</code>. For example, the pattern <code>"@@###E0"</code> is equivalent to
+ * <code>"0.0###E0"</code>.
+ *
+ * <li>If signficant digits are in use, then the integer and fraction digit counts, as set
+ * via the API, are ignored.  If significant digits are not in use, then the signficant
+ * digit counts, as set via the API, are ignored.
+ *
+ * </ul>
+ *
+ * <h4>Padding</h4>
+ *
+ * <p><code>DecimalFormat</code> supports padding the result of {@link #format} to a
+ * specific width.  Padding may be specified either through the API or through the pattern
+ * syntax.  In a pattern the pad escape character, followed by a single pad character,
+ * causes padding to be parsed and formatted.  The pad escape character is '*' in
+ * unlocalized patterns, and can be localized using {@link
+ * DecimalFormatSymbols#setPadEscape}.  For example, <code>"$*x#,##0.00"</code> formats
+ * 123 to <code>"$xx123.00"</code>, and 1234 to <code>"$1,234.00"</code>.
+ *
+ * <ul>
+ *
+ * <li>When padding is in effect, the width of the positive subpattern, including prefix
+ * and suffix, determines the format width.  For example, in the pattern <code>"* #0
+ * o''clock"</code>, the format width is 10.
+ *
+ * <li>The width is counted in 16-bit code units (Java <code>char</code>s).
+ *
+ * <li>Some parameters which usually do not matter have meaning when padding is used,
+ * because the pattern width is significant with padding.  In the pattern "*
+ * ##,##,#,##0.##", the format width is 14.  The initial characters "##,##," do not affect
+ * the grouping size or maximum integer digits, but they do affect the format width.
+ *
+ * <li>Padding may be inserted at one of four locations: before the prefix, after the
+ * prefix, before the suffix, or after the suffix.  If padding is specified in any other
+ * location, {@link #applyPattern} throws an {@link IllegalArgumentException}.  If there
+ * is no prefix, before the prefix and after the prefix are equivalent, likewise for the
+ * suffix.
+ *
+ * <li>When specified in a pattern, the 16-bit <code>char</code> immediately following the
+ * pad escape is the pad character. This may be any character, including a special pattern
+ * character. That is, the pad escape <em>escapes</em> the following character. If there
+ * is no character after the pad escape, then the pattern is illegal.
+ *
+ * </ul>
+ *
+ * <p>
+ * <strong>Rounding</strong>
+ *
+ * <p><code>DecimalFormat</code> supports rounding to a specific increment.  For example,
+ * 1230 rounded to the nearest 50 is 1250.  1.234 rounded to the nearest 0.65 is 1.3.  The
+ * rounding increment may be specified through the API or in a pattern.  To specify a
+ * rounding increment in a pattern, include the increment in the pattern itself.  "#,#50"
+ * specifies a rounding increment of 50.  "#,##0.05" specifies a rounding increment of
+ * 0.05.
+ *
+ * <ul>
+ *
+ * <li>Rounding only affects the string produced by formatting.  It does not affect
+ * parsing or change any numerical values.
+ *
+ * <li>A <em>rounding mode</em> determines how values are rounded; see the {@link
+ * com.ibm.icu.math.BigDecimal} documentation for a description of the modes.  Rounding
+ * increments specified in patterns use the default mode, {@link
+ * com.ibm.icu.math.BigDecimal#ROUND_HALF_EVEN}.
+ *
+ * <li>Some locales use rounding in their currency formats to reflect the smallest
+ * currency denomination.
+ *
+ * <li>In a pattern, digits '1' through '9' specify rounding, but otherwise behave
+ * identically to digit '0'.
+ *
+ * </ul>
+ *
+ * <h4>Synchronization</h4>
+ *
+ * <p><code>DecimalFormat</code> objects are not synchronized.  Multiple threads should
+ * not access one formatter concurrently.
+ *
+ * @see          java.text.Format
+ * @see          NumberFormat
+ * @author       Mark Davis
+ * @author       Alan Liu
+ * @deprecated DecimalFormat was overhauled in ICU 59.  This is the old implementation, provided
+ *    temporarily to ease the transition.  This class will be removed from ICU 60.
+ */
+@Deprecated
+public class DecimalFormat_ICU58 extends NumberFormat {
+
+    /**
+     * Creates a DecimalFormat using the default pattern and symbols for the default
+     * <code>FORMAT</code> locale. This is a convenient way to obtain a DecimalFormat when
+     * internationalization is not the main concern.
+     *
+     * <p>To obtain standard formats for a given locale, use the factory methods on
+     * NumberFormat such as getNumberInstance.  These factories will return the most
+     * appropriate sub-class of NumberFormat for a given locale.
+     *
+     * @see NumberFormat#getInstance
+     * @see NumberFormat#getNumberInstance
+     * @see NumberFormat#getCurrencyInstance
+     * @see NumberFormat#getPercentInstance
+     * @see Category#FORMAT
+     * @stable ICU 2.0
+     */
+    public DecimalFormat_ICU58() {
+        ULocale def = ULocale.getDefault(Category.FORMAT);
+        String pattern = getPattern(def, 0);
+        // Always applyPattern after the symbols are set
+        this.symbols = new DecimalFormatSymbols(def);
+        setCurrency(Currency.getInstance(def));
+        applyPatternWithoutExpandAffix(pattern, false);
+        if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
+            currencyPluralInfo = new CurrencyPluralInfo(def);
+            // the exact pattern is not known until the plural count is known.
+            // so, no need to expand affix now.
+        } else {
+            expandAffixAdjustWidth(null);
+        }
+    }
+
+    /**
+     * Creates a DecimalFormat from the given pattern and the symbols for the default
+     * <code>FORMAT</code> locale. This is a convenient way to obtain a DecimalFormat when
+     * internationalization is not the main concern.
+     *
+     * <p>To obtain standard formats for a given locale, use the factory methods on
+     * NumberFormat such as getNumberInstance.  These factories will return the most
+     * appropriate sub-class of NumberFormat for a given locale.
+     *
+     * @param pattern A non-localized pattern string.
+     * @throws IllegalArgumentException if the given pattern is invalid.
+     * @see NumberFormat#getInstance
+     * @see NumberFormat#getNumberInstance
+     * @see NumberFormat#getCurrencyInstance
+     * @see NumberFormat#getPercentInstance
+     * @see Category#FORMAT
+     * @stable ICU 2.0
+     */
+    public DecimalFormat_ICU58(String pattern) {
+        // Always applyPattern after the symbols are set
+        ULocale def = ULocale.getDefault(Category.FORMAT);
+        this.symbols = new DecimalFormatSymbols(def);
+        setCurrency(Currency.getInstance(def));
+        applyPatternWithoutExpandAffix(pattern, false);
+        if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
+            currencyPluralInfo = new CurrencyPluralInfo(def);
+        } else {
+            expandAffixAdjustWidth(null);
+        }
+    }
+
+    /**
+     * Creates a DecimalFormat from the given pattern and symbols. Use this constructor
+     * when you need to completely customize the behavior of the format.
+     *
+     * <p>To obtain standard formats for a given locale, use the factory methods on
+     * NumberFormat such as getInstance or getCurrencyInstance. If you need only minor
+     * adjustments to a standard format, you can modify the format returned by a
+     * NumberFormat factory method.
+     *
+     * @param pattern a non-localized pattern string
+     * @param symbols the set of symbols to be used
+     * @exception IllegalArgumentException if the given pattern is invalid
+     * @see NumberFormat#getInstance
+     * @see NumberFormat#getNumberInstance
+     * @see NumberFormat#getCurrencyInstance
+     * @see NumberFormat#getPercentInstance
+     * @see DecimalFormatSymbols
+     * @stable ICU 2.0
+     */
+    public DecimalFormat_ICU58(String pattern, DecimalFormatSymbols symbols) {
+        createFromPatternAndSymbols(pattern, symbols);
+    }
+
+    private void createFromPatternAndSymbols(String pattern, DecimalFormatSymbols inputSymbols) {
+        // Always applyPattern after the symbols are set
+        symbols = (DecimalFormatSymbols) inputSymbols.clone();
+        if (pattern.indexOf(CURRENCY_SIGN) >= 0) {
+            // Only spend time with currency symbols when we're going to display it.
+            // Also set some defaults before the apply pattern.
+            setCurrencyForSymbols();
+        }
+        applyPatternWithoutExpandAffix(pattern, false);
+        if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
+            currencyPluralInfo = new CurrencyPluralInfo(symbols.getULocale());
+        } else {
+            expandAffixAdjustWidth(null);
+        }
+    }
+
+    /**
+     * Creates a DecimalFormat from the given pattern, symbols, information used for
+     * currency plural format, and format style. Use this constructor when you need to
+     * completely customize the behavior of the format.
+     *
+     * <p>To obtain standard formats for a given locale, use the factory methods on
+     * NumberFormat such as getInstance or getCurrencyInstance.
+     *
+     * <p>If you need only minor adjustments to a standard format, you can modify the
+     * format returned by a NumberFormat factory method using the setters.
+     *
+     * <p>If you want to completely customize a decimal format, using your own
+     * DecimalFormatSymbols (such as group separators) and your own information for
+     * currency plural formatting (such as plural rule and currency plural patterns), you
+     * can use this constructor.
+     *
+     * @param pattern a non-localized pattern string
+     * @param symbols the set of symbols to be used
+     * @param infoInput the information used for currency plural format, including
+     * currency plural patterns and plural rules.
+     * @param style the decimal formatting style, it is one of the following values:
+     * NumberFormat.NUMBERSTYLE; NumberFormat.CURRENCYSTYLE; NumberFormat.PERCENTSTYLE;
+     * NumberFormat.SCIENTIFICSTYLE; NumberFormat.INTEGERSTYLE;
+     * NumberFormat.ISOCURRENCYSTYLE; NumberFormat.PLURALCURRENCYSTYLE;
+     * @stable ICU 4.2
+     */
+    public DecimalFormat_ICU58(String pattern, DecimalFormatSymbols symbols, CurrencyPluralInfo infoInput,
+                         int style) {
+        CurrencyPluralInfo info = infoInput;
+        if (style == NumberFormat.PLURALCURRENCYSTYLE) {
+            info = (CurrencyPluralInfo) infoInput.clone();
+        }
+        create(pattern, symbols, info, style);
+    }
+
+    private void create(String pattern, DecimalFormatSymbols inputSymbols, CurrencyPluralInfo info,
+                        int inputStyle) {
+        if (inputStyle != NumberFormat.PLURALCURRENCYSTYLE) {
+            createFromPatternAndSymbols(pattern, inputSymbols);
+        } else {
+            // Always applyPattern after the symbols are set
+            symbols = (DecimalFormatSymbols) inputSymbols.clone();
+            currencyPluralInfo = info;
+            // the pattern used in format is not fixed until formatting, in which, the
+            // number is known and will be used to pick the right pattern based on plural
+            // count.  Here, set the pattern as the pattern of plural count == "other".
+            // For most locale, the patterns are probably the same for all plural
+            // count. If not, the right pattern need to be re-applied during format.
+            String currencyPluralPatternForOther =
+                currencyPluralInfo.getCurrencyPluralPattern("other");
+            applyPatternWithoutExpandAffix(currencyPluralPatternForOther, false);
+            setCurrencyForSymbols();
+        }
+        style = inputStyle;
+    }
+
+    /**
+     * Creates a DecimalFormat for currency plural format from the given pattern, symbols,
+     * and style.
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    @Deprecated
+    public DecimalFormat_ICU58(String pattern, DecimalFormatSymbols inputSymbols, int style) {
+        CurrencyPluralInfo info = null;
+        if (style == NumberFormat.PLURALCURRENCYSTYLE) {
+            info = new CurrencyPluralInfo(inputSymbols.getULocale());
+        }
+        create(pattern, inputSymbols, info, style);
+    }
+
+    /**
+     * {@inheritDoc}
+     * @stable ICU 2.0
+     */
+    @Override
+    public StringBuffer format(double number, StringBuffer result, FieldPosition fieldPosition) {
+        return format(number, result, fieldPosition, false);
+    }
+
+    // See if number is negative.
+    // usage: isNegative(multiply(numberToBeFormatted));
+    private boolean isNegative(double number) {
+        // Detecting whether a double is negative is easy with the exception of the value
+        // -0.0. This is a double which has a zero mantissa (and exponent), but a negative
+        // sign bit. It is semantically distinct from a zero with a positive sign bit, and
+        // this distinction is important to certain kinds of computations. However, it's a
+        // little tricky to detect, since (-0.0 == 0.0) and !(-0.0 < 0.0). How then, you
+        // may ask, does it behave distinctly from +0.0? Well, 1/(-0.0) ==
+        // -Infinity. Proper detection of -0.0 is needed to deal with the issues raised by
+        // bugs 4106658, 4106667, and 4147706. Liu 7/6/98.
+        return (number < 0.0) || (number == 0.0 && 1 / number < 0.0);
+    }
+
+    // Rounds the number and strips of the negative sign.
+    // usage: round(multiply(numberToBeFormatted))
+    private double round(double number) {
+        boolean isNegative = isNegative(number);
+        if (isNegative)
+            number = -number;
+
+        // Apply rounding after multiplier
+        if (roundingDouble > 0.0) {
+            // number = roundingDouble
+            //    * round(number / roundingDouble, roundingMode, isNegative);
+            return round(
+                number, roundingDouble, roundingDoubleReciprocal, roundingMode,
+                isNegative);
+        }
+        return number;
+    }
+
+    // Multiplies given number by multipler (if there is one) returning the new
+    // number. If there is no multiplier, returns the number passed in unchanged.
+    private double multiply(double number) {
+        if (multiplier != 1) {
+            return number * multiplier;
+        }
+        return number;
+    }
+
+    // [Spark/CDL] The actual method to format number. If boolean value
+    // parseAttr == true, then attribute information will be recorded.
+    private StringBuffer format(double number, StringBuffer result, FieldPosition fieldPosition,
+                                boolean parseAttr) {
+        fieldPosition.setBeginIndex(0);
+        fieldPosition.setEndIndex(0);
+
+        if (Double.isNaN(number)) {
+            if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
+                fieldPosition.setBeginIndex(result.length());
+            } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
+                fieldPosition.setBeginIndex(result.length());
+            }
+
+            result.append(symbols.getNaN());
+            // TODO: Combine setting a single FieldPosition or adding to an AttributedCharacterIterator
+            // into a function like recordAttribute(FieldAttribute, begin, end).
+
+            // [Spark/CDL] Add attribute for NaN here.
+            // result.append(symbols.getNaN());
+            if (parseAttr) {
+                addAttribute(Field.INTEGER, result.length() - symbols.getNaN().length(),
+                             result.length());
+            }
+            if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
+                fieldPosition.setEndIndex(result.length());
+            } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
+                fieldPosition.setEndIndex(result.length());
+            }
+
+            addPadding(result, fieldPosition, 0, 0);
+            return result;
+        }
+
+        // Do this BEFORE checking to see if value is negative or infinite and
+        // before rounding.
+        number = multiply(number);
+        boolean isNegative = isNegative(number);
+        number = round(number);
+
+        if (Double.isInfinite(number)) {
+            int prefixLen = appendAffix(result, isNegative, true, fieldPosition, parseAttr);
+
+            if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
+                fieldPosition.setBeginIndex(result.length());
+            } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
+                fieldPosition.setBeginIndex(result.length());
+            }
+
+            // [Spark/CDL] Add attribute for infinity here.
+            result.append(symbols.getInfinity());
+            if (parseAttr) {
+                addAttribute(Field.INTEGER, result.length() - symbols.getInfinity().length(),
+                             result.length());
+            }
+            if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
+                fieldPosition.setEndIndex(result.length());
+            } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
+                fieldPosition.setEndIndex(result.length());
+            }
+
+            int suffixLen = appendAffix(result, isNegative, false, fieldPosition, parseAttr);
+
+            addPadding(result, fieldPosition, prefixLen, suffixLen);
+            return result;
+        }
+
+        int precision = precision(false);
+
+        // This is to fix rounding for scientific notation. See ticket:10542.
+        // This code should go away when a permanent fix is done for ticket:9931.
+        //
+        // This block of code only executes for scientific notation so it will not interfere with the
+        // previous fix in {@link #resetActualRounding} for fixed decimal numbers.
+        // Moreover this code only runs when there is rounding to be done (precision > 0) and when the
+        // rounding mode is something other than ROUND_HALF_EVEN.
+        // This block of code does the correct rounding of number in advance so that it will fit into
+        // the number of digits indicated by precision. In this way, we avoid using the default
+        // ROUND_HALF_EVEN behavior of DigitList. For example, if number = 0.003016 and roundingMode =
+        // ROUND_DOWN and precision = 3 then after this code executes, number = 0.00301 (3 significant digits)
+        if (useExponentialNotation && precision > 0 && number != 0.0 && roundingMode != BigDecimal.ROUND_HALF_EVEN) {
+           int log10RoundingIncr = 1 - precision + (int) Math.floor(Math.log10(Math.abs(number)));
+           double roundingIncReciprocal = 0.0;
+           double roundingInc = 0.0;
+           if (log10RoundingIncr < 0) {
+               roundingIncReciprocal =
+                       BigDecimal.ONE.movePointRight(-log10RoundingIncr).doubleValue();
+           } else {
+               roundingInc =
+                       BigDecimal.ONE.movePointRight(log10RoundingIncr).doubleValue();
+           }
+           number = DecimalFormat_ICU58.round(number, roundingInc, roundingIncReciprocal, roundingMode, isNegative);
+        }
+        // End fix for ticket:10542
+
+        // At this point we are guaranteed a nonnegative finite
+        // number.
+        synchronized (digitList) {
+            digitList.set(number, precision, !useExponentialNotation &&
+                          !areSignificantDigitsUsed());
+            return subformat(number, result, fieldPosition, isNegative, false, parseAttr);
+        }
+    }
+
+    /**
+     * This is a special function used by the CompactDecimalFormat subclass.
+     * It completes only the rounding portion of the formatting and returns
+     * the resulting double. CompactDecimalFormat uses the result to compute
+     * the plural form to use.
+     *
+     * @param number The number to format.
+     * @return The number rounded to the correct number of significant digits
+     * with negative sign stripped off.
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    @Deprecated
+    double adjustNumberAsInFormatting(double number) {
+        if (Double.isNaN(number)) {
+            return number;
+        }
+        number = round(multiply(number));
+        if (Double.isInfinite(number)) {
+            return number;
+        }
+        return toDigitList(number).getDouble();
+    }
+
+    @Deprecated
+    DigitList toDigitList(double number) {
+        DigitList result = new DigitList();
+        result.set(number, precision(false), false);
+        return result;
+    }
+
+    /**
+      * This is a special function used by the CompactDecimalFormat subclass
+      * to determine if the number to be formatted is negative.
+      *
+      * @param number The number to format.
+      * @return True if number is negative.
+      * @internal
+      * @deprecated This API is ICU internal only.
+      */
+     @Deprecated
+     boolean isNumberNegative(double number) {
+         if (Double.isNaN(number)) {
+             return false;
+         }
+         return isNegative(multiply(number));
+     }
+
+    /**
+     * Round a double value to the nearest multiple of the given rounding increment,
+     * according to the given mode. This is equivalent to rounding value/roundingInc to
+     * the nearest integer, according to the given mode, and returning that integer *
+     * roundingInc. Note this is changed from the version in 2.4, since division of
+     * doubles have inaccuracies. jitterbug 1871.
+     *
+     * @param number
+     *            the absolute value of the number to be rounded
+     * @param roundingInc
+     *            the rounding increment
+     * @param roundingIncReciprocal
+     *            if non-zero, is the reciprocal of rounding inc.
+     * @param mode
+     *            a BigDecimal rounding mode
+     * @param isNegative
+     *            true if the number to be rounded is negative
+     * @return the absolute value of the rounded result
+     */
+    private static double round(double number, double roundingInc, double roundingIncReciprocal,
+                                int mode, boolean isNegative) {
+
+        double div = roundingIncReciprocal == 0.0 ? number / roundingInc : number *
+            roundingIncReciprocal;
+
+        // do the absolute cases first
+
+        switch (mode) {
+        case BigDecimal.ROUND_CEILING:
+            div = (isNegative ? Math.floor(div + epsilon) : Math.ceil(div - epsilon));
+            break;
+        case BigDecimal.ROUND_FLOOR:
+            div = (isNegative ? Math.ceil(div - epsilon) : Math.floor(div + epsilon));
+            break;
+        case BigDecimal.ROUND_DOWN:
+            div = (Math.floor(div + epsilon));
+            break;
+        case BigDecimal.ROUND_UP:
+            div = (Math.ceil(div - epsilon));
+            break;
+        case BigDecimal.ROUND_UNNECESSARY:
+            if (div != Math.floor(div)) {
+                throw new ArithmeticException("Rounding necessary");
+            }
+            return number;
+        default:
+
+            // Handle complex cases, where the choice depends on the closer value.
+
+            // We figure out the distances to the two possible values, ceiling and floor.
+            // We then go for the diff that is smaller.  Only if they are equal does the
+            // mode matter.
+
+            double ceil = Math.ceil(div);
+            double ceildiff = ceil - div; // (ceil * roundingInc) - number;
+            double floor = Math.floor(div);
+            double floordiff = div - floor; // number - (floor * roundingInc);
+
+            // Note that the diff values were those mapped back to the "normal" space by
+            // using the roundingInc. I don't have access to the original author of the
+            // code but suspect that that was to produce better result in edge cases
+            // because of machine precision, rather than simply using the difference
+            // between, say, ceil and div.  However, it didn't work in all cases. Am
+            // trying instead using an epsilon value.
+
+            switch (mode) {
+            case BigDecimal.ROUND_HALF_EVEN:
+                // We should be able to just return Math.rint(a), but this
+                // doesn't work in some VMs.
+                // if one is smaller than the other, take the corresponding side
+                if (floordiff + epsilon < ceildiff) {
+                    div = floor;
+                } else if (ceildiff + epsilon < floordiff) {
+                    div = ceil;
+                } else { // they are equal, so we want to round to whichever is even
+                    double testFloor = floor / 2;
+                    div = (testFloor == Math.floor(testFloor)) ? floor : ceil;
+                }
+                break;
+            case BigDecimal.ROUND_HALF_DOWN:
+                div = ((floordiff <= ceildiff + epsilon) ? floor : ceil);
+                break;
+            case BigDecimal.ROUND_HALF_UP:
+                div = ((ceildiff <= floordiff + epsilon) ? ceil : floor);
+                break;
+            default:
+                throw new IllegalArgumentException("Invalid rounding mode: " + mode);
+            }
+        }
+        number = roundingIncReciprocal == 0.0 ? div * roundingInc : div / roundingIncReciprocal;
+        return number;
+    }
+
+    private static double epsilon = 0.00000000001;
+
+    /**
+     * @stable ICU 2.0
+     */
+    // [Spark/CDL] Delegate to format_long_StringBuffer_FieldPosition_boolean
+    @Override
+    public StringBuffer format(long number, StringBuffer result, FieldPosition fieldPosition) {
+        return format(number, result, fieldPosition, false);
+    }
+
+    private StringBuffer format(long number, StringBuffer result, FieldPosition fieldPosition,
+                                boolean parseAttr) {
+        fieldPosition.setBeginIndex(0);
+        fieldPosition.setEndIndex(0);
+
+        // If we are to do rounding, we need to move into the BigDecimal
+        // domain in order to do divide/multiply correctly.
+        if (actualRoundingIncrementICU != null) {
+            return format(BigDecimal.valueOf(number), result, fieldPosition);
+        }
+
+        boolean isNegative = (number < 0);
+        if (isNegative)
+            number = -number;
+
+        // In general, long values always represent real finite numbers, so we don't have
+        // to check for +/- Infinity or NaN. However, there is one case we have to be
+        // careful of: The multiplier can push a number near MIN_VALUE or MAX_VALUE
+        // outside the legal range. We check for this before multiplying, and if it
+        // happens we use BigInteger instead.
+        if (multiplier != 1) {
+            boolean tooBig = false;
+            if (number < 0) { // This can only happen if number == Long.MIN_VALUE
+                long cutoff = Long.MIN_VALUE / multiplier;
+                tooBig = (number <= cutoff); // number == cutoff can only happen if multiplier == -1
+            } else {
+                long cutoff = Long.MAX_VALUE / multiplier;
+                tooBig = (number > cutoff);
+            }
+            if (tooBig) {
+                // [Spark/CDL] Use
+                // format_BigInteger_StringBuffer_FieldPosition_boolean instead
+                // parseAttr is used to judge whether to synthesize attributes.
+                return format(BigInteger.valueOf(isNegative ? -number : number), result,
+                              fieldPosition, parseAttr);
+            }
+        }
+
+        number *= multiplier;
+        synchronized (digitList) {
+            digitList.set(number, precision(true));
+            // Issue 11808
+            if (digitList.wasRounded() && roundingMode == BigDecimal.ROUND_UNNECESSARY) {
+                throw new ArithmeticException("Rounding necessary");
+            }
+            return subformat(number, result, fieldPosition, isNegative, true, parseAttr);
+        }
+    }
+
+    /**
+     * Formats a BigInteger number.
+     *
+     * @stable ICU 2.0
+     */
+    @Override
+    public StringBuffer format(BigInteger number, StringBuffer result,
+                               FieldPosition fieldPosition) {
+        return format(number, result, fieldPosition, false);
+    }
+
+    private StringBuffer format(BigInteger number, StringBuffer result, FieldPosition fieldPosition,
+                                boolean parseAttr) {
+        // If we are to do rounding, we need to move into the BigDecimal
+        // domain in order to do divide/multiply correctly.
+        if (actualRoundingIncrementICU != null) {
+            return format(new BigDecimal(number), result, fieldPosition);
+        }
+
+        if (multiplier != 1) {
+            number = number.multiply(BigInteger.valueOf(multiplier));
+        }
+
+        // At this point we are guaranteed a nonnegative finite
+        // number.
+        synchronized (digitList) {
+            digitList.set(number, precision(true));
+            // For issue 11808.
+            if (digitList.wasRounded() && roundingMode == BigDecimal.ROUND_UNNECESSARY) {
+                throw new ArithmeticException("Rounding necessary");
+            }
+            return subformat(number.intValue(), result, fieldPosition, number.signum() < 0, true,
+                             parseAttr);
+        }
+    }
+
+    /**
+     * Formats a BigDecimal number.
+     *
+     * @stable ICU 2.0
+     */
+    @Override
+    public StringBuffer format(java.math.BigDecimal number, StringBuffer result,
+                               FieldPosition fieldPosition) {
+        return format(number, result, fieldPosition, false);
+    }
+
+    private StringBuffer format(java.math.BigDecimal number, StringBuffer result,
+                                FieldPosition fieldPosition,
+            boolean parseAttr) {
+        if (multiplier != 1) {
+            number = number.multiply(java.math.BigDecimal.valueOf(multiplier));
+        }
+
+        if (actualRoundingIncrement != null) {
+            number = number.divide(actualRoundingIncrement, 0, roundingMode).multiply(actualRoundingIncrement);
+        }
+
+        synchronized (digitList) {
+            digitList.set(number, precision(false), !useExponentialNotation &&
+                          !areSignificantDigitsUsed());
+            // For issue 11808.
+            if (digitList.wasRounded() && roundingMode == BigDecimal.ROUND_UNNECESSARY) {
+                throw new ArithmeticException("Rounding necessary");
+            }
+            return subformat(number.doubleValue(), result, fieldPosition, number.signum() < 0,
+                             false, parseAttr);
+        }
+    }
+
+    /**
+     * Formats a BigDecimal number.
+     *
+     * @stable ICU 2.0
+     */
+    @Override
+    public StringBuffer format(BigDecimal number, StringBuffer result,
+                               FieldPosition fieldPosition) {
+         // This method is just a copy of the corresponding java.math.BigDecimal method
+         // for now. It isn't very efficient since it must create a conversion object to
+         // do math on the rounding increment. In the future we may try to clean this up,
+         // or even better, limit our support to just one flavor of BigDecimal.
+        if (multiplier != 1) {
+            number = number.multiply(BigDecimal.valueOf(multiplier), mathContext);
+        }
+
+        if (actualRoundingIncrementICU != null) {
+            number = number.divide(actualRoundingIncrementICU, 0, roundingMode)
+                .multiply(actualRoundingIncrementICU, mathContext);
+        }
+
+        synchronized (digitList) {
+            digitList.set(number, precision(false), !useExponentialNotation &&
+                          !areSignificantDigitsUsed());
+            // For issue 11808.
+            if (digitList.wasRounded() && roundingMode == BigDecimal.ROUND_UNNECESSARY) {
+                throw new ArithmeticException("Rounding necessary");
+            }
+            return subformat(number.doubleValue(), result, fieldPosition, number.signum() < 0,
+                             false, false);
+        }
+    }
+
+    /**
+     * Returns true if a grouping separator belongs at the given position, based on whether
+     * grouping is in use and the values of the primary and secondary grouping interval.
+     *
+     * @param pos the number of integer digits to the right of the current position. Zero
+     * indicates the position after the rightmost integer digit.
+     * @return true if a grouping character belongs at the current position.
+     */
+    private boolean isGroupingPosition(int pos) {
+        boolean result = false;
+        if (isGroupingUsed() && (pos > 0) && (groupingSize > 0)) {
+            if ((groupingSize2 > 0) && (pos > groupingSize)) {
+                result = ((pos - groupingSize) % groupingSize2) == 0;
+            } else {
+                result = pos % groupingSize == 0;
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Return the number of fraction digits to display, or the total
+     * number of digits for significant digit formats and exponential
+     * formats.
+     */
+    private int precision(boolean isIntegral) {
+        if (areSignificantDigitsUsed()) {
+            return getMaximumSignificantDigits();
+        } else if (useExponentialNotation) {
+            return getMinimumIntegerDigits() + getMaximumFractionDigits();
+        } else {
+            return isIntegral ? 0 : getMaximumFractionDigits();
+        }
+    }
+
+    private StringBuffer subformat(int number, StringBuffer result, FieldPosition fieldPosition,
+                                   boolean isNegative, boolean isInteger, boolean parseAttr) {
+        if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
+            // compute the plural category from the digitList plus other settings
+            return subformat(currencyPluralInfo.select(getFixedDecimal(number)),
+                             result, fieldPosition, isNegative,
+                             isInteger, parseAttr);
+        } else {
+            return subformat(result, fieldPosition, isNegative, isInteger, parseAttr);
+        }
+    }
+
+    /**
+     * This is ugly, but don't see a better way to do it without major restructuring of the code.
+     */
+    /*package*/ FixedDecimal getFixedDecimal(double number) {
+        // get the visible fractions and the number of fraction digits.
+       return getFixedDecimal(number, digitList);
+    }
+
+    FixedDecimal getFixedDecimal(double number, DigitList dl) {
+        int fractionalDigitsInDigitList = dl.count - dl.decimalAt;
+        int v;
+        long f;
+        int maxFractionalDigits;
+        int minFractionalDigits;
+        if (useSignificantDigits) {
+            maxFractionalDigits = maxSignificantDigits - dl.decimalAt;
+            minFractionalDigits = minSignificantDigits - dl.decimalAt;
+            if (minFractionalDigits < 0) {
+                minFractionalDigits = 0;
+            }
+            if (maxFractionalDigits < 0) {
+                maxFractionalDigits = 0;
+            }
+        } else {
+            maxFractionalDigits = getMaximumFractionDigits();
+            minFractionalDigits = getMinimumFractionDigits();
+        }
+        v = fractionalDigitsInDigitList;
+        if (v < minFractionalDigits) {
+            v = minFractionalDigits;
+        } else if (v > maxFractionalDigits) {
+            v = maxFractionalDigits;
+        }
+        f = 0;
+        if (v > 0) {
+            for (int i = Math.max(0, dl.decimalAt); i < dl.count; ++i) {
+                f *= 10;
+                f += (dl.digits[i] - '0');
+            }
+            for (int i = v; i < fractionalDigitsInDigitList; ++i) {
+                f *= 10;
+            }
+        }
+        return new FixedDecimal(number, v, f);
+    }
+
+    private StringBuffer subformat(double number, StringBuffer result, FieldPosition fieldPosition,
+                                   boolean isNegative,
+            boolean isInteger, boolean parseAttr) {
+        if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
+            // compute the plural category from the digitList plus other settings
+            return subformat(currencyPluralInfo.select(getFixedDecimal(number)),
+                             result, fieldPosition, isNegative,
+                             isInteger, parseAttr);
+        } else {
+            return subformat(result, fieldPosition, isNegative, isInteger, parseAttr);
+        }
+    }
+
+    private StringBuffer subformat(String pluralCount, StringBuffer result, FieldPosition fieldPosition,
+            boolean isNegative, boolean isInteger, boolean parseAttr) {
+        // There are 2 ways to activate currency plural format: by applying a pattern with
+        // 3 currency sign directly, or by instantiate a decimal formatter using
+        // PLURALCURRENCYSTYLE.  For both cases, the number of currency sign in the
+        // pattern is 3.  Even if the number of currency sign in the pattern is 3, it does
+        // not mean we need to reset the pattern.  For 1st case, we do not need to reset
+        // pattern.  For 2nd case, we might need to reset pattern, if the default pattern
+        // (corresponding to plural count 'other') we use is different from the pattern
+        // based on 'pluralCount'.
+        //
+        // style is only valid when decimal formatter is constructed through
+        // DecimalFormat(pattern, symbol, style)
+        if (style == NumberFormat.PLURALCURRENCYSTYLE) {
+            // May need to reset pattern if the style is PLURALCURRENCYSTYLE.
+            String currencyPluralPattern = currencyPluralInfo.getCurrencyPluralPattern(pluralCount);
+            if (formatPattern.equals(currencyPluralPattern) == false) {
+                applyPatternWithoutExpandAffix(currencyPluralPattern, false);
+            }
+        }
+        // Expand the affix to the right name according to the plural rule.  This is only
+        // used for currency plural formatting.  Currency plural name is not a fixed
+        // static one, it is a dynamic name based on the currency plural count.  So, the
+        // affixes need to be expanded here.  For other cases, the affix is a static one
+        // based on pattern alone, and it is already expanded during applying pattern, or
+        // setDecimalFormatSymbols, or setCurrency.
+        expandAffixAdjustWidth(pluralCount);
+        return subformat(result, fieldPosition, isNegative, isInteger, parseAttr);
+    }
+
+    /**
+     * Complete the formatting of a finite number. On entry, the
+     * digitList must be filled in with the correct digits.
+     */
+    private StringBuffer subformat(StringBuffer result, FieldPosition fieldPosition,
+                                   boolean isNegative, boolean isInteger, boolean parseAttr) {
+        // NOTE: This isn't required anymore because DigitList takes care of this.
+        //
+        // // The negative of the exponent represents the number of leading // zeros
+        // between the decimal and the first non-zero digit, for // a value < 0.1 (e.g.,
+        // for 0.00123, -fExponent == 2). If this // is more than the maximum fraction
+        // digits, then we have an underflow // for the printed representation. We
+        // recognize this here and set // the DigitList representation to zero in this
+        // situation.
+        //
+        // if (-digitList.decimalAt >= getMaximumFractionDigits())
+        // {
+        // digitList.count = 0;
+        // }
+
+
+
+        // Per bug 4147706, DecimalFormat must respect the sign of numbers which format as
+        // zero. This allows sensible computations and preserves relations such as
+        // signum(1/x) = signum(x), where x is +Infinity or -Infinity.  Prior to this fix,
+        // we always formatted zero values as if they were positive. Liu 7/6/98.
+        if (digitList.isZero()) {
+            digitList.decimalAt = 0; // Normalize
+        }
+
+        int prefixLen = appendAffix(result, isNegative, true, fieldPosition, parseAttr);
+
+        if (useExponentialNotation) {
+            subformatExponential(result, fieldPosition, parseAttr);
+        } else {
+            subformatFixed(result, fieldPosition, isInteger, parseAttr);
+        }
+
+        int suffixLen = appendAffix(result, isNegative, false, fieldPosition, parseAttr);
+        addPadding(result, fieldPosition, prefixLen, suffixLen);
+        return result;
+    }
+
+    private void subformatFixed(StringBuffer result,
+            FieldPosition fieldPosition,
+            boolean isInteger,
+            boolean parseAttr) {
+        String[] digits = symbols.getDigitStrings();
+
+        String grouping = currencySignCount == CURRENCY_SIGN_COUNT_ZERO ?
+                symbols.getGroupingSeparatorString(): symbols.getMonetaryGroupingSeparatorString();
+        String decimal = currencySignCount == CURRENCY_SIGN_COUNT_ZERO ?
+                symbols.getDecimalSeparatorString() : symbols.getMonetaryDecimalSeparatorString();
+        boolean useSigDig = areSignificantDigitsUsed();
+        int maxIntDig = getMaximumIntegerDigits();
+        int minIntDig = getMinimumIntegerDigits();
+        int i;
+        // [Spark/CDL] Record the integer start index.
+        int intBegin = result.length();
+        // Record field information for caller.
+        if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD ||
+                fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
+            fieldPosition.setBeginIndex(intBegin);
+        }
+        long fractionalDigits = 0;
+        int fractionalDigitsCount = 0;
+        boolean recordFractionDigits = false;
+
+        int sigCount = 0;
+        int minSigDig = getMinimumSignificantDigits();
+        int maxSigDig = getMaximumSignificantDigits();
+        if (!useSigDig) {
+            minSigDig = 0;
+            maxSigDig = Integer.MAX_VALUE;
+        }
+
+        // Output the integer portion. Here 'count' is the total number of integer
+        // digits we will display, including both leading zeros required to satisfy
+        // getMinimumIntegerDigits, and actual digits present in the number.
+        int count = useSigDig ? Math.max(1, digitList.decimalAt) : minIntDig;
+        if (digitList.decimalAt > 0 && count < digitList.decimalAt) {
+            count = digitList.decimalAt;
+        }
+
+        // Handle the case where getMaximumIntegerDigits() is smaller than the real
+        // number of integer digits. If this is so, we output the least significant
+        // max integer digits. For example, the value 1997 printed with 2 max integer
+        // digits is just "97".
+
+        int digitIndex = 0; // Index into digitList.fDigits[]
+        if (count > maxIntDig && maxIntDig >= 0) {
+            count = maxIntDig;
+            digitIndex = digitList.decimalAt - count;
+        }
+
+        int sizeBeforeIntegerPart = result.length();
+        for (i = count - 1; i >= 0; --i) {
+            if (i < digitList.decimalAt && digitIndex < digitList.count
+                && sigCount < maxSigDig) {
+                // Output a real digit
+                result.append(digits[digitList.getDigitValue(digitIndex++)]);
+                ++sigCount;
+            } else {
+                // Output a zero (leading or trailing)
+                result.append(digits[0]);
+                if (sigCount > 0) {
+                    ++sigCount;
+                }
+            }
+
+            // Output grouping separator if necessary.
+            if (isGroupingPosition(i)) {
+                result.append(grouping);
+                // [Spark/CDL] Add grouping separator attribute here.
+                // Set only for the first instance.
+                // Length of grouping separator is 1.
+                if (fieldPosition.getFieldAttribute() == Field.GROUPING_SEPARATOR &&
+                        fieldPosition.getBeginIndex() == 0 && fieldPosition.getEndIndex() == 0) {
+                    fieldPosition.setBeginIndex(result.length()-1);
+                    fieldPosition.setEndIndex(result.length());
+                }
+                if (parseAttr) {
+                    addAttribute(Field.GROUPING_SEPARATOR, result.length() - 1, result.length());
+                }
+            }
+        }
+
+        // Record field information for caller.
+        if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD ||
+                fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
+            fieldPosition.setEndIndex(result.length());
+        }
+
+        // This handles the special case of formatting 0. For zero only, we count the
+        // zero to the left of the decimal point as one signficant digit. Ordinarily we
+        // do not count any leading 0's as significant. If the number we are formatting
+        // is not zero, then either sigCount or digits.getCount() will be non-zero.
+        if (sigCount == 0 && digitList.count == 0) {
+          sigCount = 1;
+        }
+
+        // Determine whether or not there are any printable fractional digits. If
+        // we've used up the digits we know there aren't.
+        boolean fractionPresent = (!isInteger && digitIndex < digitList.count)
+                || (useSigDig ? (sigCount < minSigDig) : (getMinimumFractionDigits() > 0));
+
+        // If there is no fraction present, and we haven't printed any integer digits,
+        // then print a zero. Otherwise we won't print _any_ digits, and we won't be
+        // able to parse this string.
+        if (!fractionPresent && result.length() == sizeBeforeIntegerPart)
+            result.append(digits[0]);
+        // [Spark/CDL] Add attribute for integer part.
+        if (parseAttr) {
+            addAttribute(Field.INTEGER, intBegin, result.length());
+        }
+        // Output the decimal separator if we always do so.
+        if (decimalSeparatorAlwaysShown || fractionPresent) {
+            if (fieldPosition.getFieldAttribute() == Field.DECIMAL_SEPARATOR) {
+                fieldPosition.setBeginIndex(result.length());
+            }
+            result.append(decimal);
+            if (fieldPosition.getFieldAttribute() == Field.DECIMAL_SEPARATOR) {
+                fieldPosition.setEndIndex(result.length());
+            }
+            // [Spark/CDL] Add attribute for decimal separator
+            if (parseAttr) {
+                addAttribute(Field.DECIMAL_SEPARATOR, result.length() - 1, result.length());
+            }
+        }
+
+        // Record field information for caller.
+        if (fieldPosition.getField() == NumberFormat.FRACTION_FIELD) {
+            fieldPosition.setBeginIndex(result.length());
+        } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.FRACTION) {
+            fieldPosition.setBeginIndex(result.length());
+        }
+
+        // [Spark/CDL] Record the begin index of fraction part.
+        int fracBegin = result.length();
+        recordFractionDigits = fieldPosition instanceof UFieldPosition;
+
+        count = useSigDig ? Integer.MAX_VALUE : getMaximumFractionDigits();
+        if (useSigDig && (sigCount == maxSigDig ||
+                          (sigCount >= minSigDig && digitIndex == digitList.count))) {
+            count = 0;
+        }
+        for (i = 0; i < count; ++i) {
+            // Here is where we escape from the loop. We escape if we've output the
+            // maximum fraction digits (specified in the for expression above). We
+            // also stop when we've output the minimum digits and either: we have an
+            // integer, so there is no fractional stuff to display, or we're out of
+            // significant digits.
+            if (!useSigDig && i >= getMinimumFractionDigits() &&
+                (isInteger || digitIndex >= digitList.count)) {
+                break;
+            }
+
+            // Output leading fractional zeros. These are zeros that come after the
+            // decimal but before any significant digits. These are only output if
+            // abs(number being formatted) < 1.0.
+            if (-1 - i > (digitList.decimalAt - 1)) {
+                result.append(digits[0]);
+                if (recordFractionDigits) {
+                    ++fractionalDigitsCount;
+                    fractionalDigits *= 10;
+                }
+                continue;
+            }
+
+            // Output a digit, if we have any precision left, or a zero if we
+            // don't. We don't want to output noise digits.
+            if (!isInteger && digitIndex < digitList.count) {
+                byte digit = digitList.getDigitValue(digitIndex++);
+                result.append(digits[digit]);
+                if (recordFractionDigits) {
+                    ++fractionalDigitsCount;
+                    fractionalDigits *= 10;
+                    fractionalDigits += digit;
+                }
+            } else {
+                result.append(digits[0]);
+                if (recordFractionDigits) {
+                    ++fractionalDigitsCount;
+                    fractionalDigits *= 10;
+                }
+            }
+
+            // If we reach the maximum number of significant digits, or if we output
+            // all the real digits and reach the minimum, then we are done.
+            ++sigCount;
+            if (useSigDig && (sigCount == maxSigDig ||
+                              (digitIndex == digitList.count && sigCount >= minSigDig))) {
+                break;
+            }
+        }
+
+        // Record field information for caller.
+        if (fieldPosition.getField() == NumberFormat.FRACTION_FIELD) {
+            fieldPosition.setEndIndex(result.length());
+        } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.FRACTION) {
+            fieldPosition.setEndIndex(result.length());
+        }
+        if (recordFractionDigits) {
+            ((UFieldPosition) fieldPosition).setFractionDigits(fractionalDigitsCount, fractionalDigits);
+        }
+
+        // [Spark/CDL] Add attribute information if necessary.
+        if (parseAttr && (decimalSeparatorAlwaysShown || fractionPresent)) {
+            addAttribute(Field.FRACTION, fracBegin, result.length());
+        }
+    }
+
+    private void subformatExponential(StringBuffer result,
+            FieldPosition fieldPosition,
+            boolean parseAttr) {
+        String[] digits = symbols.getDigitStringsLocal();
+        String decimal = currencySignCount == CURRENCY_SIGN_COUNT_ZERO ?
+                symbols.getDecimalSeparatorString() : symbols.getMonetaryDecimalSeparatorString();
+        boolean useSigDig = areSignificantDigitsUsed();
+        int maxIntDig = getMaximumIntegerDigits();
+        int minIntDig = getMinimumIntegerDigits();
+        int i;
+        // Record field information for caller.
+        if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
+            fieldPosition.setBeginIndex(result.length());
+            fieldPosition.setEndIndex(-1);
+        } else if (fieldPosition.getField() == NumberFormat.FRACTION_FIELD) {
+            fieldPosition.setBeginIndex(-1);
+        } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
+            fieldPosition.setBeginIndex(result.length());
+            fieldPosition.setEndIndex(-1);
+        } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.FRACTION) {
+            fieldPosition.setBeginIndex(-1);
+        }
+
+        // [Spark/CDL]
+        // the begin index of integer part
+        // the end index of integer part
+        // the begin index of fractional part
+        int intBegin = result.length();
+        int intEnd = -1;
+        int fracBegin = -1;
+        int minFracDig = 0;
+        if (useSigDig) {
+            maxIntDig = minIntDig = 1;
+            minFracDig = getMinimumSignificantDigits() - 1;
+        } else {
+            minFracDig = getMinimumFractionDigits();
+            if (maxIntDig > MAX_SCIENTIFIC_INTEGER_DIGITS) {
+                maxIntDig = 1;
+                if (maxIntDig < minIntDig) {
+                    maxIntDig = minIntDig;
+                }
+            }
+            if (maxIntDig > minIntDig) {
+                minIntDig = 1;
+            }
+        }
+        long fractionalDigits = 0;
+        int fractionalDigitsCount = 0;
+        boolean recordFractionDigits = false;
+
+        // Minimum integer digits are handled in exponential format by adjusting the
+        // exponent. For example, 0.01234 with 3 minimum integer digits is "123.4E-4".
+
+        // Maximum integer digits are interpreted as indicating the repeating
+        // range. This is useful for engineering notation, in which the exponent is
+        // restricted to a multiple of 3. For example, 0.01234 with 3 maximum integer
+        // digits is "12.34e-3".  If maximum integer digits are defined and are larger
+        // than minimum integer digits, then minimum integer digits are ignored.
+
+        int exponent = digitList.decimalAt;
+        if (maxIntDig > 1 && maxIntDig != minIntDig) {
+            // A exponent increment is defined; adjust to it.
+            exponent = (exponent > 0) ? (exponent - 1) / maxIntDig : (exponent / maxIntDig) - 1;
+            exponent *= maxIntDig;
+        } else {
+            // No exponent increment is defined; use minimum integer digits.
+            // If none is specified, as in "#E0", generate 1 integer digit.
+            exponent -= (minIntDig > 0 || minFracDig > 0) ? minIntDig : 1;
+        }
+
+        // We now output a minimum number of digits, and more if there are more
+        // digits, up to the maximum number of digits. We place the decimal point
+        // after the "integer" digits, which are the first (decimalAt - exponent)
+        // digits.
+        int minimumDigits = minIntDig + minFracDig;
+        // The number of integer digits is handled specially if the number
+        // is zero, since then there may be no digits.
+        int integerDigits = digitList.isZero() ? minIntDig : digitList.decimalAt - exponent;
+        int totalDigits = digitList.count;
+        if (minimumDigits > totalDigits)
+            totalDigits = minimumDigits;
+        if (integerDigits > totalDigits)
+            totalDigits = integerDigits;
+
+        for (i = 0; i < totalDigits; ++i) {
+            if (i == integerDigits) {
+                // Record field information for caller.
+                if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
+                    fieldPosition.setEndIndex(result.length());
+                } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
+                    fieldPosition.setEndIndex(result.length());
+                }
+
+                // [Spark/CDL] Add attribute for integer part
+                if (parseAttr) {
+                    intEnd = result.length();
+                    addAttribute(Field.INTEGER, intBegin, result.length());
+                }
+                if (fieldPosition.getFieldAttribute() == Field.DECIMAL_SEPARATOR) {
+                    fieldPosition.setBeginIndex(result.length());
+                }
+                result.append(decimal);
+                if (fieldPosition.getFieldAttribute() == Field.DECIMAL_SEPARATOR) {
+                    fieldPosition.setEndIndex(result.length());
+                }
+                // [Spark/CDL] Add attribute for decimal separator
+                fracBegin = result.length();
+                if (parseAttr) {
+                    // Length of decimal separator is 1.
+                    int decimalSeparatorBegin = result.length() - 1;
+                    addAttribute(Field.DECIMAL_SEPARATOR, decimalSeparatorBegin,
+                                 result.length());
+                }
+                // Record field information for caller.
+                if (fieldPosition.getField() == NumberFormat.FRACTION_FIELD) {
+                    fieldPosition.setBeginIndex(result.length());
+                } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.FRACTION) {
+                    fieldPosition.setBeginIndex(result.length());
+                }
+                recordFractionDigits = fieldPosition instanceof UFieldPosition;
+
+            }
+            byte digit = (i < digitList.count) ? digitList.getDigitValue(i) : (byte)0;
+            result.append(digits[digit]);
+            if (recordFractionDigits) {
+                ++fractionalDigitsCount;
+                fractionalDigits *= 10;
+                fractionalDigits += digit;
+            }
+        }
+
+        // For ICU compatibility and format 0 to 0E0 with pattern "#E0" [Richard/GCL]
+        if (digitList.isZero() && (totalDigits == 0)) {
+            result.append(digits[0]);
+        }
+
+        // add the decimal separator if it is to be always shown AND there are no decimal digits
+        if ((fracBegin == -1) && this.decimalSeparatorAlwaysShown) {
+            if (fieldPosition.getFieldAttribute() == Field.DECIMAL_SEPARATOR) {
+                fieldPosition.setBeginIndex(result.length());
+            }
+            result.append(decimal);
+            if (fieldPosition.getFieldAttribute() == Field.DECIMAL_SEPARATOR) {
+                fieldPosition.setEndIndex(result.length());
+            }
+            if (parseAttr) {
+                // Length of decimal separator is 1.
+                int decimalSeparatorBegin = result.length() - 1;
+                addAttribute(Field.DECIMAL_SEPARATOR, decimalSeparatorBegin, result.length());
+            }
+        }
+
+        // Record field information
+        if (fieldPosition.getField() == NumberFormat.INTEGER_FIELD) {
+            if (fieldPosition.getEndIndex() < 0) {
+                fieldPosition.setEndIndex(result.length());
+            }
+        } else if (fieldPosition.getField() == NumberFormat.FRACTION_FIELD) {
+            if (fieldPosition.getBeginIndex() < 0) {
+                fieldPosition.setBeginIndex(result.length());
+            }
+            fieldPosition.setEndIndex(result.length());
+        } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.INTEGER) {
+            if (fieldPosition.getEndIndex() < 0) {
+                fieldPosition.setEndIndex(result.length());
+            }
+        } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.FRACTION) {
+            if (fieldPosition.getBeginIndex() < 0) {
+                fieldPosition.setBeginIndex(result.length());
+            }
+            fieldPosition.setEndIndex(result.length());
+        }
+        if (recordFractionDigits) {
+            ((UFieldPosition) fieldPosition).setFractionDigits(fractionalDigitsCount, fractionalDigits);
+        }
+
+        // [Spark/CDL] Calculate the end index of integer part and fractional
+        // part if they are not properly processed yet.
+        if (parseAttr) {
+            if (intEnd < 0) {
+                addAttribute(Field.INTEGER, intBegin, result.length());
+            }
+            if (fracBegin > 0) {
+                addAttribute(Field.FRACTION, fracBegin, result.length());
+            }
+        }
+
+        // The exponent is output using the pattern-specified minimum exponent
+        // digits. There is no maximum limit to the exponent digits, since truncating
+        // the exponent would result in an unacceptable inaccuracy.
+        if (fieldPosition.getFieldAttribute() == Field.EXPONENT_SYMBOL) {
+            fieldPosition.setBeginIndex(result.length());
+        }
+
+        result.append(symbols.getExponentSeparator());
+        if (fieldPosition.getFieldAttribute() == Field.EXPONENT_SYMBOL) {
+            fieldPosition.setEndIndex(result.length());
+        }
+        // [Spark/CDL] For exponent symbol, add an attribute.
+        if (parseAttr) {
+            addAttribute(Field.EXPONENT_SYMBOL, result.length() -
+                         symbols.getExponentSeparator().length(), result.length());
+        }
+        // For zero values, we force the exponent to zero. We must do this here, and
+        // not earlier, because the value is used to determine integer digit count
+        // above.
+        if (digitList.isZero())
+            exponent = 0;
+
+        boolean negativeExponent = exponent < 0;
+        if (negativeExponent) {
+            exponent = -exponent;
+            if (fieldPosition.getFieldAttribute() == Field.EXPONENT_SIGN) {
+                fieldPosition.setBeginIndex(result.length());
+            }
+            result.append(symbols.getMinusSignString());
+            if (fieldPosition.getFieldAttribute() == Field.EXPONENT_SIGN) {
+                fieldPosition.setEndIndex(result.length());
+            }
+            // [Spark/CDL] If exponent has sign, then add an exponent sign
+            // attribute.
+            if (parseAttr) {
+                // Length of exponent sign is 1.
+                addAttribute(Field.EXPONENT_SIGN, result.length() - 1, result.length());
+            }
+        } else if (exponentSignAlwaysShown) {
+            if (fieldPosition.getFieldAttribute() == Field.EXPONENT_SIGN) {
+                fieldPosition.setBeginIndex(result.length());
+            }
+            result.append(symbols.getPlusSignString());
+            if (fieldPosition.getFieldAttribute() == Field.EXPONENT_SIGN) {
+                fieldPosition.setEndIndex(result.length());
+            }
+            // [Spark/CDL] Add an plus sign attribute.
+            if (parseAttr) {
+                // Length of exponent sign is 1.
+                int expSignBegin = result.length() - 1;
+                addAttribute(Field.EXPONENT_SIGN, expSignBegin, result.length());
+            }
+        }
+        int expBegin = result.length();
+        digitList.set(exponent);
+        {
+            int expDig = minExponentDigits;
+            if (useExponentialNotation && expDig < 1) {
+                expDig = 1;
+            }
+            for (i = digitList.decimalAt; i < expDig; ++i)
+                result.append(digits[0]);
+        }
+        for (i = 0; i < digitList.decimalAt; ++i) {
+            result.append((i < digitList.count) ? digits[digitList.getDigitValue(i)]
+                          : digits[0]);
+        }
+        // [Spark/CDL] Add attribute for exponent part.
+        if (fieldPosition.getFieldAttribute() == Field.EXPONENT) {
+            fieldPosition.setBeginIndex(expBegin);
+            fieldPosition.setEndIndex(result.length());
+        }
+        if (parseAttr) {
+            addAttribute(Field.EXPONENT, expBegin, result.length());
+        }
+    }
+
+    private final void addPadding(StringBuffer result, FieldPosition fieldPosition, int prefixLen,
+                                  int suffixLen) {
+        if (formatWidth > 0) {
+            int len = formatWidth - result.length();
+            if (len > 0) {
+                char[] padding = new char[len];
+                for (int i = 0; i < len; ++i) {
+                    padding[i] = pad;
+                }
+                switch (padPosition) {
+                case PAD_AFTER_PREFIX:
+                    result.insert(prefixLen, padding);
+                    break;
+                case PAD_BEFORE_PREFIX:
+                    result.insert(0, padding);
+                    break;
+                case PAD_BEFORE_SUFFIX:
+                    result.insert(result.length() - suffixLen, padding);
+                    break;
+                case PAD_AFTER_SUFFIX:
+                    result.append(padding);
+                    break;
+                }
+                if (padPosition == PAD_BEFORE_PREFIX || padPosition == PAD_AFTER_PREFIX) {
+                    fieldPosition.setBeginIndex(fieldPosition.getBeginIndex() + len);
+                    fieldPosition.setEndIndex(fieldPosition.getEndIndex() + len);
+                }
+            }
+        }
+    }
+
+    /**
+     * Parses the given string, returning a <code>Number</code> object to represent the
+     * parsed value. <code>Double</code> objects are returned to represent non-integral
+     * values which cannot be stored in a <code>BigDecimal</code>. These are
+     * <code>NaN</code>, infinity, -infinity, and -0.0. If {@link #isParseBigDecimal()} is
+     * false (the default), all other values are returned as <code>Long</code>,
+     * <code>BigInteger</code>, or <code>BigDecimal</code> values, in that order of
+     * preference. If {@link #isParseBigDecimal()} is true, all other values are returned
+     * as <code>BigDecimal</code> valuse. If the parse fails, null is returned.
+     *
+     * @param text the string to be parsed
+     * @param parsePosition defines the position where parsing is to begin, and upon
+     * return, the position where parsing left off. If the position has not changed upon
+     * return, then parsing failed.
+     * @return a <code>Number</code> object with the parsed value or
+     * <code>null</code> if the parse failed
+     * @stable ICU 2.0
+     */
+    @Override
+    public Number parse(String text, ParsePosition parsePosition) {
+        return (Number) parse(text, parsePosition, null);
+    }
+
+    /**
+     * Parses text from the given string as a CurrencyAmount. Unlike the parse() method,
+     * this method will attempt to parse a generic currency name, searching for a match of
+     * this object's locale's currency display names, or for a 3-letter ISO currency
+     * code. This method will fail if this format is not a currency format, that is, if it
+     * does not contain the currency pattern symbol (U+00A4) in its prefix or suffix.
+     *
+     * @param text the text to parse
+     * @param pos input-output position; on input, the position within text to match; must
+     *  have 0 &lt;= pos.getIndex() &lt; text.length(); on output, the position after the last
+     *  matched character. If the parse fails, the position in unchanged upon output.
+     * @return a CurrencyAmount, or null upon failure
+     * @stable ICU 49
+     */
+    @Override
+    public CurrencyAmount parseCurrency(CharSequence text, ParsePosition pos) {
+        Currency[] currency = new Currency[1];
+        return (CurrencyAmount) parse(text.toString(), pos, currency);
+    }
+
+    /**
+     * Parses the given text as either a Number or a CurrencyAmount.
+     *
+     * @param text the string to parse
+     * @param parsePosition input-output position; on input, the position within text to
+     * match; must have 0 <= pos.getIndex() < text.length(); on output, the position after
+     * the last matched character. If the parse fails, the position in unchanged upon
+     * output.
+     * @param currency if non-null, a CurrencyAmount is parsed and returned; otherwise a
+     * Number is parsed and returned
+     * @return a Number or CurrencyAmount or null
+     */
+    private Object parse(String text, ParsePosition parsePosition, Currency[] currency) {
+        int backup;
+        int i = backup = parsePosition.getIndex();
+
+        // Handle NaN as a special case:
+
+        // Skip padding characters, if around prefix
+        if (formatWidth > 0 &&
+            (padPosition == PAD_BEFORE_PREFIX || padPosition == PAD_AFTER_PREFIX)) {
+            i = skipPadding(text, i);
+        }
+        if (text.regionMatches(i, symbols.getNaN(), 0, symbols.getNaN().length())) {
+            i += symbols.getNaN().length();
+            // Skip padding characters, if around suffix
+            if (formatWidth > 0 && (padPosition == PAD_BEFORE_SUFFIX ||
+                                    padPosition == PAD_AFTER_SUFFIX)) {
+                i = skipPadding(text, i);
+            }
+            parsePosition.setIndex(i);
+            return new Double(Double.NaN);
+        }
+
+        // NaN parse failed; start over
+        i = backup;
+
+        boolean[] status = new boolean[STATUS_LENGTH];
+        if (currencySignCount != CURRENCY_SIGN_COUNT_ZERO) {
+            if (!parseForCurrency(text, parsePosition, currency, status)) {
+                return null;
+            }
+        } else if (currency != null) {
+            return null;
+        } else {
+            if (!subparse(text, parsePosition, digitList, status, currency, negPrefixPattern,
+                          negSuffixPattern, posPrefixPattern, posSuffixPattern,
+                          false, Currency.SYMBOL_NAME)) {
+                parsePosition.setIndex(backup);
+                return null;
+            }
+        }
+
+        Number n = null;
+
+        // Handle infinity
+        if (status[STATUS_INFINITE]) {
+            n = new Double(status[STATUS_POSITIVE] ? Double.POSITIVE_INFINITY :
+                           Double.NEGATIVE_INFINITY);
+        }
+
+        // Handle underflow
+        else if (status[STATUS_UNDERFLOW]) {
+            n = status[STATUS_POSITIVE] ? new Double("0.0") : new Double("-0.0");
+        }
+
+        // Handle -0.0
+        else if (!status[STATUS_POSITIVE] && digitList.isZero()) {
+            n = new Double("-0.0");
+        }
+
+        else {
+            // Do as much of the multiplier conversion as possible without
+            // losing accuracy.
+            int mult = multiplier; // Don't modify this.multiplier
+            while (mult % 10 == 0) {
+                --digitList.decimalAt;
+                mult /= 10;
+            }
+
+            // Handle integral values
+            if (!parseBigDecimal && mult == 1 && digitList.isIntegral()) {
+                // hack quick long
+                if (digitList.decimalAt < 12) { // quick check for long
+                    long l = 0;
+                    if (digitList.count > 0) {
+                        int nx = 0;
+                        while (nx < digitList.count) {
+                            l = l * 10 + (char) digitList.digits[nx++] - '0';
+                        }
+                        while (nx++ < digitList.decimalAt) {
+                            l *= 10;
+                        }
+                        if (!status[STATUS_POSITIVE]) {
+                            l = -l;
+                        }
+                    }
+                    n = Long.valueOf(l);
+                } else {
+                    BigInteger big = digitList.getBigInteger(status[STATUS_POSITIVE]);
+                    n = (big.bitLength() < 64) ? (Number) Long.valueOf(big.longValue()) : (Number) big;
+                }
+            }
+            // Handle non-integral values or the case where parseBigDecimal is set
+            else {
+                BigDecimal big = digitList.getBigDecimalICU(status[STATUS_POSITIVE]);
+                n = big;
+                if (mult != 1) {
+                    n = big.divide(BigDecimal.valueOf(mult), mathContext);
+                }
+            }
+        }
+
+        // Assemble into CurrencyAmount if necessary
+        return (currency != null) ? (Object) new CurrencyAmount(n, currency[0]) : (Object) n;
+    }
+
+    private boolean parseForCurrency(String text, ParsePosition parsePosition,
+            Currency[] currency, boolean[] status) {
+        int origPos = parsePosition.getIndex();
+        if (!isReadyForParsing) {
+            int savedCurrencySignCount = currencySignCount;
+            setupCurrencyAffixForAllPatterns();
+            // reset pattern back
+            if (savedCurrencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
+                applyPatternWithoutExpandAffix(formatPattern, false);
+            } else {
+                applyPattern(formatPattern, false);
+            }
+            isReadyForParsing = true;
+        }
+        int maxPosIndex = origPos;
+        int maxErrorPos = -1;
+        boolean[] savedStatus = null;
+        // First, parse against current pattern.
+        // Since current pattern could be set by applyPattern(),
+        // it could be an arbitrary pattern, and it may not be the one
+        // defined in current locale.
+        boolean[] tmpStatus = new boolean[STATUS_LENGTH];
+        ParsePosition tmpPos = new ParsePosition(origPos);
+        DigitList tmpDigitList = new DigitList();
+        boolean found;
+        if (style == NumberFormat.PLURALCURRENCYSTYLE) {
+            found = subparse(text, tmpPos, tmpDigitList, tmpStatus, currency,
+                             negPrefixPattern, negSuffixPattern, posPrefixPattern, posSuffixPattern,
+                             true, Currency.LONG_NAME);
+        } else {
+            found = subparse(text, tmpPos, tmpDigitList, tmpStatus, currency,
+                             negPrefixPattern, negSuffixPattern, posPrefixPattern, posSuffixPattern,
+                             true, Currency.SYMBOL_NAME);
+        }
+        if (found) {
+            if (tmpPos.getIndex() > maxPosIndex) {
+                maxPosIndex = tmpPos.getIndex();
+                savedStatus = tmpStatus;
+                digitList = tmpDigitList;
+            }
+        } else {
+            maxErrorPos = tmpPos.getErrorIndex();
+        }
+        // Then, parse against affix patterns.  Those are currency patterns and currency
+        // plural patterns defined in the locale.
+        for (AffixForCurrency affix : affixPatternsForCurrency) {
+            tmpStatus = new boolean[STATUS_LENGTH];
+            tmpPos = new ParsePosition(origPos);
+            tmpDigitList = new DigitList();
+            boolean result = subparse(text, tmpPos, tmpDigitList, tmpStatus, currency,
+                                      affix.getNegPrefix(), affix.getNegSuffix(),
+                                      affix.getPosPrefix(), affix.getPosSuffix(),
+                                      true, affix.getPatternType());
+            if (result) {
+                found = true;
+                if (tmpPos.getIndex() > maxPosIndex) {
+                    maxPosIndex = tmpPos.getIndex();
+                    savedStatus = tmpStatus;
+                    digitList = tmpDigitList;
+                }
+            } else {
+                maxErrorPos = (tmpPos.getErrorIndex() > maxErrorPos) ? tmpPos.getErrorIndex()
+                    : maxErrorPos;
+            }
+        }
+        // Finally, parse against simple affix to find the match.  For example, in
+        // TestMonster suite, if the to-be-parsed text is "-\u00A40,00".
+        // complexAffixCompare will not find match, since there is no ISO code matches
+        // "\u00A4", and the parse stops at "\u00A4".  We will just use simple affix
+        // comparison (look for exact match) to pass it.
+        //
+        // TODO: We should parse against simple affix first when
+        // output currency is not requested. After the complex currency
+        // parsing implementation was introduced, the default currency
+        // instance parsing slowed down because of the new code flow.
+        // I filed #10312 - Yoshito
+        tmpStatus = new boolean[STATUS_LENGTH];
+        tmpPos = new ParsePosition(origPos);
+        tmpDigitList = new DigitList();
+
+        // Disable complex currency parsing and try it again.
+        boolean result = subparse(text, tmpPos, tmpDigitList, tmpStatus, currency,
+                                  negativePrefix, negativeSuffix, positivePrefix, positiveSuffix,
+                                  false /* disable complex currency parsing */, Currency.SYMBOL_NAME);
+        if (result) {
+            if (tmpPos.getIndex() > maxPosIndex) {
+                maxPosIndex = tmpPos.getIndex();
+                savedStatus = tmpStatus;
+                digitList = tmpDigitList;
+            }
+            found = true;
+        } else {
+            maxErrorPos = (tmpPos.getErrorIndex() > maxErrorPos) ? tmpPos.getErrorIndex() :
+                maxErrorPos;
+        }
+
+        if (!found) {
+            // parsePosition.setIndex(origPos);
+            parsePosition.setErrorIndex(maxErrorPos);
+        } else {
+            parsePosition.setIndex(maxPosIndex);
+            parsePosition.setErrorIndex(-1);
+            for (int index = 0; index < STATUS_LENGTH; ++index) {
+                status[index] = savedStatus[index];
+            }
+        }
+        return found;
+    }
+
+    // Get affix patterns used in locale's currency pattern (NumberPatterns[1]) and
+    // currency plural pattern (CurrencyUnitPatterns).
+    private void setupCurrencyAffixForAllPatterns() {
+        if (currencyPluralInfo == null) {
+            currencyPluralInfo = new CurrencyPluralInfo(symbols.getULocale());
+        }
+        affixPatternsForCurrency = new HashSet<AffixForCurrency>();
+
+        // save the current pattern, since it will be changed by
+        // applyPatternWithoutExpandAffix
+        String savedFormatPattern = formatPattern;
+
+        // CURRENCYSTYLE and ISOCURRENCYSTYLE should have the same prefix and suffix, so,
+        // only need to save one of them.  Here, chose onlyApplyPatternWithoutExpandAffix
+        // without saving the actualy pattern in 'pattern' data member.  TODO: is it uloc?
+        applyPatternWithoutExpandAffix(getPattern(symbols.getULocale(), NumberFormat.CURRENCYSTYLE),
+                                       false);
+        AffixForCurrency affixes = new AffixForCurrency(
+            negPrefixPattern, negSuffixPattern, posPrefixPattern, posSuffixPattern,
+            Currency.SYMBOL_NAME);
+        affixPatternsForCurrency.add(affixes);
+
+        // add plural pattern
+        Iterator<String> iter = currencyPluralInfo.pluralPatternIterator();
+        Set<String> currencyUnitPatternSet = new HashSet<String>();
+        while (iter.hasNext()) {
+            String pluralCount = iter.next();
+            String currencyPattern = currencyPluralInfo.getCurrencyPluralPattern(pluralCount);
+            if (currencyPattern != null &&
+                currencyUnitPatternSet.contains(currencyPattern) == false) {
+                currencyUnitPatternSet.add(currencyPattern);
+                applyPatternWithoutExpandAffix(currencyPattern, false);
+                affixes = new AffixForCurrency(negPrefixPattern, negSuffixPattern, posPrefixPattern,
+                                               posSuffixPattern, Currency.LONG_NAME);
+                affixPatternsForCurrency.add(affixes);
+            }
+        }
+        // reset pattern back
+        formatPattern = savedFormatPattern;
+    }
+
+    // currency formatting style options
+    private static final int CURRENCY_SIGN_COUNT_ZERO = 0;
+    private static final int CURRENCY_SIGN_COUNT_IN_SYMBOL_FORMAT = 1;
+    private static final int CURRENCY_SIGN_COUNT_IN_ISO_FORMAT = 2;
+    private static final int CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT = 3;
+
+    private static final int STATUS_INFINITE = 0;
+    private static final int STATUS_POSITIVE = 1;
+    private static final int STATUS_UNDERFLOW = 2;
+    private static final int STATUS_LENGTH = 3;
+
+    private static final UnicodeSet dotEquivalents = new UnicodeSet(
+            //"[.\u2024\u3002\uFE12\uFE52\uFF0E\uFF61]"
+            0x002E, 0x002E,
+            0x2024, 0x2024,
+            0x3002, 0x3002,
+            0xFE12, 0xFE12,
+            0xFE52, 0xFE52,
+            0xFF0E, 0xFF0E,
+            0xFF61, 0xFF61).freeze();
+
+    private static final UnicodeSet commaEquivalents = new UnicodeSet(
+            //"[,\u060C\u066B\u3001\uFE10\uFE11\uFE50\uFE51\uFF0C\uFF64]"
+            0x002C, 0x002C,
+            0x060C, 0x060C,
+            0x066B, 0x066B,
+            0x3001, 0x3001,
+            0xFE10, 0xFE11,
+            0xFE50, 0xFE51,
+            0xFF0C, 0xFF0C,
+            0xFF64, 0xFF64).freeze();
+
+//    private static final UnicodeSet otherGroupingSeparators = new UnicodeSet(
+//            //"[\\ '\u00A0\u066C\u2000-\u200A\u2018\u2019\u202F\u205F\u3000\uFF07]"
+//            0x0020, 0x0020,
+//            0x0027, 0x0027,
+//            0x00A0, 0x00A0,
+//            0x066C, 0x066C,
+//            0x2000, 0x200A,
+//            0x2018, 0x2019,
+//            0x202F, 0x202F,
+//            0x205F, 0x205F,
+//            0x3000, 0x3000,
+//            0xFF07, 0xFF07).freeze();
+
+    private static final UnicodeSet strictDotEquivalents = new UnicodeSet(
+            //"[.\u2024\uFE52\uFF0E\uFF61]"
+            0x002E, 0x002E,
+            0x2024, 0x2024,
+            0xFE52, 0xFE52,
+            0xFF0E, 0xFF0E,
+            0xFF61, 0xFF61).freeze();
+
+    private static final UnicodeSet strictCommaEquivalents = new UnicodeSet(
+            //"[,\u066B\uFE10\uFE50\uFF0C]"
+            0x002C, 0x002C,
+            0x066B, 0x066B,
+            0xFE10, 0xFE10,
+            0xFE50, 0xFE50,
+            0xFF0C, 0xFF0C).freeze();
+
+//    private static final UnicodeSet strictOtherGroupingSeparators = new UnicodeSet(
+//            //"[\\ '\u00A0\u066C\u2000-\u200A\u2018\u2019\u202F\u205F\u3000\uFF07]"
+//            0x0020, 0x0020,
+//            0x0027, 0x0027,
+//            0x00A0, 0x00A0,
+//            0x066C, 0x066C,
+//            0x2000, 0x200A,
+//            0x2018, 0x2019,
+//            0x202F, 0x202F,
+//            0x205F, 0x205F,
+//            0x3000, 0x3000,
+//            0xFF07, 0xFF07).freeze();
+
+    private static final UnicodeSet defaultGroupingSeparators =
+        // new UnicodeSet(dotEquivalents).addAll(commaEquivalents)
+        //     .addAll(otherGroupingSeparators).freeze();
+        new UnicodeSet(
+                0x0020, 0x0020,
+                0x0027, 0x0027,
+                0x002C, 0x002C,
+                0x002E, 0x002E,
+                0x00A0, 0x00A0,
+                0x060C, 0x060C,
+                0x066B, 0x066C,
+                0x2000, 0x200A,
+                0x2018, 0x2019,
+                0x2024, 0x2024,
+                0x202F, 0x202F,
+                0x205F, 0x205F,
+                0x3000, 0x3002,
+                0xFE10, 0xFE12,
+                0xFE50, 0xFE52,
+                0xFF07, 0xFF07,
+                0xFF0C, 0xFF0C,
+                0xFF0E, 0xFF0E,
+                0xFF61, 0xFF61,
+                0xFF64, 0xFF64).freeze();
+
+    private static final UnicodeSet strictDefaultGroupingSeparators =
+        // new UnicodeSet(strictDotEquivalents).addAll(strictCommaEquivalents)
+        //     .addAll(strictOtherGroupingSeparators).freeze();
+        new UnicodeSet(
+                0x0020, 0x0020,
+                0x0027, 0x0027,
+                0x002C, 0x002C,
+                0x002E, 0x002E,
+                0x00A0, 0x00A0,
+                0x066B, 0x066C,
+                0x2000, 0x200A,
+                0x2018, 0x2019,
+                0x2024, 0x2024,
+                0x202F, 0x202F,
+                0x205F, 0x205F,
+                0x3000, 0x3000,
+                0xFE10, 0xFE10,
+                0xFE50, 0xFE50,
+                0xFE52, 0xFE52,
+                0xFF07, 0xFF07,
+                0xFF0C, 0xFF0C,
+                0xFF0E, 0xFF0E,
+                0xFF61, 0xFF61).freeze();
+
+    static final UnicodeSet minusSigns =
+        new UnicodeSet(
+                0x002D, 0x002D,
+                0x207B, 0x207B,
+                0x208B, 0x208B,
+                0x2212, 0x2212,
+                0x2796, 0x2796,
+                0xFE63, 0xFE63,
+                0xFF0D, 0xFF0D).freeze();
+
+    static final UnicodeSet plusSigns =
+            new UnicodeSet(
+                0x002B, 0x002B,
+                0x207A, 0x207A,
+                0x208A, 0x208A,
+                0x2795, 0x2795,
+                0xFB29, 0xFB29,
+                0xFE62, 0xFE62,
+                0xFF0B, 0xFF0B).freeze();
+
+    // equivalent grouping and decimal support
+    static final boolean skipExtendedSeparatorParsing = ICUConfig.get(
+        "com.ibm.icu.text.DecimalFormat.SkipExtendedSeparatorParsing", "false")
+        .equals("true");
+
+    // allow control of requiring a matching decimal point when parsing
+    boolean parseRequireDecimalPoint = false;
+
+    // When parsing a number with big exponential value, it requires to transform the
+    // value into a string representation to construct BigInteger instance.  We want to
+    // set the maximum size because it can easily trigger OutOfMemoryException.
+    // PARSE_MAX_EXPONENT is currently set to 1000 (See getParseMaxDigits()),
+    // which is much bigger than MAX_VALUE of Double ( See the problem reported by ticket#5698
+    private int PARSE_MAX_EXPONENT = 1000;
+
+    /**
+     * Parses the given text into a number. The text is parsed beginning at parsePosition,
+     * until an unparseable character is seen.
+     *
+     * @param text the string to parse.
+     * @param parsePosition the position at which to being parsing. Upon return, the first
+     * unparseable character.
+     * @param digits the DigitList to set to the parsed value.
+     * @param status Upon return contains boolean status flags indicating whether the
+     * value was infinite and whether it was positive.
+     * @param currency return value for parsed currency, for generic currency parsing
+     * mode, or null for normal parsing. In generic currency parsing mode, any currency is
+     * parsed, not just the currency that this formatter is set to.
+     * @param negPrefix negative prefix pattern
+     * @param negSuffix negative suffix pattern
+     * @param posPrefix positive prefix pattern
+     * @param negSuffix negative suffix pattern
+     * @param parseComplexCurrency whether it is complex currency parsing or not.
+     * @param type type of currency to parse against, LONG_NAME only or not.
+     */
+    private final boolean subparse(
+        String text, ParsePosition parsePosition, DigitList digits,
+        boolean status[], Currency currency[], String negPrefix, String negSuffix, String posPrefix,
+        String posSuffix, boolean parseComplexCurrency, int type) {
+
+        int position = parsePosition.getIndex();
+        int oldStart = parsePosition.getIndex();
+
+        // Match padding before prefix
+        if (formatWidth > 0 && padPosition == PAD_BEFORE_PREFIX) {
+            position = skipPadding(text, position);
+        }
+
+        // Match positive and negative prefixes; prefer longest match.
+        int posMatch = compareAffix(text, position, false, true, posPrefix, parseComplexCurrency, type, currency);
+        int negMatch = compareAffix(text, position, true, true, negPrefix, parseComplexCurrency, type, currency);
+        if (posMatch >= 0 && negMatch >= 0) {
+            if (posMatch > negMatch) {
+                negMatch = -1;
+            } else if (negMatch > posMatch) {
+                posMatch = -1;
+            }
+        }
+        if (posMatch >= 0) {
+            position += posMatch;
+        } else if (negMatch >= 0) {
+            position += negMatch;
+        } else {
+            parsePosition.setErrorIndex(position);
+            return false;
+        }
+
+        // Match padding after prefix
+        if (formatWidth > 0 && padPosition == PAD_AFTER_PREFIX) {
+            position = skipPadding(text, position);
+        }
+
+        // process digits or Inf, find decimal position
+        status[STATUS_INFINITE] = false;
+        if (text.regionMatches(position, symbols.getInfinity(), 0,
+                                              symbols.getInfinity().length())) {
+            position += symbols.getInfinity().length();
+            status[STATUS_INFINITE] = true;
+        } else {
+            // We now have a string of digits, possibly with grouping symbols, and decimal
+            // points. We want to process these into a DigitList.  We don't want to put a
+            // bunch of leading zeros into the DigitList though, so we keep track of the
+            // location of the decimal point, put only significant digits into the
+            // DigitList, and adjust the exponent as needed.
+
+            digits.decimalAt = digits.count = 0;
+            String decimal = (currencySignCount == CURRENCY_SIGN_COUNT_ZERO) ?
+                    symbols.getDecimalSeparatorString() : symbols.getMonetaryDecimalSeparatorString();
+            String grouping = (currencySignCount == CURRENCY_SIGN_COUNT_ZERO) ?
+                    symbols.getGroupingSeparatorString() : symbols.getMonetaryGroupingSeparatorString();
+
+            String exponentSep = symbols.getExponentSeparator();
+            boolean sawDecimal = false;
+            boolean sawGrouping = false;
+            boolean sawDigit = false;
+            long exponent = 0; // Set to the exponent value, if any
+
+            // strict parsing
+            boolean strictParse = isParseStrict();
+            boolean strictFail = false; // did we exit with a strict parse failure?
+            int lastGroup = -1; // where did we last see a grouping separator?
+            int groupedDigitCount = 0;  // tracking count of digits delimited by grouping separator
+            int gs2 = groupingSize2 == 0 ? groupingSize : groupingSize2;
+
+            UnicodeSet decimalEquiv = skipExtendedSeparatorParsing ? UnicodeSet.EMPTY :
+                getEquivalentDecimals(decimal, strictParse);
+            UnicodeSet groupEquiv = skipExtendedSeparatorParsing ? UnicodeSet.EMPTY :
+                (strictParse ? strictDefaultGroupingSeparators : defaultGroupingSeparators);
+
+            // We have to track digitCount ourselves, because digits.count will pin when
+            // the maximum allowable digits is reached.
+            int digitCount = 0;
+
+            int backup = -1;    // used for preserving the last confirmed position
+            int[] parsedDigit = {-1};   // allocates int[1] for parsing a single digit
+
+            while (position < text.length()) {
+                // Check if the sequence at the current position matches a decimal digit
+                int matchLen = matchesDigit(text, position, parsedDigit);
+                if (matchLen > 0) {
+                    // matched a digit
+                    // Cancel out backup setting (see grouping handler below)
+                    if (backup != -1) {
+                        if (strictParse) {
+                            // comma followed by digit, so group before comma is a secondary
+                            // group. If there was a group separator before that, the group
+                            // must == the secondary group length, else it can be <= the the
+                            // secondary group length.
+                            if ((lastGroup != -1 && groupedDigitCount != gs2)
+                                    || (lastGroup == -1 && groupedDigitCount > gs2)) {
+                                strictFail = true;
+                                break;
+                            }
+                        }
+                        lastGroup = backup;
+                        groupedDigitCount = 0;
+                    }
+
+                    groupedDigitCount++;
+                    position += matchLen;
+                    backup = -1;
+                    sawDigit = true;
+                    if (parsedDigit[0] == 0 && digits.count == 0) {
+                        // Handle leading zeros
+                        if (!sawDecimal) {
+                            // Ignore leading zeros in integer part of number.
+                            continue;
+                        }
+                        // If we have seen the decimal, but no significant digits yet,
+                        // then we account for leading zeros by decrementing the
+                        // digits.decimalAt into negative values.
+                        --digits.decimalAt;
+                    } else {
+                        ++digitCount;
+                        digits.append((char) (parsedDigit[0] + '0'));
+                    }
+                    continue;
+                }
+
+                // Check if the sequence at the current position matches locale's decimal separator
+                int decimalStrLen = decimal.length();
+                if (text.regionMatches(position, decimal, 0, decimalStrLen)) {
+                    // matched a decimal separator
+                    if (strictParse) {
+                        if (backup != -1 ||
+                            (lastGroup != -1 && groupedDigitCount != groupingSize)) {
+                            strictFail = true;
+                            break;
+                        }
+                    }
+
+                    // If we're only parsing integers, or if we ALREADY saw the decimal,
+                    // then don't parse this one.
+                    if (isParseIntegerOnly() || sawDecimal) {
+                        break;
+                    }
+
+                    digits.decimalAt = digitCount; // Not digits.count!
+                    sawDecimal = true;
+                    position += decimalStrLen;
+                    continue;
+                }
+
+                if (isGroupingUsed()) {
+                    // Check if the sequence at the current position matches locale's grouping separator
+                    int groupingStrLen = grouping.length();
+                    if (text.regionMatches(position, grouping, 0, groupingStrLen)) {
+                        if (sawDecimal) {
+                            break;
+                        }
+
+                        if (strictParse) {
+                            if ((!sawDigit || backup != -1)) {
+                                // leading group, or two group separators in a row
+                                strictFail = true;
+                                break;
+                            }
+                        }
+
+                        // Ignore grouping characters, if we are using them, but require that
+                        // they be followed by a digit. Otherwise we backup and reprocess
+                        // them.
+                        backup = position;
+                        position += groupingStrLen;
+                        sawGrouping = true;
+                        continue;
+                    }
+                }
+
+                // Check if the code point at the current position matches one of decimal/grouping equivalent group chars
+                int cp = text.codePointAt(position);
+                if (!sawDecimal && decimalEquiv.contains(cp)) {
+                    // matched a decimal separator
+                    if (strictParse) {
+                        if (backup != -1 ||
+                            (lastGroup != -1 && groupedDigitCount != groupingSize)) {
+                            strictFail = true;
+                            break;
+                        }
+                    }
+
+                    // If we're only parsing integers, or if we ALREADY saw the decimal,
+                    // then don't parse this one.
+                    if (isParseIntegerOnly()) {
+                        break;
+                    }
+
+                    digits.decimalAt = digitCount; // Not digits.count!
+
+                    // Once we see a decimal separator character, we only accept that
+                    // decimal separator character from then on.
+                    decimal = String.valueOf(Character.toChars(cp));
+
+                    sawDecimal = true;
+                    position += Character.charCount(cp);
+                    continue;
+                }
+
+                if (isGroupingUsed() && !sawGrouping && groupEquiv.contains(cp)) {
+                    // matched a grouping separator
+                    if (sawDecimal) {
+                        break;
+                    }
+
+                    if (strictParse) {
+                        if ((!sawDigit || backup != -1)) {
+                            // leading group, or two group separators in a row
+                            strictFail = true;
+                            break;
+                        }
+                    }
+
+                    // Once we see a grouping character, we only accept that grouping
+                    // character from then on.
+                    grouping = String.valueOf(Character.toChars(cp));
+
+                    // Ignore grouping characters, if we are using them, but require that
+                    // they be followed by a digit. Otherwise we backup and reprocess
+                    // them.
+                    backup = position;
+                    position += Character.charCount(cp);
+                    sawGrouping = true;
+                    continue;
+                }
+
+                // Check if the sequence at the current position matches locale's exponent separator
+                int exponentSepStrLen = exponentSep.length();
+                if (text.regionMatches(true, position, exponentSep, 0, exponentSepStrLen)) {
+                    // parse sign, if present
+                    boolean negExp = false;
+                    int pos = position + exponentSep.length();
+                    if (pos < text.length()) {
+                        String plusSign = symbols.getPlusSignString();
+                        String minusSign = symbols.getMinusSignString();
+                        if (text.regionMatches(pos, plusSign, 0, plusSign.length())) {
+                            pos += plusSign.length();
+                        } else if (text.regionMatches(pos, minusSign, 0, minusSign.length())) {
+                            pos += minusSign.length();
+                            negExp = true;
+                        }
+                    }
+
+                    DigitList exponentDigits = new DigitList();
+                    exponentDigits.count = 0;
+                    while (pos < text.length()) {
+                        int digitMatchLen = matchesDigit(text, pos, parsedDigit);
+                        if (digitMatchLen > 0) {
+                            exponentDigits.append((char) (parsedDigit[0] + '0'));
+                            pos += digitMatchLen;
+                        } else {
+                            break;
+                        }
+                    }
+
+                    if (exponentDigits.count > 0) {
+                        // defer strict parse until we know we have a bona-fide exponent
+                        if (strictParse && sawGrouping) {
+                            strictFail = true;
+                            break;
+                        }
+
+                        // Quick overflow check for exponential part.  Actual limit check
+                        // will be done later in this code.
+                        if (exponentDigits.count > 10 /* maximum decimal digits for int */) {
+                            if (negExp) {
+                                // set underflow flag
+                                status[STATUS_UNDERFLOW] = true;
+                            } else {
+                                // set infinite flag
+                                status[STATUS_INFINITE] = true;
+                            }
+                        } else {
+                            exponentDigits.decimalAt = exponentDigits.count;
+                            exponent = exponentDigits.getLong();
+                            if (negExp) {
+                                exponent = -exponent;
+                            }
+                        }
+                        position = pos; // Advance past the exponent
+                    }
+
+                    break; // Whether we fail or succeed, we exit this loop
+                }
+
+                // All other cases, stop parsing
+                break;
+            }
+
+            if (digits.decimalAt == 0 && isDecimalPatternMatchRequired()) {
+                if (this.formatPattern.indexOf(decimal) != -1) {
+                    parsePosition.setIndex(oldStart);
+                    parsePosition.setErrorIndex(position);
+                    return false;
+                }
+            }
+
+            if (backup != -1)
+                position = backup;
+
+            // If there was no decimal point we have an integer
+            if (!sawDecimal) {
+                digits.decimalAt = digitCount; // Not digits.count!
+            }
+
+            // check for strict parse errors
+            if (strictParse && !sawDecimal) {
+                if (lastGroup != -1 && groupedDigitCount != groupingSize) {
+                    strictFail = true;
+                }
+            }
+            if (strictFail) {
+                // only set with strictParse and a leading zero error leading zeros are an
+                // error with strict parsing except immediately before nondigit (except
+                // group separator followed by digit), or end of text.
+
+                parsePosition.setIndex(oldStart);
+                parsePosition.setErrorIndex(position);
+                return false;
+            }
+
+            // Adjust for exponent, if any
+            exponent += digits.decimalAt;
+            if (exponent < -getParseMaxDigits()) {
+                status[STATUS_UNDERFLOW] = true;
+            } else if (exponent > getParseMaxDigits()) {
+                status[STATUS_INFINITE] = true;
+            } else {
+                digits.decimalAt = (int) exponent;
+            }
+
+            // If none of the text string was recognized. For example, parse "x" with
+            // pattern "#0.00" (return index and error index both 0) parse "$" with
+            // pattern "$#0.00". (return index 0 and error index 1).
+            if (!sawDigit && digitCount == 0) {
+                parsePosition.setIndex(oldStart);
+                parsePosition.setErrorIndex(oldStart);
+                return false;
+            }
+        }
+
+        // Match padding before suffix
+        if (formatWidth > 0 && padPosition == PAD_BEFORE_SUFFIX) {
+            position = skipPadding(text, position);
+        }
+
+        // Match positive and negative suffixes; prefer longest match.
+        if (posMatch >= 0) {
+            posMatch = compareAffix(text, position, false, false, posSuffix, parseComplexCurrency, type, currency);
+        }
+        if (negMatch >= 0) {
+            negMatch = compareAffix(text, position, true, false, negSuffix, parseComplexCurrency, type, currency);
+        }
+        if (posMatch >= 0 && negMatch >= 0) {
+            if (posMatch > negMatch) {
+                negMatch = -1;
+            } else if (negMatch > posMatch) {
+                posMatch = -1;
+            }
+        }
+
+        // Fail if neither or both
+        if ((posMatch >= 0) == (negMatch >= 0)) {
+            parsePosition.setErrorIndex(position);
+            return false;
+        }
+
+        position += (posMatch >= 0 ? posMatch : negMatch);
+
+        // Match padding after suffix
+        if (formatWidth > 0 && padPosition == PAD_AFTER_SUFFIX) {
+            position = skipPadding(text, position);
+        }
+
+        parsePosition.setIndex(position);
+
+        status[STATUS_POSITIVE] = (posMatch >= 0);
+
+        if (parsePosition.getIndex() == oldStart) {
+            parsePosition.setErrorIndex(position);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Check if the substring at the specified position matches a decimal digit.
+     * If matched, this method sets the decimal value to <code>decVal</code> and
+     * returns matched length.
+     *
+     * @param str       The input string
+     * @param start     The start index
+     * @param decVal    Receives decimal value
+     * @return          Length of match, or 0 if the sequence at the position is not
+     *                  a decimal digit.
+     */
+    private int matchesDigit(String str, int start, int[] decVal) {
+        String[] localeDigits = symbols.getDigitStringsLocal();
+
+        // Check if the sequence at the current position matches locale digits.
+        for (int i = 0; i < 10; i++) {
+            int digitStrLen = localeDigits[i].length();
+            if (str.regionMatches(start, localeDigits[i], 0, digitStrLen)) {
+                decVal[0] = i;
+                return digitStrLen;
+            }
+        }
+
+        // If no locale digit match, then check if this is a Unicode digit
+        int cp = str.codePointAt(start);
+        decVal[0] = UCharacter.digit(cp, 10);
+        if (decVal[0] >= 0) {
+            return Character.charCount(cp);
+        }
+
+        return 0;
+    }
+
+    /**
+     * Returns a set of characters equivalent to the given desimal separator used for
+     * parsing number.  This method may return an empty set.
+     */
+    private UnicodeSet getEquivalentDecimals(String decimal, boolean strictParse) {
+        UnicodeSet equivSet = UnicodeSet.EMPTY;
+        if (strictParse) {
+            if (strictDotEquivalents.contains(decimal)) {
+                equivSet = strictDotEquivalents;
+            } else if (strictCommaEquivalents.contains(decimal)) {
+                equivSet = strictCommaEquivalents;
+            }
+        } else {
+            if (dotEquivalents.contains(decimal)) {
+                equivSet = dotEquivalents;
+            } else if (commaEquivalents.contains(decimal)) {
+                equivSet = commaEquivalents;
+            }
+        }
+        return equivSet;
+    }
+
+    /**
+     * Starting at position, advance past a run of pad characters, if any. Return the
+     * index of the first character after position that is not a pad character. Result is
+     * >= position.
+     */
+    private final int skipPadding(String text, int position) {
+        while (position < text.length() && text.charAt(position) == pad) {
+            ++position;
+        }
+        return position;
+    }
+
+    /**
+     * Returns the length matched by the given affix, or -1 if none. Runs of white space
+     * in the affix, match runs of white space in the input. Pattern white space and input
+     * white space are determined differently; see code.
+     *
+     * @param text input text
+     * @param pos offset into input at which to begin matching
+     * @param isNegative
+     * @param isPrefix
+     * @param affixPat affix pattern used for currency affix comparison
+     * @param complexCurrencyParsing whether it is currency parsing or not
+     * @param type compare against currency type, LONG_NAME only or not.
+     * @param currency return value for parsed currency, for generic currency parsing
+     * mode, or null for normal parsing.  In generic currency parsing mode, any currency
+     * is parsed, not just the currency that this formatter is set to.
+     * @return length of input that matches, or -1 if match failure
+     */
+    private int compareAffix(String text, int pos, boolean isNegative, boolean isPrefix,
+                             String affixPat, boolean complexCurrencyParsing, int type, Currency[] currency) {
+        if (currency != null || currencyChoice != null || (currencySignCount != CURRENCY_SIGN_COUNT_ZERO && complexCurrencyParsing)) {
+            return compareComplexAffix(affixPat, text, pos, type, currency);
+        }
+        if (isPrefix) {
+            return compareSimpleAffix(isNegative ? negativePrefix : positivePrefix, text, pos);
+        } else {
+            return compareSimpleAffix(isNegative ? negativeSuffix : positiveSuffix, text, pos);
+        }
+
+    }
+
+    /**
+     * Check for bidi marks: LRM, RLM, ALM
+     */
+    private static boolean isBidiMark(int c) {
+        return (c==0x200E || c==0x200F || c==0x061C);
+    }
+
+    /**
+     * Remove bidi marks from affix
+     */
+    private static String trimMarksFromAffix(String affix) {
+        boolean hasBidiMark = false;
+        int idx = 0;
+        for (; idx < affix.length(); idx++) {
+            if (isBidiMark(affix.charAt(idx))) {
+                hasBidiMark = true;
+                break;
+            }
+        }
+        if (!hasBidiMark) {
+            return affix;
+        }
+
+        StringBuilder buf = new StringBuilder();
+        buf.append(affix, 0, idx);
+        idx++;  // skip the first Bidi mark
+        for (; idx < affix.length(); idx++) {
+            char c = affix.charAt(idx);
+            if (!isBidiMark(c)) {
+                buf.append(c);
+            }
+        }
+
+        return buf.toString();
+    }
+
+    /**
+     * Return the length matched by the given affix, or -1 if none. Runs of white space in
+     * the affix, match runs of white space in the input. Pattern white space and input
+     * white space are determined differently; see code.
+     *
+     * @param affix pattern string, taken as a literal
+     * @param input input text
+     * @param pos offset into input at which to begin matching
+     * @return length of input that matches, or -1 if match failure
+     */
+    private static int compareSimpleAffix(String affix, String input, int pos) {
+        int start = pos;
+        // Affixes here might consist of sign, currency symbol and related spacing, etc.
+        // For more efficiency we should keep lazily-created trimmed affixes around in
+        // instance variables instead of trimming each time they are used (the next step).
+        String trimmedAffix = (affix.length() > 1)? trimMarksFromAffix(affix): affix;
+        for (int i = 0; i < trimmedAffix.length();) {
+            int c = UTF16.charAt(trimmedAffix, i);
+            int len = UTF16.getCharCount(c);
+            if (PatternProps.isWhiteSpace(c)) {
+                // We may have a pattern like: \u200F and input text like: \u200F Note
+                // that U+200F and U+0020 are Pattern_White_Space but only U+0020 is
+                // UWhiteSpace. So we have to first do a direct match of the run of RULE
+                // whitespace in the pattern, then match any extra characters.
+                boolean literalMatch = false;
+                while (pos < input.length()) {
+                    int ic = UTF16.charAt(input, pos);
+                    if (ic == c) {
+                        literalMatch = true;
+                        i += len;
+                        pos += len;
+                        if (i == trimmedAffix.length()) {
+                            break;
+                        }
+                        c = UTF16.charAt(trimmedAffix, i);
+                        len = UTF16.getCharCount(c);
+                        if (!PatternProps.isWhiteSpace(c)) {
+                            break;
+                        }
+                    } else if (isBidiMark(ic)) {
+                        pos++; // just skip over this input text
+                    } else {
+                        break;
+                    }
+                }
+
+                // Advance over run in trimmedAffix
+                i = skipPatternWhiteSpace(trimmedAffix, i);
+
+                // Advance over run in input text. Must see at least one white space char
+                // in input, unless we've already matched some characters literally.
+                int s = pos;
+                pos = skipUWhiteSpace(input, pos);
+                if (pos == s && !literalMatch) {
+                    return -1;
+                }
+                // If we skip UWhiteSpace in the input text, we need to skip it in the
+                // pattern.  Otherwise, the previous lines may have skipped over text
+                // (such as U+00A0) that is also in the trimmedAffix.
+                i = skipUWhiteSpace(trimmedAffix, i);
+            } else {
+                boolean match = false;
+                while (pos < input.length()) {
+                    int ic = UTF16.charAt(input, pos);
+                    if (!match && equalWithSignCompatibility(ic, c)) {
+                        i += len;
+                        pos += len;
+                        match = true;
+                    } else if (isBidiMark(ic)) {
+                        pos++; // just skip over this input text
+                    } else {
+                        break;
+                    }
+                }
+                if (!match) {
+                    return -1;
+                }
+            }
+        }
+        return pos - start;
+    }
+
+    private static boolean equalWithSignCompatibility(int lhs, int rhs) {
+        return lhs == rhs
+                || (minusSigns.contains(lhs) && minusSigns.contains(rhs))
+                || (plusSigns.contains(lhs) && plusSigns.contains(rhs));
+    }
+
+    /**
+     * Skips over a run of zero or more Pattern_White_Space characters at pos in text.
+     */
+    private static int skipPatternWhiteSpace(String text, int pos) {
+        while (pos < text.length()) {
+            int c = UTF16.charAt(text, pos);
+            if (!PatternProps.isWhiteSpace(c)) {
+                break;
+            }
+            pos += UTF16.getCharCount(c);
+        }
+        return pos;
+    }
+
+    /**
+     * Skips over a run of zero or more isUWhiteSpace() characters at pos in text.
+     */
+    private static int skipUWhiteSpace(String text, int pos) {
+        while (pos < text.length()) {
+            int c = UTF16.charAt(text, pos);
+            if (!UCharacter.isUWhiteSpace(c)) {
+                break;
+            }
+            pos += UTF16.getCharCount(c);
+        }
+        return pos;
+    }
+
+     /**
+     * Skips over a run of zero or more bidi marks at pos in text.
+     */
+    private static int skipBidiMarks(String text, int pos) {
+        while (pos < text.length()) {
+            int c = UTF16.charAt(text, pos);
+            if (!isBidiMark(c)) {
+                break;
+            }
+            pos += UTF16.getCharCount(c);
+        }
+        return pos;
+    }
+
+   /**
+     * Returns the length matched by the given affix, or -1 if none.
+     *
+     * @param affixPat pattern string
+     * @param text input text
+     * @param pos offset into input at which to begin matching
+     * @param type parse against currency type, LONG_NAME only or not.
+     * @param currency return value for parsed currency, for generic
+     * currency parsing mode, or null for normal parsing.  In generic
+     * currency parsing mode, any currency is parsed, not just the
+     * currency that this formatter is set to.
+     * @return position after the matched text, or -1 if match failure
+     */
+    private int compareComplexAffix(String affixPat, String text, int pos, int type,
+                                    Currency[] currency) {
+        int start = pos;
+        for (int i = 0; i < affixPat.length() && pos >= 0;) {
+            char c = affixPat.charAt(i++);
+            if (c == QUOTE) {
+                for (;;) {
+                    int j = affixPat.indexOf(QUOTE, i);
+                    if (j == i) {
+                        pos = match(text, pos, QUOTE);
+                        i = j + 1;
+                        break;
+                    } else if (j > i) {
+                        pos = match(text, pos, affixPat.substring(i, j));
+                        i = j + 1;
+                        if (i < affixPat.length() && affixPat.charAt(i) == QUOTE) {
+                            pos = match(text, pos, QUOTE);
+                            ++i;
+                            // loop again
+                        } else {
+                            break;
+                        }
+                    } else {
+                        // Unterminated quote; should be caught by apply
+                        // pattern.
+                        throw new RuntimeException();
+                    }
+                }
+                continue;
+            }
+
+            String affix = null;
+
+            switch (c) {
+            case CURRENCY_SIGN:
+                // since the currency names in choice format is saved the same way as
+                // other currency names, do not need to do currency choice parsing here.
+                // the general currency parsing parse against all names, including names
+                // in choice format.  assert(currency != null || (getCurrency() != null &&
+                // currencyChoice != null));
+                boolean intl = i < affixPat.length() && affixPat.charAt(i) == CURRENCY_SIGN;
+                if (intl) {
+                    ++i;
+                }
+                boolean plural = i < affixPat.length() && affixPat.charAt(i) == CURRENCY_SIGN;
+                if (plural) {
+                    ++i;
+                    intl = false;
+                }
+                // Parse generic currency -- anything for which we have a display name, or
+                // any 3-letter ISO code.  Try to parse display name for our locale; first
+                // determine our locale.  TODO: use locale in CurrencyPluralInfo
+                ULocale uloc = getLocale(ULocale.VALID_LOCALE);
+                if (uloc == null) {
+                    // applyPattern has been called; use the symbols
+                    uloc = symbols.getLocale(ULocale.VALID_LOCALE);
+                }
+                // Delegate parse of display name => ISO code to Currency
+                ParsePosition ppos = new ParsePosition(pos);
+                // using Currency.parse to handle mixed style parsing.
+                String iso = Currency.parse(uloc, text, type, ppos);
+
+                // If parse succeeds, populate currency[0]
+                if (iso != null) {
+                    if (currency != null) {
+                        currency[0] = Currency.getInstance(iso);
+                    } else {
+                        // The formatter is currency-style but the client has not requested
+                        // the value of the parsed currency. In this case, if that value does
+                        // not match the formatter's current value, then the parse fails.
+                        Currency effectiveCurr = getEffectiveCurrency();
+                        if (iso.compareTo(effectiveCurr.getCurrencyCode()) != 0) {
+                            pos = -1;
+                            continue;
+                        }
+                    }
+                    pos = ppos.getIndex();
+                } else {
+                    pos = -1;
+                }
+                continue;
+            case PATTERN_PERCENT:
+                affix = symbols.getPercentString();
+                break;
+            case PATTERN_PER_MILLE:
+                affix = symbols.getPerMillString();
+                break;
+            case PATTERN_PLUS_SIGN:
+                affix = symbols.getPlusSignString();
+                break;
+            case PATTERN_MINUS_SIGN:
+                affix = symbols.getMinusSignString();
+                break;
+            default:
+                // fall through to affix != null test, which will fail
+                break;
+            }
+
+            if (affix != null) {
+                pos = match(text, pos, affix);
+                continue;
+            }
+
+            pos = match(text, pos, c);
+            if (PatternProps.isWhiteSpace(c)) {
+                i = skipPatternWhiteSpace(affixPat, i);
+            }
+        }
+
+        return pos - start;
+    }
+
+    /**
+     * Matches a single character at text[pos] and return the index of the next character
+     * upon success. Return -1 on failure. If ch is a Pattern_White_Space then match a run of
+     * white space in text.
+     */
+    static final int match(String text, int pos, int ch) {
+        if (pos < 0 || pos >= text.length()) {
+            return -1;
+        }
+        pos = skipBidiMarks(text, pos);
+        if (PatternProps.isWhiteSpace(ch)) {
+            // Advance over run of white space in input text
+            // Must see at least one white space char in input
+            int s = pos;
+            pos = skipPatternWhiteSpace(text, pos);
+            if (pos == s) {
+                return -1;
+            }
+            return pos;
+        }
+        if (pos >= text.length() || UTF16.charAt(text, pos) != ch) {
+            return -1;
+        }
+        pos = skipBidiMarks(text, pos + UTF16.getCharCount(ch));
+        return pos;
+    }
+
+    /**
+     * Matches a string at text[pos] and return the index of the next character upon
+     * success. Return -1 on failure. Match a run of white space in str with a run of
+     * white space in text.
+     */
+    static final int match(String text, int pos, String str) {
+        for (int i = 0; i < str.length() && pos >= 0;) {
+            int ch = UTF16.charAt(str, i);
+            i += UTF16.getCharCount(ch);
+            if (isBidiMark(ch)) {
+                continue;
+            }
+            pos = match(text, pos, ch);
+            if (PatternProps.isWhiteSpace(ch)) {
+                i = skipPatternWhiteSpace(str, i);
+            }
+        }
+        return pos;
+    }
+
+    /**
+     * Returns a copy of the decimal format symbols used by this format.
+     *
+     * @return desired DecimalFormatSymbols
+     * @see DecimalFormatSymbols
+     * @stable ICU 2.0
+     */
+    public DecimalFormatSymbols getDecimalFormatSymbols() {
+        try {
+            // don't allow multiple references
+            return (DecimalFormatSymbols) symbols.clone();
+        } catch (Exception foo) {
+            return null; // should never happen
+        }
+    }
+
+    /**
+     * Sets the decimal format symbols used by this format. The format uses a copy of the
+     * provided symbols.
+     *
+     * @param newSymbols desired DecimalFormatSymbols
+     * @see DecimalFormatSymbols
+     * @stable ICU 2.0
+     */
+    public void setDecimalFormatSymbols(DecimalFormatSymbols newSymbols) {
+        symbols = (DecimalFormatSymbols) newSymbols.clone();
+        setCurrencyForSymbols();
+        expandAffixes(null);
+    }
+
+    /**
+     * Update the currency object to match the symbols. This method is used only when the
+     * caller has passed in a symbols object that may not be the default object for its
+     * locale.
+     */
+    private void setCurrencyForSymbols() {
+
+        // Bug 4212072 Update the affix strings according to symbols in order to keep the
+        // affix strings up to date.  [Richard/GCL]
+
+        // With the introduction of the Currency object, the currency symbols in the DFS
+        // object are ignored. For backward compatibility, we check any explicitly set DFS
+        // object. If it is a default symbols object for its locale, we change the
+        // currency object to one for that locale. If it is custom, we set the currency to
+        // null.
+        DecimalFormatSymbols def = new DecimalFormatSymbols(symbols.getULocale());
+
+        if (symbols.getCurrencySymbol().equals(def.getCurrencySymbol())
+                && symbols.getInternationalCurrencySymbol()
+                       .equals(def.getInternationalCurrencySymbol())) {
+            setCurrency(Currency.getInstance(symbols.getULocale()));
+        } else {
+            setCurrency(null);
+        }
+    }
+
+    /**
+     * Returns the positive prefix.
+     *
+     * <p>Examples: +123, $123, sFr123
+     * @return the prefix
+     * @stable ICU 2.0
+     */
+    public String getPositivePrefix() {
+        return positivePrefix;
+    }
+
+    /**
+     * Sets the positive prefix.
+     *
+     * <p>Examples: +123, $123, sFr123
+     * @param newValue the prefix
+     * @stable ICU 2.0
+     */
+    public void setPositivePrefix(String newValue) {
+        positivePrefix = newValue;
+        posPrefixPattern = null;
+    }
+
+    /**
+     * Returns the negative prefix.
+     *
+     * <p>Examples: -123, ($123) (with negative suffix), sFr-123
+     *
+     * @return the prefix
+     * @stable ICU 2.0
+     */
+    public String getNegativePrefix() {
+        return negativePrefix;
+    }
+
+    /**
+     * Sets the negative prefix.
+     *
+     * <p>Examples: -123, ($123) (with negative suffix), sFr-123
+     * @param newValue the prefix
+     * @stable ICU 2.0
+     */
+    public void setNegativePrefix(String newValue) {
+        negativePrefix = newValue;
+        negPrefixPattern = null;
+    }
+
+    /**
+     * Returns the positive suffix.
+     *
+     * <p>Example: 123%
+     *
+     * @return the suffix
+     * @stable ICU 2.0
+     */
+    public String getPositiveSuffix() {
+        return positiveSuffix;
+    }
+
+    /**
+     * Sets the positive suffix.
+     *
+     * <p>Example: 123%
+     * @param newValue the suffix
+     * @stable ICU 2.0
+     */
+    public void setPositiveSuffix(String newValue) {
+        positiveSuffix = newValue;
+        posSuffixPattern = null;
+    }
+
+    /**
+     * Returns the negative suffix.
+     *
+     * <p>Examples: -123%, ($123) (with positive suffixes)
+     *
+     * @return the suffix
+     * @stable ICU 2.0
+     */
+    public String getNegativeSuffix() {
+        return negativeSuffix;
+    }
+
+    /**
+     * Sets the positive suffix.
+     *
+     * <p>Examples: 123%
+     * @param newValue the suffix
+     * @stable ICU 2.0
+     */
+    public void setNegativeSuffix(String newValue) {
+        negativeSuffix = newValue;
+        negSuffixPattern = null;
+    }
+
+    /**
+     * Returns the multiplier for use in percent, permill, etc. For a percentage, set the
+     * suffixes to have "%" and the multiplier to be 100. (For Arabic, use arabic percent
+     * symbol). For a permill, set the suffixes to have "\u2031" and the multiplier to be
+     * 1000.
+     *
+     * <p>Examples: with 100, 1.23 -&gt; "123", and "123" -&gt; 1.23
+     *
+     * @return the multiplier
+     * @stable ICU 2.0
+     */
+    public int getMultiplier() {
+        return multiplier;
+    }
+
+    /**
+     * Sets the multiplier for use in percent, permill, etc. For a percentage, set the
+     * suffixes to have "%" and the multiplier to be 100. (For Arabic, use arabic percent
+     * symbol). For a permill, set the suffixes to have "\u2031" and the multiplier to be
+     * 1000.
+     *
+     * <p>Examples: with 100, 1.23 -&gt; "123", and "123" -&gt; 1.23
+     *
+     * @param newValue the multiplier
+     * @stable ICU 2.0
+     */
+    public void setMultiplier(int newValue) {
+        if (newValue == 0) {
+            throw new IllegalArgumentException("Bad multiplier: " + newValue);
+        }
+        multiplier = newValue;
+    }
+
+    /**
+     * {@icu} Returns the rounding increment.
+     *
+     * @return A positive rounding increment, or <code>null</code> if a custom rounding
+     * increment is not in effect.
+     * @see #setRoundingIncrement
+     * @see #getRoundingMode
+     * @see #setRoundingMode
+     * @stable ICU 2.0
+     */
+    public java.math.BigDecimal getRoundingIncrement() {
+        if (roundingIncrementICU == null)
+            return null;
+        return roundingIncrementICU.toBigDecimal();
+    }
+
+    /**
+     * {@icu} Sets the rounding increment. In the absence of a rounding increment, numbers
+     * will be rounded to the number of digits displayed.
+     *
+     * @param newValue A positive rounding increment, or <code>null</code> or
+     * <code>BigDecimal(0.0)</code> to use the default rounding increment.
+     * @throws IllegalArgumentException if <code>newValue</code> is &lt; 0.0
+     * @see #getRoundingIncrement
+     * @see #getRoundingMode
+     * @see #setRoundingMode
+     * @stable ICU 2.0
+     */
+    public void setRoundingIncrement(java.math.BigDecimal newValue) {
+        if (newValue == null) {
+            setRoundingIncrement((BigDecimal) null);
+        } else {
+            setRoundingIncrement(new BigDecimal(newValue));
+        }
+    }
+
+    /**
+     * {@icu} Sets the rounding increment. In the absence of a rounding increment, numbers
+     * will be rounded to the number of digits displayed.
+     *
+     * @param newValue A positive rounding increment, or <code>null</code> or
+     * <code>BigDecimal(0.0)</code> to use the default rounding increment.
+     * @throws IllegalArgumentException if <code>newValue</code> is &lt; 0.0
+     * @see #getRoundingIncrement
+     * @see #getRoundingMode
+     * @see #setRoundingMode
+     * @stable ICU 3.6
+     */
+    public void setRoundingIncrement(BigDecimal newValue) {
+        int i = newValue == null ? 0 : newValue.compareTo(BigDecimal.ZERO);
+        if (i < 0) {
+            throw new IllegalArgumentException("Illegal rounding increment");
+        }
+        if (i == 0) {
+            setInternalRoundingIncrement(null);
+        } else {
+            setInternalRoundingIncrement(newValue);
+        }
+        resetActualRounding();
+    }
+
+    /**
+     * {@icu} Sets the rounding increment. In the absence of a rounding increment, numbers
+     * will be rounded to the number of digits displayed.
+     *
+     * @param newValue A positive rounding increment, or 0.0 to use the default
+     * rounding increment.
+     * @throws IllegalArgumentException if <code>newValue</code> is &lt; 0.0
+     * @see #getRoundingIncrement
+     * @see #getRoundingMode
+     * @see #setRoundingMode
+     * @stable ICU 2.0
+     */
+    public void setRoundingIncrement(double newValue) {
+        if (newValue < 0.0) {
+            throw new IllegalArgumentException("Illegal rounding increment");
+        }
+        if (newValue == 0.0d) {
+            setInternalRoundingIncrement((BigDecimal) null);
+        } else {
+            // Should use BigDecimal#valueOf(double) instead of constructor
+            // to avoid the double precision problem.
+            setInternalRoundingIncrement(BigDecimal.valueOf(newValue));
+        }
+        resetActualRounding();
+    }
+
+    /**
+     * Returns the rounding mode.
+     *
+     * @return A rounding mode, between <code>BigDecimal.ROUND_UP</code> and
+     * <code>BigDecimal.ROUND_UNNECESSARY</code>.
+     * @see #setRoundingIncrement
+     * @see #getRoundingIncrement
+     * @see #setRoundingMode
+     * @see java.math.BigDecimal
+     * @stable ICU 2.0
+     */
+    @Override
+    public int getRoundingMode() {
+        return roundingMode;
+    }
+
+    /**
+     * Sets the rounding mode. This has no effect unless the rounding increment is greater
+     * than zero.
+     *
+     * @param roundingMode A rounding mode, between <code>BigDecimal.ROUND_UP</code> and
+     * <code>BigDecimal.ROUND_UNNECESSARY</code>.
+     * @exception IllegalArgumentException if <code>roundingMode</code> is unrecognized.
+     * @see #setRoundingIncrement
+     * @see #getRoundingIncrement
+     * @see #getRoundingMode
+     * @see java.math.BigDecimal
+     * @stable ICU 2.0
+     */
+    @Override
+    public void setRoundingMode(int roundingMode) {
+        if (roundingMode < BigDecimal.ROUND_UP || roundingMode > BigDecimal.ROUND_UNNECESSARY) {
+            throw new IllegalArgumentException("Invalid rounding mode: " + roundingMode);
+        }
+
+        this.roundingMode = roundingMode;
+        resetActualRounding();
+    }
+
+    /**
+     * Returns the width to which the output of <code>format()</code> is padded. The width is
+     * counted in 16-bit code units.
+     *
+     * @return the format width, or zero if no padding is in effect
+     * @see #setFormatWidth
+     * @see #getPadCharacter
+     * @see #setPadCharacter
+     * @see #getPadPosition
+     * @see #setPadPosition
+     * @stable ICU 2.0
+     */
+    public int getFormatWidth() {
+        return formatWidth;
+    }
+
+    /**
+     * Sets the width to which the output of <code>format()</code> is
+     * padded. The width is counted in 16-bit code units.  This method
+     * also controls whether padding is enabled.
+     *
+     * @param width the width to which to pad the result of
+     * <code>format()</code>, or zero to disable padding
+     * @exception IllegalArgumentException if <code>width</code> is &lt; 0
+     * @see #getFormatWidth
+     * @see #getPadCharacter
+     * @see #setPadCharacter
+     * @see #getPadPosition
+     * @see #setPadPosition
+     * @stable ICU 2.0
+     */
+    public void setFormatWidth(int width) {
+        if (width < 0) {
+            throw new IllegalArgumentException("Illegal format width");
+        }
+        formatWidth = width;
+    }
+
+    /**
+     * {@icu} Returns the character used to pad to the format width. The default is ' '.
+     *
+     * @return the pad character
+     * @see #setFormatWidth
+     * @see #getFormatWidth
+     * @see #setPadCharacter
+     * @see #getPadPosition
+     * @see #setPadPosition
+     * @stable ICU 2.0
+     */
+    public char getPadCharacter() {
+        return pad;
+    }
+
+    /**
+     * {@icu} Sets the character used to pad to the format width. If padding is not
+     * enabled, then this will take effect if padding is later enabled.
+     *
+     * @param padChar the pad character
+     * @see #setFormatWidth
+     * @see #getFormatWidth
+     * @see #getPadCharacter
+     * @see #getPadPosition
+     * @see #setPadPosition
+     * @stable ICU 2.0
+     */
+    public void setPadCharacter(char padChar) {
+        pad = padChar;
+    }
+
+    /**
+     * {@icu} Returns the position at which padding will take place. This is the location at
+     * which padding will be inserted if the result of <code>format()</code> is shorter
+     * than the format width.
+     *
+     * @return the pad position, one of <code>PAD_BEFORE_PREFIX</code>,
+     *         <code>PAD_AFTER_PREFIX</code>, <code>PAD_BEFORE_SUFFIX</code>, or
+     *         <code>PAD_AFTER_SUFFIX</code>.
+     * @see #setFormatWidth
+     * @see #getFormatWidth
+     * @see #setPadCharacter
+     * @see #getPadCharacter
+     * @see #setPadPosition
+     * @see #PAD_BEFORE_PREFIX
+     * @see #PAD_AFTER_PREFIX
+     * @see #PAD_BEFORE_SUFFIX
+     * @see #PAD_AFTER_SUFFIX
+     * @stable ICU 2.0
+     */
+    public int getPadPosition() {
+        return padPosition;
+    }
+
+    /**
+     * {@icu} Sets the position at which padding will take place. This is the location at
+     * which padding will be inserted if the result of <code>format()</code> is shorter
+     * than the format width. This has no effect unless padding is enabled.
+     *
+     * @param padPos the pad position, one of <code>PAD_BEFORE_PREFIX</code>,
+     * <code>PAD_AFTER_PREFIX</code>, <code>PAD_BEFORE_SUFFIX</code>, or
+     * <code>PAD_AFTER_SUFFIX</code>.
+     * @exception IllegalArgumentException if the pad position in unrecognized
+     * @see #setFormatWidth
+     * @see #getFormatWidth
+     * @see #setPadCharacter
+     * @see #getPadCharacter
+     * @see #getPadPosition
+     * @see #PAD_BEFORE_PREFIX
+     * @see #PAD_AFTER_PREFIX
+     * @see #PAD_BEFORE_SUFFIX
+     * @see #PAD_AFTER_SUFFIX
+     * @stable ICU 2.0
+     */
+    public void setPadPosition(int padPos) {
+        if (padPos < PAD_BEFORE_PREFIX || padPos > PAD_AFTER_SUFFIX) {
+            throw new IllegalArgumentException("Illegal pad position");
+        }
+        padPosition = padPos;
+    }
+
+    /**
+     * {@icu} Returns whether or not scientific notation is used.
+     *
+     * @return true if this object formats and parses scientific notation
+     * @see #setScientificNotation
+     * @see #getMinimumExponentDigits
+     * @see #setMinimumExponentDigits
+     * @see #isExponentSignAlwaysShown
+     * @see #setExponentSignAlwaysShown
+     * @stable ICU 2.0
+     */
+    public boolean isScientificNotation() {
+        return useExponentialNotation;
+    }
+
+    /**
+     * {@icu} Sets whether or not scientific notation is used. When scientific notation is
+     * used, the effective maximum number of integer digits is &lt;= 8. If the maximum number
+     * of integer digits is set to more than 8, the effective maximum will be 1. This
+     * allows this call to generate a 'default' scientific number format without
+     * additional changes.
+     *
+     * @param useScientific true if this object formats and parses scientific notation
+     * @see #isScientificNotation
+     * @see #getMinimumExponentDigits
+     * @see #setMinimumExponentDigits
+     * @see #isExponentSignAlwaysShown
+     * @see #setExponentSignAlwaysShown
+     * @stable ICU 2.0
+     */
+    public void setScientificNotation(boolean useScientific) {
+        useExponentialNotation = useScientific;
+    }
+
+    /**
+     * {@icu} Returns the minimum exponent digits that will be shown.
+     *
+     * @return the minimum exponent digits that will be shown
+     * @see #setScientificNotation
+     * @see #isScientificNotation
+     * @see #setMinimumExponentDigits
+     * @see #isExponentSignAlwaysShown
+     * @see #setExponentSignAlwaysShown
+     * @stable ICU 2.0
+     */
+    public byte getMinimumExponentDigits() {
+        return minExponentDigits;
+    }
+
+    /**
+     * {@icu} Sets the minimum exponent digits that will be shown. This has no effect
+     * unless scientific notation is in use.
+     *
+     * @param minExpDig a value &gt;= 1 indicating the fewest exponent
+     * digits that will be shown
+     * @exception IllegalArgumentException if <code>minExpDig</code> &lt; 1
+     * @see #setScientificNotation
+     * @see #isScientificNotation
+     * @see #getMinimumExponentDigits
+     * @see #isExponentSignAlwaysShown
+     * @see #setExponentSignAlwaysShown
+     * @stable ICU 2.0
+     */
+    public void setMinimumExponentDigits(byte minExpDig) {
+        if (minExpDig < 1) {
+            throw new IllegalArgumentException("Exponent digits must be >= 1");
+        }
+        minExponentDigits = minExpDig;
+    }
+
+    /**
+     * {@icu} Returns whether the exponent sign is always shown.
+     *
+     * @return true if the exponent is always prefixed with either the localized minus
+     * sign or the localized plus sign, false if only negative exponents are prefixed with
+     * the localized minus sign.
+     * @see #setScientificNotation
+     * @see #isScientificNotation
+     * @see #setMinimumExponentDigits
+     * @see #getMinimumExponentDigits
+     * @see #setExponentSignAlwaysShown
+     * @stable ICU 2.0
+     */
+    public boolean isExponentSignAlwaysShown() {
+        return exponentSignAlwaysShown;
+    }
+
+    /**
+     * {@icu} Sets whether the exponent sign is always shown. This has no effect unless
+     * scientific notation is in use.
+     *
+     * @param expSignAlways true if the exponent is always prefixed with either the
+     * localized minus sign or the localized plus sign, false if only negative exponents
+     * are prefixed with the localized minus sign.
+     * @see #setScientificNotation
+     * @see #isScientificNotation
+     * @see #setMinimumExponentDigits
+     * @see #getMinimumExponentDigits
+     * @see #isExponentSignAlwaysShown
+     * @stable ICU 2.0
+     */
+    public void setExponentSignAlwaysShown(boolean expSignAlways) {
+        exponentSignAlwaysShown = expSignAlways;
+    }
+
+    /**
+     * Returns the grouping size. Grouping size is the number of digits between grouping
+     * separators in the integer portion of a number. For example, in the number
+     * "123,456.78", the grouping size is 3.
+     *
+     * @see #setGroupingSize
+     * @see NumberFormat#isGroupingUsed
+     * @see DecimalFormatSymbols#getGroupingSeparator
+     * @stable ICU 2.0
+     */
+    public int getGroupingSize() {
+        return groupingSize;
+    }
+
+    /**
+     * Sets the grouping size. Grouping size is the number of digits between grouping
+     * separators in the integer portion of a number. For example, in the number
+     * "123,456.78", the grouping size is 3.
+     *
+     * @see #getGroupingSize
+     * @see NumberFormat#setGroupingUsed
+     * @see DecimalFormatSymbols#setGroupingSeparator
+     * @stable ICU 2.0
+     */
+    public void setGroupingSize(int newValue) {
+        groupingSize = (byte) newValue;
+    }
+
+    /**
+     * {@icu} Returns the secondary grouping size. In some locales one grouping interval
+     * is used for the least significant integer digits (the primary grouping size), and
+     * another is used for all others (the secondary grouping size). A formatter
+     * supporting a secondary grouping size will return a positive integer unequal to the
+     * primary grouping size returned by <code>getGroupingSize()</code>. For example, if
+     * the primary grouping size is 4, and the secondary grouping size is 2, then the
+     * number 123456789 formats as "1,23,45,6789", and the pattern appears as "#,##,###0".
+     *
+     * @return the secondary grouping size, or a value less than one if there is none
+     * @see #setSecondaryGroupingSize
+     * @see NumberFormat#isGroupingUsed
+     * @see DecimalFormatSymbols#getGroupingSeparator
+     * @stable ICU 2.0
+     */
+    public int getSecondaryGroupingSize() {
+        return groupingSize2;
+    }
+
+    /**
+     * {@icu} Sets the secondary grouping size. If set to a value less than 1, then
+     * secondary grouping is turned off, and the primary grouping size is used for all
+     * intervals, not just the least significant.
+     *
+     * @see #getSecondaryGroupingSize
+     * @see NumberFormat#setGroupingUsed
+     * @see DecimalFormatSymbols#setGroupingSeparator
+     * @stable ICU 2.0
+     */
+    public void setSecondaryGroupingSize(int newValue) {
+        groupingSize2 = (byte) newValue;
+    }
+
+    /**
+     * {@icu} Returns the MathContext used by this format.
+     *
+     * @return desired MathContext
+     * @see #getMathContext
+     * @stable ICU 4.2
+     */
+    public MathContext getMathContextICU() {
+        return mathContext;
+    }
+
+    /**
+     * {@icu} Returns the MathContext used by this format.
+     *
+     * @return desired MathContext
+     * @see #getMathContext
+     * @stable ICU 4.2
+     */
+    public java.math.MathContext getMathContext() {
+        try {
+            // don't allow multiple references
+            return mathContext == null ? null : new java.math.MathContext(mathContext.getDigits(),
+                    java.math.RoundingMode.valueOf(mathContext.getRoundingMode()));
+        } catch (Exception foo) {
+            return null; // should never happen
+        }
+    }
+
+    /**
+     * {@icu} Sets the MathContext used by this format.
+     *
+     * @param newValue desired MathContext
+     * @see #getMathContext
+     * @stable ICU 4.2
+     */
+    public void setMathContextICU(MathContext newValue) {
+        mathContext = newValue;
+    }
+
+    /**
+     * {@icu} Sets the MathContext used by this format.
+     *
+     * @param newValue desired MathContext
+     * @see #getMathContext
+     * @stable ICU 4.2
+     */
+    public void setMathContext(java.math.MathContext newValue) {
+        mathContext = new MathContext(newValue.getPrecision(), MathContext.SCIENTIFIC, false,
+                                      (newValue.getRoundingMode()).ordinal());
+    }
+
+    /**
+     * Returns the behavior of the decimal separator with integers. (The decimal
+     * separator will always appear with decimals.)  <p> Example: Decimal ON: 12345 -&gt;
+     * 12345.; OFF: 12345 -&gt; 12345
+     *
+     * @stable ICU 2.0
+     */
+    public boolean isDecimalSeparatorAlwaysShown() {
+        return decimalSeparatorAlwaysShown;
+    }
+
+    /**
+     * When decimal match is not required, the input does not have to
+     * contain a decimal mark when there is a decimal mark specified in the
+     * pattern.
+     * @param value true if input must contain a match to decimal mark in pattern
+     * Default is false.
+     * @stable ICU 54
+     */
+     public void setDecimalPatternMatchRequired(boolean value) {
+         parseRequireDecimalPoint = value;
+     }
+
+    /**
+     * {@icu} Returns whether the input to parsing must contain a decimal mark if there
+     * is a decimal mark in the pattern.
+     * @return true if input must contain a match to decimal mark in pattern
+     * @stable ICU 54
+     */
+    public boolean isDecimalPatternMatchRequired() {
+        return parseRequireDecimalPoint;
+    }
+
+
+    /**
+     * Sets the behavior of the decimal separator with integers. (The decimal separator
+     * will always appear with decimals.)
+     *
+     * <p>This only affects formatting, and only where there might be no digits after the
+     * decimal point, e.g., if true, 3456.00 -&gt; "3,456." if false, 3456.00 -&gt; "3456" This
+     * is independent of parsing. If you want parsing to stop at the decimal point, use
+     * setParseIntegerOnly.
+     *
+     * <p>
+     * Example: Decimal ON: 12345 -&gt; 12345.; OFF: 12345 -&gt; 12345
+     *
+     * @stable ICU 2.0
+     */
+    public void setDecimalSeparatorAlwaysShown(boolean newValue) {
+        decimalSeparatorAlwaysShown = newValue;
+    }
+
+    /**
+     * {@icu} Returns a copy of the CurrencyPluralInfo used by this format. It might
+     * return null if the decimal format is not a plural type currency decimal
+     * format. Plural type currency decimal format means either the pattern in the decimal
+     * format contains 3 currency signs, or the decimal format is initialized with
+     * PLURALCURRENCYSTYLE.
+     *
+     * @return desired CurrencyPluralInfo
+     * @see CurrencyPluralInfo
+     * @stable ICU 4.2
+     */
+    public CurrencyPluralInfo getCurrencyPluralInfo() {
+        try {
+            // don't allow multiple references
+            return currencyPluralInfo == null ? null :
+                (CurrencyPluralInfo) currencyPluralInfo.clone();
+        } catch (Exception foo) {
+            return null; // should never happen
+        }
+    }
+
+    /**
+     * {@icu} Sets the CurrencyPluralInfo used by this format. The format uses a copy of
+     * the provided information.
+     *
+     * @param newInfo desired CurrencyPluralInfo
+     * @see CurrencyPluralInfo
+     * @stable ICU 4.2
+     */
+    public void setCurrencyPluralInfo(CurrencyPluralInfo newInfo) {
+        currencyPluralInfo = (CurrencyPluralInfo) newInfo.clone();
+        isReadyForParsing = false;
+    }
+
+    /**
+     * Overrides clone.
+     * @stable ICU 2.0
+     */
+    @Override
+    public Object clone() {
+        try {
+            DecimalFormat_ICU58 other = (DecimalFormat_ICU58) super.clone();
+            other.symbols = (DecimalFormatSymbols) symbols.clone();
+            other.digitList = new DigitList(); // fix for JB#5358
+            if (currencyPluralInfo != null) {
+                other.currencyPluralInfo = (CurrencyPluralInfo) currencyPluralInfo.clone();
+            }
+            other.attributes = new ArrayList<FieldPosition>(); // #9240
+            other.currencyUsage = currencyUsage;
+
+            // TODO: We need to figure out whether we share a single copy of DigitList by
+            // multiple cloned copies.  format/subformat are designed to use a single
+            // instance, but parse/subparse implementation is not.
+            return other;
+        } catch (Exception e) {
+            throw new IllegalStateException();
+        }
+    }
+
+    /**
+     * Overrides equals.
+     * @stable ICU 2.0
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null)
+            return false;
+        if (!super.equals(obj))
+            return false; // super does class check
+
+        DecimalFormat_ICU58 other = (DecimalFormat_ICU58) obj;
+        // Add the comparison of the four new added fields ,they are posPrefixPattern,
+        // posSuffixPattern, negPrefixPattern, negSuffixPattern. [Richard/GCL]
+        // following are added to accomodate changes for currency plural format.
+        return currencySignCount == other.currencySignCount
+                && (style != NumberFormat.PLURALCURRENCYSTYLE ||
+                    equals(posPrefixPattern, other.posPrefixPattern)
+                && equals(posSuffixPattern, other.posSuffixPattern)
+                && equals(negPrefixPattern, other.negPrefixPattern)
+                && equals(negSuffixPattern, other.negSuffixPattern))
+                && multiplier == other.multiplier
+                && groupingSize == other.groupingSize
+                && groupingSize2 == other.groupingSize2
+                && decimalSeparatorAlwaysShown == other.decimalSeparatorAlwaysShown
+                && useExponentialNotation == other.useExponentialNotation
+                && (!useExponentialNotation || minExponentDigits == other.minExponentDigits)
+                && useSignificantDigits == other.useSignificantDigits
+                && (!useSignificantDigits || minSignificantDigits == other.minSignificantDigits
+                        && maxSignificantDigits == other.maxSignificantDigits)
+                && symbols.equals(other.symbols)
+                && Utility.objectEquals(currencyPluralInfo, other.currencyPluralInfo)
+                && currencyUsage.equals(other.currencyUsage);
+    }
+
+    // method to unquote the strings and compare
+    private boolean equals(String pat1, String pat2) {
+        if (pat1 == null || pat2 == null) {
+            return (pat1 == null && pat2 == null);
+        }
+        // fast path
+        if (pat1.equals(pat2)) {
+            return true;
+        }
+        return unquote(pat1).equals(unquote(pat2));
+    }
+
+    private String unquote(String pat) {
+        StringBuilder buf = new StringBuilder(pat.length());
+        int i = 0;
+        while (i < pat.length()) {
+            char ch = pat.charAt(i++);
+            if (ch != QUOTE) {
+                buf.append(ch);
+            }
+        }
+        return buf.toString();
+    }
+
+    // protected void handleToString(StringBuffer buf) {
+    // buf.append("\nposPrefixPattern: '" + posPrefixPattern + "'\n");
+    // buf.append("positivePrefix: '" + positivePrefix + "'\n");
+    // buf.append("posSuffixPattern: '" + posSuffixPattern + "'\n");
+    // buf.append("positiveSuffix: '" + positiveSuffix + "'\n");
+    // buf.append("negPrefixPattern: '" +
+    //     com.ibm.icu.impl.Utility.format1ForSource(negPrefixPattern) + "'\n");
+    // buf.append("negativePrefix: '" +
+    //     com.ibm.icu.impl.Utility.format1ForSource(negativePrefix) + "'\n");
+    // buf.append("negSuffixPattern: '" + negSuffixPattern + "'\n");
+    // buf.append("negativeSuffix: '" + negativeSuffix + "'\n");
+    // buf.append("multiplier: '" + multiplier + "'\n");
+    // buf.append("groupingSize: '" + groupingSize + "'\n");
+    // buf.append("groupingSize2: '" + groupingSize2 + "'\n");
+    // buf.append("decimalSeparatorAlwaysShown: '" + decimalSeparatorAlwaysShown + "'\n");
+    // buf.append("useExponentialNotation: '" + useExponentialNotation + "'\n");
+    // buf.append("minExponentDigits: '" + minExponentDigits + "'\n");
+    // buf.append("useSignificantDigits: '" + useSignificantDigits + "'\n");
+    // buf.append("minSignificantDigits: '" + minSignificantDigits + "'\n");
+    // buf.append("maxSignificantDigits: '" + maxSignificantDigits + "'\n");
+    // buf.append("symbols: '" + symbols + "'");
+    // }
+
+    /**
+     * Overrides hashCode.
+     * @stable ICU 2.0
+     */
+    @Override
+    public int hashCode() {
+        return super.hashCode() * 37 + positivePrefix.hashCode();
+        // just enough fields for a reasonable distribution
+    }
+
+    /**
+     * Synthesizes a pattern string that represents the current state of this Format
+     * object.
+     *
+     * @see #applyPattern
+     * @stable ICU 2.0
+     */
+    public String toPattern() {
+        if (style == NumberFormat.PLURALCURRENCYSTYLE) {
+            // the prefix or suffix pattern might not be defined yet, so they can not be
+            // synthesized, instead, get them directly.  but it might not be the actual
+            // pattern used in formatting.  the actual pattern used in formatting depends
+            // on the formatted number's plural count.
+            return formatPattern;
+        }
+        return toPattern(false);
+    }
+
+    /**
+     * Synthesizes a localized pattern string that represents the current state of this
+     * Format object.
+     *
+     * @see #applyPattern
+     * @stable ICU 2.0
+     */
+    public String toLocalizedPattern() {
+        if (style == NumberFormat.PLURALCURRENCYSTYLE) {
+            return formatPattern;
+        }
+        return toPattern(true);
+    }
+
+    /**
+     * Expands the affix pattern strings into the expanded affix strings. If any affix
+     * pattern string is null, do not expand it. This method should be called any time the
+     * symbols or the affix patterns change in order to keep the expanded affix strings up
+     * to date. This method also will be called before formatting if format currency
+     * plural names, since the plural name is not a static one, it is based on the
+     * currency plural count, the affix will be known only after the currency plural count
+     * is know. In which case, the parameter 'pluralCount' will be a non-null currency
+     * plural count. In all other cases, the 'pluralCount' is null, which means it is not
+     * needed.
+     */
+    // Bug 4212072 [Richard/GCL]
+    private void expandAffixes(String pluralCount) {
+        // expandAffix() will set currencyChoice to a non-null value if
+        // appropriate AND if it is null.
+        currencyChoice = null;
+
+        // Reuse one StringBuffer for better performance
+        StringBuffer buffer = new StringBuffer();
+        if (posPrefixPattern != null) {
+            expandAffix(posPrefixPattern, pluralCount, buffer);
+            positivePrefix = buffer.toString();
+        }
+        if (posSuffixPattern != null) {
+            expandAffix(posSuffixPattern, pluralCount, buffer);
+            positiveSuffix = buffer.toString();
+        }
+        if (negPrefixPattern != null) {
+            expandAffix(negPrefixPattern, pluralCount, buffer);
+            negativePrefix = buffer.toString();
+        }
+        if (negSuffixPattern != null) {
+            expandAffix(negSuffixPattern, pluralCount, buffer);
+            negativeSuffix = buffer.toString();
+        }
+    }
+
+    /**
+     * Expands an affix pattern into an affix string. All characters in the pattern are
+     * literal unless bracketed by QUOTEs. The following characters outside QUOTE are
+     * recognized: PATTERN_PERCENT, PATTERN_PER_MILLE, PATTERN_MINUS, and
+     * CURRENCY_SIGN. If CURRENCY_SIGN is doubled, it is interpreted as an international
+     * currency sign. If CURRENCY_SIGN is tripled, it is interpreted as currency plural
+     * long names, such as "US Dollars". Any other character outside QUOTE represents
+     * itself. Quoted text must be well-formed.
+     *
+     * This method is used in two distinct ways. First, it is used to expand the stored
+     * affix patterns into actual affixes. For this usage, doFormat must be false. Second,
+     * it is used to expand the stored affix patterns given a specific number (doFormat ==
+     * true), for those rare cases in which a currency format references a ChoiceFormat
+     * (e.g., en_IN display name for INR). The number itself is taken from digitList.
+     * TODO: There are no currency ChoiceFormat patterns, figure out what is still relevant here.
+     *
+     * When used in the first way, this method has a side effect: It sets currencyChoice
+     * to a ChoiceFormat object, if the currency's display name in this locale is a
+     * ChoiceFormat pattern (very rare). It only does this if currencyChoice is null to
+     * start with.
+     *
+     * @param pattern the non-null, possibly empty pattern
+     * @param pluralCount the plural count. It is only used for currency plural format. In
+     * which case, it is the plural count of the currency amount. For example, in en_US,
+     * it is the singular "one", or the plural "other". For all other cases, it is null,
+     * and is not being used.
+     * @param buffer a scratch StringBuffer; its contents will be lost
+     */
+    // Bug 4212072 [Richard/GCL]
+    private void expandAffix(String pattern, String pluralCount, StringBuffer buffer) {
+        buffer.setLength(0);
+        for (int i = 0; i < pattern.length();) {
+            char c = pattern.charAt(i++);
+            if (c == QUOTE) {
+                for (;;) {
+                    int j = pattern.indexOf(QUOTE, i);
+                    if (j == i) {
+                        buffer.append(QUOTE);
+                        i = j + 1;
+                        break;
+                    } else if (j > i) {
+                        buffer.append(pattern.substring(i, j));
+                        i = j + 1;
+                        if (i < pattern.length() && pattern.charAt(i) == QUOTE) {
+                            buffer.append(QUOTE);
+                            ++i;
+                            // loop again
+                        } else {
+                            break;
+                        }
+                    } else {
+                        // Unterminated quote; should be caught by apply
+                        // pattern.
+                        throw new RuntimeException();
+                    }
+                }
+                continue;
+            }
+
+            switch (c) {
+            case CURRENCY_SIGN:
+                // As of ICU 2.2 we use the currency object, and ignore the currency
+                // symbols in the DFS, unless we have a null currency object. This occurs
+                // if resurrecting a pre-2.2 object or if the user sets a custom DFS.
+                boolean intl = i < pattern.length() && pattern.charAt(i) == CURRENCY_SIGN;
+                boolean plural = false;
+                if (intl) {
+                    ++i;
+                    if (i < pattern.length() && pattern.charAt(i) == CURRENCY_SIGN) {
+                        plural = true;
+                        intl = false;
+                        ++i;
+                    }
+                }
+                String s = null;
+                Currency currency = getCurrency();
+                if (currency != null) {
+                    // plural name is only needed when pluralCount != null, which means
+                    // when formatting currency plural names.  For other cases,
+                    // pluralCount == null, and plural names are not needed.
+                    if (plural && pluralCount != null) {
+                        s = currency.getName(symbols.getULocale(), Currency.PLURAL_LONG_NAME,
+                                             pluralCount, null);
+                    } else if (!intl) {
+                        s = currency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null);
+                    } else {
+                        s = currency.getCurrencyCode();
+                    }
+                } else {
+                    s = intl ? symbols.getInternationalCurrencySymbol() :
+                        symbols.getCurrencySymbol();
+                }
+                // Here is where FieldPosition could be set for CURRENCY PLURAL.
+                buffer.append(s);
+                break;
+            case PATTERN_PERCENT:
+                buffer.append(symbols.getPercentString());
+                break;
+            case PATTERN_PER_MILLE:
+                buffer.append(symbols.getPerMillString());
+                break;
+            case PATTERN_MINUS_SIGN:
+                buffer.append(symbols.getMinusSignString());
+                break;
+            default:
+                buffer.append(c);
+                break;
+            }
+        }
+    }
+
+    /**
+     * Append an affix to the given StringBuffer.
+     *
+     * @param buf
+     *            buffer to append to
+     * @param isNegative
+     * @param isPrefix
+     * @param fieldPosition
+     * @param parseAttr
+     */
+    private int appendAffix(StringBuffer buf, boolean isNegative, boolean isPrefix,
+                            FieldPosition fieldPosition,
+                            boolean parseAttr) {
+        if (currencyChoice != null) {
+            String affixPat = null;
+            if (isPrefix) {
+                affixPat = isNegative ? negPrefixPattern : posPrefixPattern;
+            } else {
+                affixPat = isNegative ? negSuffixPattern : posSuffixPattern;
+            }
+            StringBuffer affixBuf = new StringBuffer();
+            expandAffix(affixPat, null, affixBuf);
+            buf.append(affixBuf);
+            return affixBuf.length();
+        }
+
+        String affix = null;
+        String pattern;
+        if (isPrefix) {
+            affix = isNegative ? negativePrefix : positivePrefix;
+            pattern = isNegative ? negPrefixPattern : posPrefixPattern;
+        } else {
+            affix = isNegative ? negativeSuffix : positiveSuffix;
+            pattern = isNegative ? negSuffixPattern : posSuffixPattern;
+        }
+        // [Spark/CDL] Invoke formatAffix2Attribute to add attributes for affix
+        if (parseAttr) {
+            // Updates for Ticket 11805.
+            int offset = affix.indexOf(symbols.getCurrencySymbol());
+            if (offset > -1) {
+                formatAffix2Attribute(isPrefix, Field.CURRENCY, buf, offset,
+                        symbols.getCurrencySymbol().length());
+            }
+            offset = affix.indexOf(symbols.getMinusSignString());
+            if (offset > -1) {
+                formatAffix2Attribute(isPrefix, Field.SIGN, buf, offset,
+                        symbols.getMinusSignString().length());
+            }
+            offset = affix.indexOf(symbols.getPercentString());
+            if (offset > -1) {
+                formatAffix2Attribute(isPrefix, Field.PERCENT, buf, offset,
+                        symbols.getPercentString().length());
+            }
+            offset = affix.indexOf(symbols.getPerMillString());
+            if (offset > -1) {
+                formatAffix2Attribute(isPrefix, Field.PERMILLE, buf, offset,
+                        symbols.getPerMillString().length());
+            }
+            offset = pattern.indexOf("¤¤¤");
+            if (offset > -1) {
+                formatAffix2Attribute(isPrefix, Field.CURRENCY, buf, offset,
+                        affix.length() - offset);
+            }
+        }
+
+        // Look for SIGN, PERCENT, PERMILLE in the formatted affix.
+        if (fieldPosition.getFieldAttribute() == NumberFormat.Field.SIGN) {
+            String sign = isNegative ? symbols.getMinusSignString() : symbols.getPlusSignString();
+            int firstPos = affix.indexOf(sign);
+            if (firstPos > -1) {
+                int startPos = buf.length() + firstPos;
+                fieldPosition.setBeginIndex(startPos);
+                fieldPosition.setEndIndex(startPos + sign.length());
+            }
+        } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.PERCENT) {
+            int firstPos = affix.indexOf(symbols.getPercentString());
+            if (firstPos > -1) {
+                int startPos = buf.length() + firstPos;
+                fieldPosition.setBeginIndex(startPos);
+                fieldPosition.setEndIndex(startPos + symbols.getPercentString().length());
+            }
+        } else if (fieldPosition.getFieldAttribute() == NumberFormat.Field.PERMILLE) {
+            int firstPos = affix.indexOf(symbols.getPerMillString());
+            if (firstPos > -1) {
+                int startPos = buf.length() + firstPos;
+                fieldPosition.setBeginIndex(startPos);
+                fieldPosition.setEndIndex(startPos + symbols.getPerMillString().length());
+            }
+        } else
+        // If CurrencySymbol or InternationalCurrencySymbol is in the affix, check for currency symbol.
+        // Get spelled out name if "¤¤¤" is in the pattern.
+        if (fieldPosition.getFieldAttribute() == NumberFormat.Field.CURRENCY) {
+            if (affix.indexOf(symbols.getCurrencySymbol()) > -1) {
+                String aff = symbols.getCurrencySymbol();
+                int firstPos = affix.indexOf(aff);
+                int start = buf.length() + firstPos;
+                int end = start + aff.length();
+                fieldPosition.setBeginIndex(start);
+                fieldPosition.setEndIndex(end);
+            } else if (affix.indexOf(symbols.getInternationalCurrencySymbol()) > -1) {
+                String aff = symbols.getInternationalCurrencySymbol();
+                int firstPos = affix.indexOf(aff);
+                int start = buf.length() + firstPos;
+                int end = start + aff.length();
+                fieldPosition.setBeginIndex(start);
+                fieldPosition.setEndIndex(end);
+            } else if (pattern.indexOf("¤¤¤") > -1) {
+                // It's a plural, and we know where it is in the pattern.
+                int firstPos = pattern.indexOf("¤¤¤");
+                int start = buf.length() + firstPos;
+                int end = buf.length() + affix.length(); // This seems clunky and wrong.
+                fieldPosition.setBeginIndex(start);
+                fieldPosition.setEndIndex(end);
+            }
+        }
+
+        buf.append(affix);
+        return affix.length();
+    }
+
+    // Fix for prefix and suffix in Ticket 11805.
+    private void formatAffix2Attribute(boolean isPrefix, Field fieldType,
+        StringBuffer buf, int offset, int symbolSize) {
+        int begin;
+        begin = offset;
+        if (!isPrefix) {
+            begin += buf.length();
+        }
+
+        addAttribute(fieldType, begin, begin + symbolSize);
+    }
+
+    /**
+     * [Spark/CDL] Use this method to add attribute.
+     */
+    private void addAttribute(Field field, int begin, int end) {
+        FieldPosition pos = new FieldPosition(field);
+        pos.setBeginIndex(begin);
+        pos.setEndIndex(end);
+        attributes.add(pos);
+    }
+
+    /**
+     * Formats the object to an attributed string, and return the corresponding iterator.
+     *
+     * @stable ICU 3.6
+     */
+    @Override
+    public AttributedCharacterIterator formatToCharacterIterator(Object obj) {
+      return formatToCharacterIterator(obj, NULL_UNIT);
+    }
+
+    AttributedCharacterIterator formatToCharacterIterator(Object obj, Unit unit) {
+        if (!(obj instanceof Number))
+            throw new IllegalArgumentException();
+        Number number = (Number) obj;
+        StringBuffer text = new StringBuffer();
+        unit.writePrefix(text);
+        attributes.clear();
+        if (obj instanceof BigInteger) {
+            format((BigInteger) number, text, new FieldPosition(0), true);
+        } else if (obj instanceof java.math.BigDecimal) {
+            format((java.math.BigDecimal) number, text, new FieldPosition(0)
+                          , true);
+        } else if (obj instanceof Double) {
+            format(number.doubleValue(), text, new FieldPosition(0), true);
+        } else if (obj instanceof Integer || obj instanceof Long) {
+            format(number.longValue(), text, new FieldPosition(0), true);
+        } else {
+            throw new IllegalArgumentException();
+        }
+        unit.writeSuffix(text);
+        AttributedString as = new AttributedString(text.toString());
+
+        // add NumberFormat field attributes to the AttributedString
+        for (int i = 0; i < attributes.size(); i++) {
+            FieldPosition pos = attributes.get(i);
+            Format.Field attribute = pos.getFieldAttribute();
+            as.addAttribute(attribute, attribute, pos.getBeginIndex(), pos.getEndIndex());
+        }
+
+        // return the CharacterIterator from AttributedString
+        return as.getIterator();
+    }
+
+    /**
+     * Appends an affix pattern to the given StringBuffer. Localize unquoted specials.
+     * <p>
+     * <b>Note:</b> This implementation does not support new String localized symbols.
+     */
+    private void appendAffixPattern(StringBuffer buffer, boolean isNegative, boolean isPrefix,
+                                    boolean localized) {
+        String affixPat = null;
+        if (isPrefix) {
+            affixPat = isNegative ? negPrefixPattern : posPrefixPattern;
+        } else {
+            affixPat = isNegative ? negSuffixPattern : posSuffixPattern;
+        }
+
+        // When there is a null affix pattern, we use the affix itself.
+        if (affixPat == null) {
+            String affix = null;
+            if (isPrefix) {
+                affix = isNegative ? negativePrefix : positivePrefix;
+            } else {
+                affix = isNegative ? negativeSuffix : positiveSuffix;
+            }
+            // Do this crudely for now: Wrap everything in quotes.
+            buffer.append(QUOTE);
+            for (int i = 0; i < affix.length(); ++i) {
+                char ch = affix.charAt(i);
+                if (ch == QUOTE) {
+                    buffer.append(ch);
+                }
+                buffer.append(ch);
+            }
+            buffer.append(QUOTE);
+            return;
+        }
+
+        if (!localized) {
+            buffer.append(affixPat);
+        } else {
+            int i, j;
+            for (i = 0; i < affixPat.length(); ++i) {
+                char ch = affixPat.charAt(i);
+                switch (ch) {
+                case QUOTE:
+                    j = affixPat.indexOf(QUOTE, i + 1);
+                    if (j < 0) {
+                        throw new IllegalArgumentException("Malformed affix pattern: " + affixPat);
+                    }
+                    buffer.append(affixPat.substring(i, j + 1));
+                    i = j;
+                    continue;
+                case PATTERN_PER_MILLE:
+                    ch = symbols.getPerMill();
+                    break;
+                case PATTERN_PERCENT:
+                    ch = symbols.getPercent();
+                    break;
+                case PATTERN_MINUS_SIGN:
+                    ch = symbols.getMinusSign();
+                    break;
+                }
+                // check if char is same as any other symbol
+                if (ch == symbols.getDecimalSeparator() || ch == symbols.getGroupingSeparator()) {
+                    buffer.append(QUOTE);
+                    buffer.append(ch);
+                    buffer.append(QUOTE);
+                } else {
+                    buffer.append(ch);
+                }
+            }
+        }
+    }
+
+    /**
+     * Does the real work of generating a pattern.
+     * <p>
+     * <b>Note:</b> This implementation does not support new String localized symbols.
+     */
+    private String toPattern(boolean localized) {
+        StringBuffer result = new StringBuffer();
+        char zero = localized ? symbols.getZeroDigit() : PATTERN_ZERO_DIGIT;
+        char digit = localized ? symbols.getDigit() : PATTERN_DIGIT;
+        char sigDigit = 0;
+        boolean useSigDig = areSignificantDigitsUsed();
+        if (useSigDig) {
+            sigDigit = localized ? symbols.getSignificantDigit() : PATTERN_SIGNIFICANT_DIGIT;
+        }
+        char group = localized ? symbols.getGroupingSeparator() : PATTERN_GROUPING_SEPARATOR;
+        int i;
+        int roundingDecimalPos = 0; // Pos of decimal in roundingDigits
+        String roundingDigits = null;
+        int padPos = (formatWidth > 0) ? padPosition : -1;
+        String padSpec = (formatWidth > 0)
+            ? new StringBuffer(2).append(localized
+                                         ? symbols.getPadEscape()
+                                         : PATTERN_PAD_ESCAPE).append(pad).toString()
+            : null;
+        if (roundingIncrementICU != null) {
+            i = roundingIncrementICU.scale();
+            roundingDigits = roundingIncrementICU.movePointRight(i).toString();
+            roundingDecimalPos = roundingDigits.length() - i;
+        }
+        for (int part = 0; part < 2; ++part) {
+            // variable not used int partStart = result.length();
+            if (padPos == PAD_BEFORE_PREFIX) {
+                result.append(padSpec);
+            }
+
+            // Use original symbols read from resources in pattern eg. use "\u00A4"
+            // instead of "$" in Locale.US [Richard/GCL]
+            appendAffixPattern(result, part != 0, true, localized);
+            if (padPos == PAD_AFTER_PREFIX) {
+                result.append(padSpec);
+            }
+            int sub0Start = result.length();
+            int g = isGroupingUsed() ? Math.max(0, groupingSize) : 0;
+            if (g > 0 && groupingSize2 > 0 && groupingSize2 != groupingSize) {
+                g += groupingSize2;
+            }
+            int maxDig = 0, minDig = 0, maxSigDig = 0;
+            if (useSigDig) {
+                minDig = getMinimumSignificantDigits();
+                maxDig = maxSigDig = getMaximumSignificantDigits();
+            } else {
+                minDig = getMinimumIntegerDigits();
+                maxDig = getMaximumIntegerDigits();
+            }
+            if (useExponentialNotation) {
+                if (maxDig > MAX_SCIENTIFIC_INTEGER_DIGITS) {
+                    maxDig = 1;
+                }
+            } else if (useSigDig) {
+                maxDig = Math.max(maxDig, g + 1);
+            } else {
+                maxDig = Math.max(Math.max(g, getMinimumIntegerDigits()), roundingDecimalPos) + 1;
+            }
+            for (i = maxDig; i > 0; --i) {
+                if (!useExponentialNotation && i < maxDig && isGroupingPosition(i)) {
+                    result.append(group);
+                }
+                if (useSigDig) {
+                    // #@,@### (maxSigDig == 5, minSigDig == 2) 65 4321 (1-based pos,
+                    // count from the right) Use # if pos > maxSigDig or 1 <= pos <=
+                    // (maxSigDig - minSigDig) Use @ if (maxSigDig - minSigDig) < pos <=
+                    // maxSigDig
+                    result.append((maxSigDig >= i && i > (maxSigDig - minDig)) ? sigDigit : digit);
+                } else {
+                    if (roundingDigits != null) {
+                        int pos = roundingDecimalPos - i;
+                        if (pos >= 0 && pos < roundingDigits.length()) {
+                            result.append((char) (roundingDigits.charAt(pos) - '0' + zero));
+                            continue;
+                        }
+                    }
+                    result.append(i <= minDig ? zero : digit);
+                }
+            }
+            if (!useSigDig) {
+                if (getMaximumFractionDigits() > 0 || decimalSeparatorAlwaysShown) {
+                    result.append(localized ? symbols.getDecimalSeparator() :
+                                  PATTERN_DECIMAL_SEPARATOR);
+                }
+                int pos = roundingDecimalPos;
+                for (i = 0; i < getMaximumFractionDigits(); ++i) {
+                    if (roundingDigits != null && pos < roundingDigits.length()) {
+                        result.append(pos < 0 ? zero :
+                                      (char) (roundingDigits.charAt(pos) - '0' + zero));
+                        ++pos;
+                        continue;
+                    }
+                    result.append(i < getMinimumFractionDigits() ? zero : digit);
+                }
+            }
+            if (useExponentialNotation) {
+                if (localized) {
+                    result.append(symbols.getExponentSeparator());
+                } else {
+                    result.append(PATTERN_EXPONENT);
+                }
+                if (exponentSignAlwaysShown) {
+                    result.append(localized ? symbols.getPlusSign() : PATTERN_PLUS_SIGN);
+                }
+                for (i = 0; i < minExponentDigits; ++i) {
+                    result.append(zero);
+                }
+            }
+            if (padSpec != null && !useExponentialNotation) {
+                int add = formatWidth
+                        - result.length()
+                        + sub0Start
+                        - ((part == 0)
+                           ? positivePrefix.length() + positiveSuffix.length()
+                           : negativePrefix.length() + negativeSuffix.length());
+                while (add > 0) {
+                    result.insert(sub0Start, digit);
+                    ++maxDig;
+                    --add;
+                    // Only add a grouping separator if we have at least 2 additional
+                    // characters to be added, so we don't end up with ",###".
+                    if (add > 1 && isGroupingPosition(maxDig)) {
+                        result.insert(sub0Start, group);
+                        --add;
+                    }
+                }
+            }
+            if (padPos == PAD_BEFORE_SUFFIX) {
+                result.append(padSpec);
+            }
+            // Use original symbols read from resources in pattern eg. use "\u00A4"
+            // instead of "$" in Locale.US [Richard/GCL]
+            appendAffixPattern(result, part != 0, false, localized);
+            if (padPos == PAD_AFTER_SUFFIX) {
+                result.append(padSpec);
+            }
+            if (part == 0) {
+                if (negativeSuffix.equals(positiveSuffix) &&
+                    negativePrefix.equals(PATTERN_MINUS_SIGN + positivePrefix)) {
+                    break;
+                } else {
+                    result.append(localized ? symbols.getPatternSeparator() : PATTERN_SEPARATOR);
+                }
+            }
+        }
+        return result.toString();
+    }
+
+    /**
+     * Applies the given pattern to this Format object. A pattern is a short-hand
+     * specification for the various formatting properties. These properties can also be
+     * changed individually through the various setter methods.
+     *
+     * <p>There is no limit to integer digits are set by this routine, since that is the
+     * typical end-user desire; use setMaximumInteger if you want to set a real value. For
+     * negative numbers, use a second pattern, separated by a semicolon
+     *
+     * <p>Example "#,#00.0#" -&gt; 1,234.56
+     *
+     * <p>This means a minimum of 2 integer digits, 1 fraction digit, and a maximum of 2
+     * fraction digits.
+     *
+     * <p>Example: "#,#00.0#;(#,#00.0#)" for negatives in parentheses.
+     *
+     * <p>In negative patterns, the minimum and maximum counts are ignored; these are
+     * presumed to be set in the positive pattern.
+     *
+     * @stable ICU 2.0
+     */
+    public void applyPattern(String pattern) {
+        applyPattern(pattern, false);
+    }
+
+    /**
+     * Applies the given pattern to this Format object. The pattern is assumed to be in a
+     * localized notation. A pattern is a short-hand specification for the various
+     * formatting properties. These properties can also be changed individually through
+     * the various setter methods.
+     *
+     * <p>There is no limit to integer digits are set by this routine, since that is the
+     * typical end-user desire; use setMaximumInteger if you want to set a real value. For
+     * negative numbers, use a second pattern, separated by a semicolon
+     *
+     * <p>Example "#,#00.0#" -&gt; 1,234.56
+     *
+     * <p>This means a minimum of 2 integer digits, 1 fraction digit, and a maximum of 2
+     * fraction digits.
+     *
+     * <p>Example: "#,#00.0#;(#,#00.0#)" for negatives in parantheses.
+     *
+     * <p>In negative patterns, the minimum and maximum counts are ignored; these are
+     * presumed to be set in the positive pattern.
+     *
+     * @stable ICU 2.0
+     */
+    public void applyLocalizedPattern(String pattern) {
+        applyPattern(pattern, true);
+    }
+
+    /**
+     * Does the real work of applying a pattern.
+     */
+    private void applyPattern(String pattern, boolean localized) {
+        applyPatternWithoutExpandAffix(pattern, localized);
+        expandAffixAdjustWidth(null);
+    }
+
+    private void expandAffixAdjustWidth(String pluralCount) {
+        // Bug 4212072 Update the affix strings according to symbols in order to keep the
+        // affix strings up to date.  [Richard/GCL]
+        expandAffixes(pluralCount);
+
+        // Now that we have the actual prefix and suffix, fix up formatWidth
+        if (formatWidth > 0) {
+            formatWidth += positivePrefix.length() + positiveSuffix.length();
+        }
+    }
+
+    private void applyPatternWithoutExpandAffix(String pattern, boolean localized) {
+        char zeroDigit = PATTERN_ZERO_DIGIT; // '0'
+        char sigDigit = PATTERN_SIGNIFICANT_DIGIT; // '@'
+        char groupingSeparator = PATTERN_GROUPING_SEPARATOR;
+        char decimalSeparator = PATTERN_DECIMAL_SEPARATOR;
+        char percent = PATTERN_PERCENT;
+        char perMill = PATTERN_PER_MILLE;
+        char digit = PATTERN_DIGIT; // '#'
+        char separator = PATTERN_SEPARATOR;
+        String exponent = String.valueOf(PATTERN_EXPONENT);
+        char plus = PATTERN_PLUS_SIGN;
+        char padEscape = PATTERN_PAD_ESCAPE;
+        char minus = PATTERN_MINUS_SIGN; // Bug 4212072 [Richard/GCL]
+        if (localized) {
+            zeroDigit = symbols.getZeroDigit();
+            sigDigit = symbols.getSignificantDigit();
+            groupingSeparator = symbols.getGroupingSeparator();
+            decimalSeparator = symbols.getDecimalSeparator();
+            percent = symbols.getPercent();
+            perMill = symbols.getPerMill();
+            digit = symbols.getDigit();
+            separator = symbols.getPatternSeparator();
+            exponent = symbols.getExponentSeparator();
+            plus = symbols.getPlusSign();
+            padEscape = symbols.getPadEscape();
+            minus = symbols.getMinusSign(); // Bug 4212072 [Richard/GCL]
+        }
+        char nineDigit = (char) (zeroDigit + 9);
+
+        boolean gotNegative = false;
+
+        int pos = 0;
+        // Part 0 is the positive pattern. Part 1, if present, is the negative
+        // pattern.
+        for (int part = 0; part < 2 && pos < pattern.length(); ++part) {
+            // The subpart ranges from 0 to 4: 0=pattern proper, 1=prefix, 2=suffix,
+            // 3=prefix in quote, 4=suffix in quote. Subpart 0 is between the prefix and
+            // suffix, and consists of pattern characters. In the prefix and suffix,
+            // percent, permille, and currency symbols are recognized and translated.
+            int subpart = 1, sub0Start = 0, sub0Limit = 0, sub2Limit = 0;
+
+            // It's important that we don't change any fields of this object
+            // prematurely. We set the following variables for the multiplier, grouping,
+            // etc., and then only change the actual object fields if everything parses
+            // correctly. This also lets us register the data from part 0 and ignore the
+            // part 1, except for the prefix and suffix.
+            StringBuilder prefix = new StringBuilder();
+            StringBuilder suffix = new StringBuilder();
+            int decimalPos = -1;
+            int multpl = 1;
+            int digitLeftCount = 0, zeroDigitCount = 0, digitRightCount = 0, sigDigitCount = 0;
+            byte groupingCount = -1;
+            byte groupingCount2 = -1;
+            int padPos = -1;
+            char padChar = 0;
+            int incrementPos = -1;
+            long incrementVal = 0;
+            byte expDigits = -1;
+            boolean expSignAlways = false;
+            int currencySignCnt = 0;
+
+            // The affix is either the prefix or the suffix.
+            StringBuilder affix = prefix;
+
+            int start = pos;
+
+            PARTLOOP: for (; pos < pattern.length(); ++pos) {
+                char ch = pattern.charAt(pos);
+                switch (subpart) {
+                case 0: // Pattern proper subpart (between prefix & suffix)
+                    // Process the digits, decimal, and grouping characters. We record
+                    // five pieces of information. We expect the digits to occur in the
+                    // pattern ####00.00####, and we record the number of left digits,
+                    // zero (central) digits, and right digits. The position of the last
+                    // grouping character is recorded (should be somewhere within the
+                    // first two blocks of characters), as is the position of the decimal
+                    // point, if any (should be in the zero digits). If there is no
+                    // decimal point, then there should be no right digits.
+                    if (ch == digit) {
+                        if (zeroDigitCount > 0 || sigDigitCount > 0) {
+                            ++digitRightCount;
+                        } else {
+                            ++digitLeftCount;
+                        }
+                        if (groupingCount >= 0 && decimalPos < 0) {
+                            ++groupingCount;
+                        }
+                    } else if ((ch >= zeroDigit && ch <= nineDigit) || ch == sigDigit) {
+                        if (digitRightCount > 0) {
+                            patternError("Unexpected '" + ch + '\'', pattern);
+                        }
+                        if (ch == sigDigit) {
+                            ++sigDigitCount;
+                        } else {
+                            ++zeroDigitCount;
+                            if (ch != zeroDigit) {
+                                int p = digitLeftCount + zeroDigitCount + digitRightCount;
+                                if (incrementPos >= 0) {
+                                    while (incrementPos < p) {
+                                        incrementVal *= 10;
+                                        ++incrementPos;
+                                    }
+                                } else {
+                                    incrementPos = p;
+                                }
+                                incrementVal += ch - zeroDigit;
+                            }
+                        }
+                        if (groupingCount >= 0 && decimalPos < 0) {
+                            ++groupingCount;
+                        }
+                    } else if (ch == groupingSeparator) {
+                        // Bug 4212072 process the Localized pattern like
+                        // "'Fr. '#'##0.05;'Fr.-'#'##0.05" (Locale="CH", groupingSeparator
+                        // == QUOTE) [Richard/GCL]
+                        if (ch == QUOTE && (pos + 1) < pattern.length()) {
+                            char after = pattern.charAt(pos + 1);
+                            if (!(after == digit || (after >= zeroDigit && after <= nineDigit))) {
+                                // A quote outside quotes indicates either the opening
+                                // quote or two quotes, which is a quote literal. That is,
+                                // we have the first quote in 'do' or o''clock.
+                                if (after == QUOTE) {
+                                    ++pos;
+                                    // Fall through to append(ch)
+                                } else {
+                                    if (groupingCount < 0) {
+                                        subpart = 3; // quoted prefix subpart
+                                    } else {
+                                        // Transition to suffix subpart
+                                        subpart = 2; // suffix subpart
+                                        affix = suffix;
+                                        sub0Limit = pos--;
+                                    }
+                                    continue;
+                                }
+                            }
+                        }
+
+                        if (decimalPos >= 0) {
+                            patternError("Grouping separator after decimal", pattern);
+                        }
+                        groupingCount2 = groupingCount;
+                        groupingCount = 0;
+                    } else if (ch == decimalSeparator) {
+                        if (decimalPos >= 0) {
+                            patternError("Multiple decimal separators", pattern);
+                        }
+                        // Intentionally incorporate the digitRightCount, even though it
+                        // is illegal for this to be > 0 at this point. We check pattern
+                        // syntax below.
+                        decimalPos = digitLeftCount + zeroDigitCount + digitRightCount;
+                    } else {
+                        if (pattern.regionMatches(pos, exponent, 0, exponent.length())) {
+                            if (expDigits >= 0) {
+                                patternError("Multiple exponential symbols", pattern);
+                            }
+                            if (groupingCount >= 0) {
+                                patternError("Grouping separator in exponential", pattern);
+                            }
+                            pos += exponent.length();
+                            // Check for positive prefix
+                            if (pos < pattern.length() && pattern.charAt(pos) == plus) {
+                                expSignAlways = true;
+                                ++pos;
+                            }
+                            // Use lookahead to parse out the exponential part of the
+                            // pattern, then jump into suffix subpart.
+                            expDigits = 0;
+                            while (pos < pattern.length() && pattern.charAt(pos) == zeroDigit) {
+                                ++expDigits;
+                                ++pos;
+                            }
+
+                            // 1. Require at least one mantissa pattern digit
+                            // 2. Disallow "#+ @" in mantissa
+                            // 3. Require at least one exponent pattern digit
+                            if (((digitLeftCount + zeroDigitCount) < 1 &&
+                                 (sigDigitCount + digitRightCount) < 1)
+                                || (sigDigitCount > 0 && digitLeftCount > 0) || expDigits < 1) {
+                                patternError("Malformed exponential", pattern);
+                            }
+                        }
+                        // Transition to suffix subpart
+                        subpart = 2; // suffix subpart
+                        affix = suffix;
+                        sub0Limit = pos--; // backup: for() will increment
+                        continue;
+                    }
+                    break;
+                case 1: // Prefix subpart
+                case 2: // Suffix subpart
+                    // Process the prefix / suffix characters Process unquoted characters
+                    // seen in prefix or suffix subpart.
+
+                    // Several syntax characters implicitly begins the next subpart if we
+                    // are in the prefix; otherwise they are illegal if unquoted.
+                    if (ch == digit || ch == groupingSeparator || ch == decimalSeparator
+                            || (ch >= zeroDigit && ch <= nineDigit) || ch == sigDigit) {
+                        // Any of these characters implicitly begins the
+                        // next subpart if we are in the prefix
+                        if (subpart == 1) { // prefix subpart
+                            subpart = 0; // pattern proper subpart
+                            sub0Start = pos--; // Reprocess this character
+                            continue;
+                        } else if (ch == QUOTE) {
+                            // Bug 4212072 process the Localized pattern like
+                            // "'Fr. '#'##0.05;'Fr.-'#'##0.05" (Locale="CH",
+                            // groupingSeparator == QUOTE) [Richard/GCL]
+
+                            // A quote outside quotes indicates either the opening quote
+                            // or two quotes, which is a quote literal. That is, we have
+                            // the first quote in 'do' or o''clock.
+                            if ((pos + 1) < pattern.length() && pattern.charAt(pos + 1) == QUOTE) {
+                                ++pos;
+                                affix.append(ch);
+                            } else {
+                                subpart += 2; // open quote
+                            }
+                            continue;
+                        }
+                        patternError("Unquoted special character '" + ch + '\'', pattern);
+                    } else if (ch == CURRENCY_SIGN) {
+                        // Use lookahead to determine if the currency sign is
+                        // doubled or not.
+                        boolean doubled = (pos + 1) < pattern.length() &&
+                            pattern.charAt(pos + 1) == CURRENCY_SIGN;
+
+                        // Bug 4212072 To meet the need of expandAffix(String,
+                        // StirngBuffer) [Richard/GCL]
+                        if (doubled) {
+                            ++pos; // Skip over the doubled character
+                            affix.append(ch); // append two: one here, one below
+                            if ((pos + 1) < pattern.length() &&
+                                pattern.charAt(pos + 1) == CURRENCY_SIGN) {
+                                ++pos; // Skip over the tripled character
+                                affix.append(ch); // append again
+                                currencySignCnt = CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT;
+                            } else {
+                                currencySignCnt = CURRENCY_SIGN_COUNT_IN_ISO_FORMAT;
+                            }
+                        } else {
+                            currencySignCnt = CURRENCY_SIGN_COUNT_IN_SYMBOL_FORMAT;
+                        }
+                        // Fall through to append(ch)
+                    } else if (ch == QUOTE) {
+                        // A quote outside quotes indicates either the opening quote or
+                        // two quotes, which is a quote literal. That is, we have the
+                        // first quote in 'do' or o''clock.
+                        if ((pos + 1) < pattern.length() && pattern.charAt(pos + 1) == QUOTE) {
+                            ++pos;
+                            affix.append(ch); // append two: one here, one below
+                        } else {
+                            subpart += 2; // open quote
+                        }
+                        // Fall through to append(ch)
+                    } else if (ch == separator) {
+                        // Don't allow separators in the prefix, and don't allow
+                        // separators in the second pattern (part == 1).
+                        if (subpart == 1 || part == 1) {
+                            patternError("Unquoted special character '" + ch + '\'', pattern);
+                        }
+                        sub2Limit = pos++;
+                        break PARTLOOP; // Go to next part
+                    } else if (ch == percent || ch == perMill) {
+                        // Next handle characters which are appended directly.
+                        if (multpl != 1) {
+                            patternError("Too many percent/permille characters", pattern);
+                        }
+                        multpl = (ch == percent) ? 100 : 1000;
+                        // Convert to non-localized pattern
+                        ch = (ch == percent) ? PATTERN_PERCENT : PATTERN_PER_MILLE;
+                        // Fall through to append(ch)
+                    } else if (ch == minus) {
+                        // Convert to non-localized pattern
+                        ch = PATTERN_MINUS_SIGN;
+                        // Fall through to append(ch)
+                    } else if (ch == padEscape) {
+                        if (padPos >= 0) {
+                            patternError("Multiple pad specifiers", pattern);
+                        }
+                        if ((pos + 1) == pattern.length()) {
+                            patternError("Invalid pad specifier", pattern);
+                        }
+                        padPos = pos++; // Advance past pad char
+                        padChar = pattern.charAt(pos);
+                        continue;
+                    }
+                    affix.append(ch);
+                    break;
+                case 3: // Prefix subpart, in quote
+                case 4: // Suffix subpart, in quote
+                    // A quote within quotes indicates either the closing quote or two
+                    // quotes, which is a quote literal. That is, we have the second quote
+                    // in 'do' or 'don''t'.
+                    if (ch == QUOTE) {
+                        if ((pos + 1) < pattern.length() && pattern.charAt(pos + 1) == QUOTE) {
+                            ++pos;
+                            affix.append(ch);
+                        } else {
+                            subpart -= 2; // close quote
+                        }
+                        // Fall through to append(ch)
+                    }
+                    // NOTE: In ICU 2.2 there was code here to parse quoted percent and
+                    // permille characters _within quotes_ and give them special
+                    // meaning. This is incorrect, since quoted characters are literals
+                    // without special meaning.
+                    affix.append(ch);
+                    break;
+                }
+            }
+
+            if (subpart == 3 || subpart == 4) {
+                patternError("Unterminated quote", pattern);
+            }
+
+            if (sub0Limit == 0) {
+                sub0Limit = pattern.length();
+            }
+
+            if (sub2Limit == 0) {
+                sub2Limit = pattern.length();
+            }
+
+            // Handle patterns with no '0' pattern character. These patterns are legal,
+            // but must be recodified to make sense. "##.###" -> "#0.###". ".###" ->
+            // ".0##".
+            //
+            // We allow patterns of the form "####" to produce a zeroDigitCount of zero
+            // (got that?); although this seems like it might make it possible for
+            // format() to produce empty strings, format() checks for this condition and
+            // outputs a zero digit in this situation. Having a zeroDigitCount of zero
+            // yields a minimum integer digits of zero, which allows proper round-trip
+            // patterns. We don't want "#" to become "#0" when toPattern() is called (even
+            // though that's what it really is, semantically).
+            if (zeroDigitCount == 0 && sigDigitCount == 0 &&
+                digitLeftCount > 0 && decimalPos >= 0) {
+                // Handle "###.###" and "###." and ".###"
+                int n = decimalPos;
+                if (n == 0)
+                    ++n; // Handle ".###"
+                digitRightCount = digitLeftCount - n;
+                digitLeftCount = n - 1;
+                zeroDigitCount = 1;
+            }
+
+            // Do syntax checking on the digits, decimal points, and quotes.
+            if ((decimalPos < 0 && digitRightCount > 0 && sigDigitCount == 0)
+                || (decimalPos >= 0
+                    && (sigDigitCount > 0
+                        || decimalPos < digitLeftCount
+                        || decimalPos > (digitLeftCount + zeroDigitCount)))
+                || groupingCount == 0
+                || groupingCount2 == 0
+                || (sigDigitCount > 0 && zeroDigitCount > 0)
+                || subpart > 2) { // subpart > 2 == unmatched quote
+                patternError("Malformed pattern", pattern);
+            }
+
+            // Make sure pad is at legal position before or after affix.
+            if (padPos >= 0) {
+                if (padPos == start) {
+                    padPos = PAD_BEFORE_PREFIX;
+                } else if (padPos + 2 == sub0Start) {
+                    padPos = PAD_AFTER_PREFIX;
+                } else if (padPos == sub0Limit) {
+                    padPos = PAD_BEFORE_SUFFIX;
+                } else if (padPos + 2 == sub2Limit) {
+                    padPos = PAD_AFTER_SUFFIX;
+                } else {
+                    patternError("Illegal pad position", pattern);
+                }
+            }
+
+            if (part == 0) {
+                // Set negative affixes temporarily to match the positive
+                // affixes. Fix this up later after processing both parts.
+
+                // Bug 4212072 To meet the need of expandAffix(String, StirngBuffer)
+                // [Richard/GCL]
+                posPrefixPattern = negPrefixPattern = prefix.toString();
+                posSuffixPattern = negSuffixPattern = suffix.toString();
+
+                useExponentialNotation = (expDigits >= 0);
+                if (useExponentialNotation) {
+                    minExponentDigits = expDigits;
+                    exponentSignAlwaysShown = expSignAlways;
+                }
+                int digitTotalCount = digitLeftCount + zeroDigitCount + digitRightCount;
+                // The effectiveDecimalPos is the position the decimal is at or would be
+                // at if there is no decimal. Note that if decimalPos<0, then
+                // digitTotalCount == digitLeftCount + zeroDigitCount.
+                int effectiveDecimalPos = decimalPos >= 0 ? decimalPos : digitTotalCount;
+                boolean useSigDig = (sigDigitCount > 0);
+                setSignificantDigitsUsed(useSigDig);
+                if (useSigDig) {
+                    setMinimumSignificantDigits(sigDigitCount);
+                    setMaximumSignificantDigits(sigDigitCount + digitRightCount);
+                } else {
+                    int minInt = effectiveDecimalPos - digitLeftCount;
+                    setMinimumIntegerDigits(minInt);
+
+                    // Upper limit on integer and fraction digits for a Java double
+                    // [Richard/GCL]
+                    setMaximumIntegerDigits(useExponentialNotation ? digitLeftCount + minInt :
+                                            DOUBLE_INTEGER_DIGITS);
+                    _setMaximumFractionDigits(decimalPos >= 0 ?
+                                             (digitTotalCount - decimalPos) : 0);
+                    setMinimumFractionDigits(decimalPos >= 0 ?
+                                             (digitLeftCount + zeroDigitCount - decimalPos) : 0);
+                }
+                setGroupingUsed(groupingCount > 0);
+                this.groupingSize = (groupingCount > 0) ? groupingCount : 0;
+                this.groupingSize2 = (groupingCount2 > 0 && groupingCount2 != groupingCount)
+                    ? groupingCount2 : 0;
+                this.multiplier = multpl;
+                setDecimalSeparatorAlwaysShown(decimalPos == 0 || decimalPos == digitTotalCount);
+                if (padPos >= 0) {
+                    padPosition = padPos;
+                    formatWidth = sub0Limit - sub0Start; // to be fixed up below
+                    pad = padChar;
+                } else {
+                    formatWidth = 0;
+                }
+                if (incrementVal != 0) {
+                    // BigDecimal scale cannot be negative (even though this makes perfect
+                    // sense), so we need to handle this.
+                    int scale = incrementPos - effectiveDecimalPos;
+                    roundingIncrementICU = BigDecimal.valueOf(incrementVal, scale > 0 ? scale : 0);
+                    if (scale < 0) {
+                        roundingIncrementICU = roundingIncrementICU.movePointRight(-scale);
+                    }
+                    roundingMode = BigDecimal.ROUND_HALF_EVEN;
+                } else {
+                    setRoundingIncrement((BigDecimal) null);
+                }
+
+                // Update currency sign count for the new pattern
+                currencySignCount = currencySignCnt;
+            } else {
+                // Bug 4212072 To meet the need of expandAffix(String, StirngBuffer)
+                // [Richard/GCL]
+                negPrefixPattern = prefix.toString();
+                negSuffixPattern = suffix.toString();
+                gotNegative = true;
+            }
+        }
+
+
+        // Bug 4140009 Process the empty pattern [Richard/GCL]
+        if (pattern.length() == 0) {
+            posPrefixPattern = posSuffixPattern = "";
+            setMinimumIntegerDigits(0);
+            setMaximumIntegerDigits(DOUBLE_INTEGER_DIGITS);
+            setMinimumFractionDigits(0);
+            _setMaximumFractionDigits(DOUBLE_FRACTION_DIGITS);
+        }
+
+        // If there was no negative pattern, or if the negative pattern is identical to
+        // the positive pattern, then prepend the minus sign to the positive pattern to
+        // form the negative pattern.
+
+        // Bug 4212072 To meet the need of expandAffix(String, StirngBuffer) [Richard/GCL]
+
+        if (!gotNegative ||
+            (negPrefixPattern.equals(posPrefixPattern)
+             && negSuffixPattern.equals(posSuffixPattern))) {
+            negSuffixPattern = posSuffixPattern;
+            negPrefixPattern = PATTERN_MINUS_SIGN + posPrefixPattern;
+        }
+        setLocale(null, null);
+        // save the pattern
+        formatPattern = pattern;
+
+        // special handlings for currency instance
+        if (currencySignCount != CURRENCY_SIGN_COUNT_ZERO) {
+            // reset rounding increment and max/min fractional digits
+            // by the currency
+            Currency theCurrency = getCurrency();
+            if (theCurrency != null) {
+                setRoundingIncrement(theCurrency.getRoundingIncrement(currencyUsage));
+                int d = theCurrency.getDefaultFractionDigits(currencyUsage);
+                setMinimumFractionDigits(d);
+                _setMaximumFractionDigits(d);
+            }
+
+            // initialize currencyPluralInfo if needed
+            if (currencySignCount == CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT
+                && currencyPluralInfo == null) {
+                currencyPluralInfo = new CurrencyPluralInfo(symbols.getULocale());
+            }
+        }
+        resetActualRounding();
+    }
+
+
+    private void patternError(String msg, String pattern) {
+        throw new IllegalArgumentException(msg + " in pattern \"" + pattern + '"');
+    }
+
+
+    // Rewrite the following 4 "set" methods Upper limit on integer and fraction digits
+    // for a Java double [Richard/GCL]
+
+    /**
+     * Sets the maximum number of digits allowed in the integer portion of a number. This
+     * override limits the integer digit count to 309.
+     *
+     * @see NumberFormat#setMaximumIntegerDigits
+     * @stable ICU 2.0
+     */
+    @Override
+    public void setMaximumIntegerDigits(int newValue) {
+        super.setMaximumIntegerDigits(Math.min(newValue, DOUBLE_INTEGER_DIGITS));
+    }
+
+    /**
+     * Sets the minimum number of digits allowed in the integer portion of a number. This
+     * override limits the integer digit count to 309.
+     *
+     * @see NumberFormat#setMinimumIntegerDigits
+     * @stable ICU 2.0
+     */
+    @Override
+    public void setMinimumIntegerDigits(int newValue) {
+        super.setMinimumIntegerDigits(Math.min(newValue, DOUBLE_INTEGER_DIGITS));
+    }
+
+    /**
+     * {@icu} Returns the minimum number of significant digits that will be
+     * displayed. This value has no effect unless {@link #areSignificantDigitsUsed()}
+     * returns true.
+     *
+     * @return the fewest significant digits that will be shown
+     * @stable ICU 3.0
+     */
+    public int getMinimumSignificantDigits() {
+        return minSignificantDigits;
+    }
+
+    /**
+     * {@icu} Returns the maximum number of significant digits that will be
+     * displayed. This value has no effect unless {@link #areSignificantDigitsUsed()}
+     * returns true.
+     *
+     * @return the most significant digits that will be shown
+     * @stable ICU 3.0
+     */
+    public int getMaximumSignificantDigits() {
+        return maxSignificantDigits;
+    }
+
+    /**
+     * {@icu} Sets the minimum number of significant digits that will be displayed. If
+     * <code>min</code> is less than one then it is set to one. If the maximum significant
+     * digits count is less than <code>min</code>, then it is set to <code>min</code>.
+     * This function also enables the use of significant digits by this formatter -
+     * {@link #areSignificantDigitsUsed()} will return true.
+     *
+     * @param min the fewest significant digits to be shown
+     * @stable ICU 3.0
+     */
+    public void setMinimumSignificantDigits(int min) {
+        if (min < 1) {
+            min = 1;
+        }
+        // pin max sig dig to >= min
+        int max = Math.max(maxSignificantDigits, min);
+        minSignificantDigits = min;
+        maxSignificantDigits = max;
+        setSignificantDigitsUsed(true);
+    }
+
+    /**
+     * {@icu} Sets the maximum number of significant digits that will be displayed. If
+     * <code>max</code> is less than one then it is set to one. If the minimum significant
+     * digits count is greater than <code>max</code>, then it is set to <code>max</code>.
+     * This function also enables the use of significant digits by this formatter -
+     * {@link #areSignificantDigitsUsed()} will return true.
+     *
+     * @param max the most significant digits to be shown
+     * @stable ICU 3.0
+     */
+    public void setMaximumSignificantDigits(int max) {
+        if (max < 1) {
+            max = 1;
+        }
+        // pin min sig dig to 1..max
+        int min = Math.min(minSignificantDigits, max);
+        minSignificantDigits = min;
+        maxSignificantDigits = max;
+        setSignificantDigitsUsed(true);
+    }
+
+    /**
+     * {@icu} Returns true if significant digits are in use or false if integer and
+     * fraction digit counts are in use.
+     *
+     * @return true if significant digits are in use
+     * @stable ICU 3.0
+     */
+    public boolean areSignificantDigitsUsed() {
+        return useSignificantDigits;
+    }
+
+    /**
+     * {@icu} Sets whether significant digits are in use, or integer and fraction digit
+     * counts are in use.
+     *
+     * @param useSignificantDigits true to use significant digits, or false to use integer
+     * and fraction digit counts
+     * @stable ICU 3.0
+     */
+    public void setSignificantDigitsUsed(boolean useSignificantDigits) {
+        this.useSignificantDigits = useSignificantDigits;
+    }
+
+    /**
+     * Sets the <tt>Currency</tt> object used to display currency amounts. This takes
+     * effect immediately, if this format is a currency format. If this format is not a
+     * currency format, then the currency object is used if and when this object becomes a
+     * currency format through the application of a new pattern.
+     *
+     * @param theCurrency new currency object to use. Must not be null.
+     * @stable ICU 2.2
+     */
+    @Override
+    public void setCurrency(Currency theCurrency) {
+        // If we are a currency format, then modify our affixes to
+        // encode the currency symbol for the given currency in our
+        // locale, and adjust the decimal digits and rounding for the
+        // given currency.
+
+        super.setCurrency(theCurrency);
+        if (theCurrency != null) {
+            String s = theCurrency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null);
+            symbols.setCurrency(theCurrency);
+            symbols.setCurrencySymbol(s);
+        }
+
+        if (currencySignCount != CURRENCY_SIGN_COUNT_ZERO) {
+            if (theCurrency != null) {
+                setRoundingIncrement(theCurrency.getRoundingIncrement(currencyUsage));
+                int d = theCurrency.getDefaultFractionDigits(currencyUsage);
+                setMinimumFractionDigits(d);
+                setMaximumFractionDigits(d);
+            }
+            if (currencySignCount != CURRENCY_SIGN_COUNT_IN_PLURAL_FORMAT) {
+                // This is not necessary for plural format type
+                // because affixes will be resolved in subformat
+                expandAffixes(null);
+            }
+        }
+    }
+
+    /**
+     * Sets the <tt>Currency Usage</tt> object used to display currency.
+     * This takes effect immediately, if this format is a
+     * currency format.
+     * @param newUsage new currency context object to use.
+     * @stable ICU 54
+     */
+    public void setCurrencyUsage(CurrencyUsage newUsage) {
+        if (newUsage == null) {
+            throw new NullPointerException("return value is null at method AAA");
+        }
+        currencyUsage = newUsage;
+        Currency theCurrency = this.getCurrency();
+
+        // We set rounding/digit based on currency context
+        if (theCurrency != null) {
+            setRoundingIncrement(theCurrency.getRoundingIncrement(currencyUsage));
+            int d = theCurrency.getDefaultFractionDigits(currencyUsage);
+            setMinimumFractionDigits(d);
+            _setMaximumFractionDigits(d);
+        }
+    }
+
+    /**
+     * Returns the <tt>Currency Usage</tt> object used to display currency
+     * @stable ICU 54
+     */
+    public CurrencyUsage getCurrencyUsage() {
+        return currencyUsage;
+    }
+
+    /**
+     * Returns the currency in effect for this formatter. Subclasses should override this
+     * method as needed. Unlike getCurrency(), this method should never return null.
+     *
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    @Deprecated
+    @Override
+    protected Currency getEffectiveCurrency() {
+        Currency c = getCurrency();
+        if (c == null) {
+            c = Currency.getInstance(symbols.getInternationalCurrencySymbol());
+        }
+        return c;
+    }
+
+    /**
+     * Sets the maximum number of digits allowed in the fraction portion of a number. This
+     * override limits the fraction digit count to 340.
+     *
+     * @see NumberFormat#setMaximumFractionDigits
+     * @stable ICU 2.0
+     */
+    @Override
+    public void setMaximumFractionDigits(int newValue) {
+        _setMaximumFractionDigits(newValue);
+        resetActualRounding();
+    }
+
+    /*
+     * Internal method for DecimalFormat, setting maximum fractional digits
+     * without triggering actual rounding recalculated.
+     */
+    private void _setMaximumFractionDigits(int newValue) {
+        super.setMaximumFractionDigits(Math.min(newValue, DOUBLE_FRACTION_DIGITS));
+    }
+
+    /**
+     * Sets the minimum number of digits allowed in the fraction portion of a number. This
+     * override limits the fraction digit count to 340.
+     *
+     * @see NumberFormat#setMinimumFractionDigits
+     * @stable ICU 2.0
+     */
+    @Override
+    public void setMinimumFractionDigits(int newValue) {
+        super.setMinimumFractionDigits(Math.min(newValue, DOUBLE_FRACTION_DIGITS));
+    }
+
+    /**
+     * Sets whether {@link #parse(String, ParsePosition)} returns BigDecimal. The
+     * default value is false.
+     *
+     * @param value true if {@link #parse(String, ParsePosition)}
+     * returns BigDecimal.
+     * @stable ICU 3.6
+     */
+    public void setParseBigDecimal(boolean value) {
+        parseBigDecimal = value;
+    }
+
+    /**
+     * Returns whether {@link #parse(String, ParsePosition)} returns BigDecimal.
+     *
+     * @return true if {@link #parse(String, ParsePosition)} returns BigDecimal.
+     * @stable ICU 3.6
+     */
+    public boolean isParseBigDecimal() {
+        return parseBigDecimal;
+    }
+
+    /**
+    * Set the maximum number of exponent digits when parsing a number.
+    * If the limit is set too high, an OutOfMemoryException may be triggered.
+    * The default value is 1000.
+    * @param newValue the new limit
+    * @stable ICU 51
+    */
+    public void setParseMaxDigits(int newValue) {
+        if (newValue > 0) {
+            PARSE_MAX_EXPONENT = newValue;
+        }
+    }
+
+    /**
+    * Get the current maximum number of exponent digits when parsing a
+    * number.
+    * @return the maximum number of exponent digits for parsing
+    * @stable ICU 51
+    */
+    public int getParseMaxDigits() {
+        return PARSE_MAX_EXPONENT;
+    }
+
+    private void writeObject(ObjectOutputStream stream) throws IOException {
+        // Ticket#6449 Format.Field instances are not serializable. When
+        // formatToCharacterIterator is called, attributes (ArrayList) stores
+        // FieldPosition instances with NumberFormat.Field. Because NumberFormat.Field is
+        // not serializable, we need to clear the contents of the list when writeObject is
+        // called. We could remove the field or make it transient, but it will break
+        // serialization compatibility.
+        attributes.clear();
+
+        stream.defaultWriteObject();
+    }
+
+    /**
+     * First, read the default serializable fields from the stream. Then if
+     * <code>serialVersionOnStream</code> is less than 1, indicating that the stream was
+     * written by JDK 1.1, initialize <code>useExponentialNotation</code> to false, since
+     * it was not present in JDK 1.1. Finally, set serialVersionOnStream back to the
+     * maximum allowed value so that default serialization will work properly if this
+     * object is streamed out again.
+     */
+    private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
+        stream.defaultReadObject();
+
+        // Bug 4185761 validate fields [Richard/GCL]
+
+        // We only need to check the maximum counts because NumberFormat .readObject has
+        // already ensured that the maximum is greater than the minimum count.
+
+        // Commented for compatibility with previous version, and reserved for further use
+        // if (getMaximumIntegerDigits() > DOUBLE_INTEGER_DIGITS ||
+        // getMaximumFractionDigits() > DOUBLE_FRACTION_DIGITS) { throw new
+        // InvalidObjectException("Digit count out of range"); }
+
+
+        // Truncate the maximumIntegerDigits to DOUBLE_INTEGER_DIGITS and
+        // maximumFractionDigits to DOUBLE_FRACTION_DIGITS
+
+        if (getMaximumIntegerDigits() > DOUBLE_INTEGER_DIGITS) {
+            setMaximumIntegerDigits(DOUBLE_INTEGER_DIGITS);
+        }
+        if (getMaximumFractionDigits() > DOUBLE_FRACTION_DIGITS) {
+            _setMaximumFractionDigits(DOUBLE_FRACTION_DIGITS);
+        }
+        if (serialVersionOnStream < 2) {
+            exponentSignAlwaysShown = false;
+            setInternalRoundingIncrement(null);
+            roundingMode = BigDecimal.ROUND_HALF_EVEN;
+            formatWidth = 0;
+            pad = ' ';
+            padPosition = PAD_BEFORE_PREFIX;
+            if (serialVersionOnStream < 1) {
+                // Didn't have exponential fields
+                useExponentialNotation = false;
+            }
+        }
+        if (serialVersionOnStream < 3) {
+            // Versions prior to 3 do not store a currency object.  Create one to match
+            // the DecimalFormatSymbols object.
+            setCurrencyForSymbols();
+        }
+        if (serialVersionOnStream < 4) {
+            currencyUsage = CurrencyUsage.STANDARD;
+        }
+        serialVersionOnStream = currentSerialVersion;
+        digitList = new DigitList();
+
+        if (roundingIncrement != null) {
+            setInternalRoundingIncrement(new BigDecimal(roundingIncrement));
+        }
+        resetActualRounding();
+    }
+
+    private void setInternalRoundingIncrement(BigDecimal value) {
+        roundingIncrementICU = value;
+        roundingIncrement = value == null ? null : value.toBigDecimal();
+    }
+
+    // ----------------------------------------------------------------------
+    // INSTANCE VARIABLES
+    // ----------------------------------------------------------------------
+
+    private transient DigitList digitList = new DigitList();
+
+    /**
+     * The symbol used as a prefix when formatting positive numbers, e.g. "+".
+     *
+     * @serial
+     * @see #getPositivePrefix
+     */
+    private String positivePrefix = "";
+
+    /**
+     * The symbol used as a suffix when formatting positive numbers. This is often an
+     * empty string.
+     *
+     * @serial
+     * @see #getPositiveSuffix
+     */
+    private String positiveSuffix = "";
+
+    /**
+     * The symbol used as a prefix when formatting negative numbers, e.g. "-".
+     *
+     * @serial
+     * @see #getNegativePrefix
+     */
+    private String negativePrefix = "-";
+
+    /**
+     * The symbol used as a suffix when formatting negative numbers. This is often an
+     * empty string.
+     *
+     * @serial
+     * @see #getNegativeSuffix
+     */
+    private String negativeSuffix = "";
+
+    /**
+     * The prefix pattern for non-negative numbers. This variable corresponds to
+     * <code>positivePrefix</code>.
+     *
+     * <p>This pattern is expanded by the method <code>expandAffix()</code> to
+     * <code>positivePrefix</code> to update the latter to reflect changes in
+     * <code>symbols</code>. If this variable is <code>null</code> then
+     * <code>positivePrefix</code> is taken as a literal value that does not change when
+     * <code>symbols</code> changes.  This variable is always <code>null</code> for
+     * <code>DecimalFormat</code> objects older than stream version 2 restored from
+     * stream.
+     *
+     * @serial
+     */
+    // [Richard/GCL]
+    private String posPrefixPattern;
+
+    /**
+     * The suffix pattern for non-negative numbers. This variable corresponds to
+     * <code>positiveSuffix</code>. This variable is analogous to
+     * <code>posPrefixPattern</code>; see that variable for further documentation.
+     *
+     * @serial
+     */
+    // [Richard/GCL]
+    private String posSuffixPattern;
+
+    /**
+     * The prefix pattern for negative numbers. This variable corresponds to
+     * <code>negativePrefix</code>. This variable is analogous to
+     * <code>posPrefixPattern</code>; see that variable for further documentation.
+     *
+     * @serial
+     */
+    // [Richard/GCL]
+    private String negPrefixPattern;
+
+    /**
+     * The suffix pattern for negative numbers. This variable corresponds to
+     * <code>negativeSuffix</code>. This variable is analogous to
+     * <code>posPrefixPattern</code>; see that variable for further documentation.
+     *
+     * @serial
+     */
+    // [Richard/GCL]
+    private String negSuffixPattern;
+
+    /**
+     * Formatter for ChoiceFormat-based currency names. If this field is not null, then
+     * delegate to it to format currency symbols.
+     * TODO: This is obsolete: Remove, and design extensible serialization. ICU ticket #12090.
+     *
+     * @since ICU 2.6
+     */
+    private ChoiceFormat currencyChoice;
+
+    /**
+     * The multiplier for use in percent, permill, etc.
+     *
+     * @serial
+     * @see #getMultiplier
+     */
+    private int multiplier = 1;
+
+    /**
+     * The number of digits between grouping separators in the integer portion of a
+     * number. Must be greater than 0 if <code>NumberFormat.groupingUsed</code> is true.
+     *
+     * @serial
+     * @see #getGroupingSize
+     * @see NumberFormat#isGroupingUsed
+     */
+    private byte groupingSize = 3; // invariant, > 0 if useThousands
+
+    /**
+     * The secondary grouping size. This is only used for Hindi numerals, which use a
+     * primary grouping of 3 and a secondary grouping of 2, e.g., "12,34,567". If this
+     * value is less than 1, then secondary grouping is equal to the primary grouping.
+     *
+     */
+    private byte groupingSize2 = 0;
+
+    /**
+     * If true, forces the decimal separator to always appear in a formatted number, even
+     * if the fractional part of the number is zero.
+     *
+     * @serial
+     * @see #isDecimalSeparatorAlwaysShown
+     */
+    private boolean decimalSeparatorAlwaysShown = false;
+
+    /**
+     * The <code>DecimalFormatSymbols</code> object used by this format. It contains the
+     * symbols used to format numbers, e.g. the grouping separator, decimal separator, and
+     * so on.
+     *
+     * @serial
+     * @see #setDecimalFormatSymbols
+     * @see DecimalFormatSymbols
+     */
+    private DecimalFormatSymbols symbols = null; // LIU new DecimalFormatSymbols();
+
+    /**
+     * True to use significant digits rather than integer and fraction digit counts.
+     *
+     * @serial
+     * @since ICU 3.0
+     */
+    private boolean useSignificantDigits = false;
+
+    /**
+     * The minimum number of significant digits to show. Must be &gt;= 1 and &lt;=
+     * maxSignificantDigits. Ignored unless useSignificantDigits == true.
+     *
+     * @serial
+     * @since ICU 3.0
+     */
+    private int minSignificantDigits = 1;
+
+    /**
+     * The maximum number of significant digits to show. Must be &gt;=
+     * minSignficantDigits. Ignored unless useSignificantDigits == true.
+     *
+     * @serial
+     * @since ICU 3.0
+     */
+    private int maxSignificantDigits = 6;
+
+    /**
+     * True to force the use of exponential (i.e. scientific) notation
+     * when formatting numbers.
+     *
+     *<p> Note that the JDK 1.2 public API provides no way to set this
+     * field, even though it is supported by the implementation and
+     * the stream format. The intent is that this will be added to the
+     * API in the future.
+     *
+     * @serial
+     */
+    private boolean useExponentialNotation; // Newly persistent in JDK 1.2
+
+    /**
+     * The minimum number of digits used to display the exponent when a number is
+     * formatted in exponential notation.  This field is ignored if
+     * <code>useExponentialNotation</code> is not true.
+     *
+     * <p>Note that the JDK 1.2 public API provides no way to set this field, even though
+     * it is supported by the implementation and the stream format. The intent is that
+     * this will be added to the API in the future.
+     *
+     * @serial
+     */
+    private byte minExponentDigits; // Newly persistent in JDK 1.2
+
+    /**
+     * If true, the exponent is always prefixed with either the plus sign or the minus
+     * sign. Otherwise, only negative exponents are prefixed with the minus sign. This has
+     * no effect unless <code>useExponentialNotation</code> is true.
+     *
+     * @serial
+     * @since AlphaWorks NumberFormat
+     */
+    private boolean exponentSignAlwaysShown = false;
+
+    /**
+     * The value to which numbers are rounded during formatting. For example, if the
+     * rounding increment is 0.05, then 13.371 would be formatted as 13.350, assuming 3
+     * fraction digits. Has the value <code>null</code> if rounding is not in effect, or a
+     * positive value if rounding is in effect. Default value <code>null</code>.
+     *
+     * @serial
+     * @since AlphaWorks NumberFormat
+     */
+    // Note: this is kept in sync with roundingIncrementICU.
+    // it is only kept around to avoid a conversion when formatting a java.math.BigDecimal
+    private java.math.BigDecimal roundingIncrement = null;
+
+    /**
+     * The value to which numbers are rounded during formatting. For example, if the
+     * rounding increment is 0.05, then 13.371 would be formatted as 13.350, assuming 3
+     * fraction digits. Has the value <code>null</code> if rounding is not in effect, or a
+     * positive value if rounding is in effect. Default value <code>null</code>. WARNING:
+     * the roundingIncrement value is the one serialized.
+     *
+     * @serial
+     * @since AlphaWorks NumberFormat
+     */
+    private transient BigDecimal roundingIncrementICU = null;
+
+    /**
+     * The rounding mode. This value controls any rounding operations which occur when
+     * applying a rounding increment or when reducing the number of fraction digits to
+     * satisfy a maximum fraction digits limit. The value may assume any of the
+     * <code>BigDecimal</code> rounding mode values. Default value
+     * <code>BigDecimal.ROUND_HALF_EVEN</code>.
+     *
+     * @serial
+     * @since AlphaWorks NumberFormat
+     */
+    private int roundingMode = BigDecimal.ROUND_HALF_EVEN;
+
+    /**
+     * Operations on <code>BigDecimal</code> numbers are controlled by a {@link
+     * MathContext} object, which provides the context (precision and other information)
+     * for the operation. The default <code>MathContext</code> settings are
+     * <code>digits=0, form=PLAIN, lostDigits=false, roundingMode=ROUND_HALF_UP</code>;
+     * these settings perform fixed point arithmetic with unlimited precision, as defined
+     * for the original BigDecimal class in Java 1.1 and Java 1.2
+     */
+    // context for plain unlimited math
+    private MathContext mathContext = new MathContext(0, MathContext.PLAIN);
+
+    /**
+     * The padded format width, or zero if there is no padding. Must be &gt;= 0. Default
+     * value zero.
+     *
+     * @serial
+     * @since AlphaWorks NumberFormat
+     */
+    private int formatWidth = 0;
+
+    /**
+     * The character used to pad the result of format to <code>formatWidth</code>, if
+     * padding is in effect. Default value ' '.
+     *
+     * @serial
+     * @since AlphaWorks NumberFormat
+     */
+    private char pad = ' ';
+
+    /**
+     * The position in the string at which the <code>pad</code> character will be
+     * inserted, if padding is in effect.  Must have a value from
+     * <code>PAD_BEFORE_PREFIX</code> to <code>PAD_AFTER_SUFFIX</code>. Default value
+     * <code>PAD_BEFORE_PREFIX</code>.
+     *
+     * @serial
+     * @since AlphaWorks NumberFormat
+     */
+    private int padPosition = PAD_BEFORE_PREFIX;
+
+    /**
+     * True if {@link #parse(String, ParsePosition)} to return BigDecimal rather than
+     * Long, Double or BigDecimal except special values. This property is introduced for
+     * J2SE 5 compatibility support.
+     *
+     * @serial
+     * @since ICU 3.6
+     * @see #setParseBigDecimal(boolean)
+     * @see #isParseBigDecimal()
+     */
+    private boolean parseBigDecimal = false;
+
+    /**
+     * The currency usage for the NumberFormat(standard or cash usage).
+     * It is used as STANDARD by default
+     * @since ICU 54
+     */
+    private CurrencyUsage currencyUsage = CurrencyUsage.STANDARD;
+
+    // ----------------------------------------------------------------------
+
+    static final int currentSerialVersion = 4;
+
+    /**
+     * The internal serial version which says which version was written Possible values
+     * are:
+     *
+     * <ul>
+     *
+     * <li><b>0</b> (default): versions before JDK 1.2
+     *
+     * <li><b>1</b>: version from JDK 1.2 and later, which includes the two new fields
+     * <code>useExponentialNotation</code> and <code>minExponentDigits</code>.
+     *
+     * <li><b>2</b>: version on AlphaWorks, which adds roundingMode, formatWidth, pad,
+     * padPosition, exponentSignAlwaysShown, roundingIncrement.
+     *
+     * <li><b>3</b>: ICU 2.2. Adds currency object.
+     *
+     * <li><b>4</b>: ICU 54. Adds currency usage(standard vs cash)
+     *
+     * </ul>
+     *
+     * @serial
+     */
+    private int serialVersionOnStream = currentSerialVersion;
+
+    // ----------------------------------------------------------------------
+    // CONSTANTS
+    // ----------------------------------------------------------------------
+
+    /**
+     * {@icu} Constant for {@link #getPadPosition()} and {@link #setPadPosition(int)} to
+     * specify pad characters inserted before the prefix.
+     *
+     * @see #setPadPosition
+     * @see #getPadPosition
+     * @see #PAD_AFTER_PREFIX
+     * @see #PAD_BEFORE_SUFFIX
+     * @see #PAD_AFTER_SUFFIX
+     * @stable ICU 2.0
+     */
+    public static final int PAD_BEFORE_PREFIX = 0;
+
+    /**
+     * {@icu} Constant for {@link #getPadPosition()} and {@link #setPadPosition(int)} to
+     * specify pad characters inserted after the prefix.
+     *
+     * @see #setPadPosition
+     * @see #getPadPosition
+     * @see #PAD_BEFORE_PREFIX
+     * @see #PAD_BEFORE_SUFFIX
+     * @see #PAD_AFTER_SUFFIX
+     * @stable ICU 2.0
+     */
+    public static final int PAD_AFTER_PREFIX = 1;
+
+    /**
+     * {@icu} Constant for {@link #getPadPosition()} and {@link #setPadPosition(int)} to
+     * specify pad characters inserted before the suffix.
+     *
+     * @see #setPadPosition
+     * @see #getPadPosition
+     * @see #PAD_BEFORE_PREFIX
+     * @see #PAD_AFTER_PREFIX
+     * @see #PAD_AFTER_SUFFIX
+     * @stable ICU 2.0
+     */
+    public static final int PAD_BEFORE_SUFFIX = 2;
+
+    /**
+     * {@icu} Constant for {@link #getPadPosition()} and {@link #setPadPosition(int)} to
+     * specify pad characters inserted after the suffix.
+     *
+     * @see #setPadPosition
+     * @see #getPadPosition
+     * @see #PAD_BEFORE_PREFIX
+     * @see #PAD_AFTER_PREFIX
+     * @see #PAD_BEFORE_SUFFIX
+     * @stable ICU 2.0
+     */
+    public static final int PAD_AFTER_SUFFIX = 3;
+
+    // Constants for characters used in programmatic (unlocalized) patterns.
+    static final char PATTERN_ZERO_DIGIT = '0';
+    static final char PATTERN_ONE_DIGIT = '1';
+    static final char PATTERN_TWO_DIGIT = '2';
+    static final char PATTERN_THREE_DIGIT = '3';
+    static final char PATTERN_FOUR_DIGIT = '4';
+    static final char PATTERN_FIVE_DIGIT = '5';
+    static final char PATTERN_SIX_DIGIT = '6';
+    static final char PATTERN_SEVEN_DIGIT = '7';
+    static final char PATTERN_EIGHT_DIGIT = '8';
+    static final char PATTERN_NINE_DIGIT = '9';
+    static final char PATTERN_GROUPING_SEPARATOR = ',';
+    static final char PATTERN_DECIMAL_SEPARATOR = '.';
+    static final char PATTERN_DIGIT = '#';
+    static final char PATTERN_SIGNIFICANT_DIGIT = '@';
+    static final char PATTERN_EXPONENT = 'E';
+    static final char PATTERN_PLUS_SIGN = '+';
+    static final char PATTERN_MINUS_SIGN = '-';
+
+    // Affix
+    private static final char PATTERN_PER_MILLE = '\u2030';
+    private static final char PATTERN_PERCENT = '%';
+    static final char PATTERN_PAD_ESCAPE = '*';
+
+    // Other
+    private static final char PATTERN_SEPARATOR = ';';
+
+    // Pad escape is package private to allow access by DecimalFormatSymbols.
+    // Also plus sign. Also exponent.
+
+    /**
+     * The CURRENCY_SIGN is the standard Unicode symbol for currency. It is used in
+     * patterns and substitued 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.
+     *
+     * The CURRENCY_SIGN is not localized.
+     */
+    private static final char CURRENCY_SIGN = '\u00A4';
+
+    private static final char QUOTE = '\'';
+
+    /**
+     * Upper limit on integer and fraction digits for a Java double [Richard/GCL]
+     */
+    static final int DOUBLE_INTEGER_DIGITS = 309;
+    static final int DOUBLE_FRACTION_DIGITS = 340;
+
+    /**
+     * When someone turns on scientific mode, we assume that more than this number of
+     * digits is due to flipping from some other mode that didn't restrict the maximum,
+     * and so we force 1 integer digit. We don't bother to track and see if someone is
+     * using exponential notation with more than this number, it wouldn't make sense
+     * anyway, and this is just to make sure that someone turning on scientific mode with
+     * default settings doesn't end up with lots of zeroes.
+     */
+    static final int MAX_SCIENTIFIC_INTEGER_DIGITS = 8;
+
+    // Proclaim JDK 1.1 serial compatibility.
+    private static final long serialVersionUID = 864413376551465018L;
+
+    private ArrayList<FieldPosition> attributes = new ArrayList<FieldPosition>();
+
+    // The following are used in currency format
+
+    // -- triple currency sign char array
+    // private static final char[] tripleCurrencySign = {0xA4, 0xA4, 0xA4};
+    // -- triple currency sign string
+    // private static final String tripleCurrencyStr = new String(tripleCurrencySign);
+    //
+    // -- default currency plural pattern char array
+    // private static final char[] defaultCurrencyPluralPatternChar =
+    //   {0, '.', '#', '#', ' ', 0xA4, 0xA4, 0xA4};
+    // -- default currency plural pattern string
+    // private static final String defaultCurrencyPluralPattern =
+    //     new String(defaultCurrencyPluralPatternChar);
+
+    // pattern used in this formatter
+    private String formatPattern = "";
+    // style is only valid when decimal formatter is constructed by
+    // DecimalFormat(pattern, decimalFormatSymbol, style)
+    private int style = NumberFormat.NUMBERSTYLE;
+    /**
+     * Represents whether this is a currency format, and which currency format style. 0:
+     * not currency format type; 1: currency style -- symbol name, such as "$" for US
+     * dollar. 2: currency style -- ISO name, such as USD for US dollar. 3: currency style
+     * -- plural long name, such as "US Dollar" for "1.00 US Dollar", or "US Dollars" for
+     * "3.00 US Dollars".
+     */
+    private int currencySignCount = CURRENCY_SIGN_COUNT_ZERO;
+
+    /**
+     * For parsing purposes, we need to remember all prefix patterns and suffix patterns
+     * of every currency format pattern, including the pattern of the default currency
+     * style, ISO currency style, and plural currency style. The patterns are set through
+     * applyPattern. The following are used to represent the affix patterns in currency
+     * plural formats.
+     */
+    private static final class AffixForCurrency {
+        // negative prefix pattern
+        private String negPrefixPatternForCurrency = null;
+        // negative suffix pattern
+        private String negSuffixPatternForCurrency = null;
+        // positive prefix pattern
+        private String posPrefixPatternForCurrency = null;
+        // positive suffix pattern
+        private String posSuffixPatternForCurrency = null;
+        private final int patternType;
+
+        public AffixForCurrency(String negPrefix, String negSuffix, String posPrefix,
+                                String posSuffix, int type) {
+            negPrefixPatternForCurrency = negPrefix;
+            negSuffixPatternForCurrency = negSuffix;
+            posPrefixPatternForCurrency = posPrefix;
+            posSuffixPatternForCurrency = posSuffix;
+            patternType = type;
+        }
+
+        public String getNegPrefix() {
+            return negPrefixPatternForCurrency;
+        }
+
+        public String getNegSuffix() {
+            return negSuffixPatternForCurrency;
+        }
+
+        public String getPosPrefix() {
+            return posPrefixPatternForCurrency;
+        }
+
+        public String getPosSuffix() {
+            return posSuffixPatternForCurrency;
+        }
+
+        public int getPatternType() {
+            return patternType;
+        }
+    }
+
+    // Affix pattern set for currency.  It is a set of AffixForCurrency, each element of
+    // the set saves the negative prefix, negative suffix, positive prefix, and positive
+    // suffix of a pattern.
+    private transient Set<AffixForCurrency> affixPatternsForCurrency = null;
+
+    // For currency parsing. Since currency parsing needs to parse against all currency
+    // patterns, before the parsing, we need to set up the affix patterns for all currencies.
+    private transient boolean isReadyForParsing = false;
+
+    // Information needed for DecimalFormat to format/parse currency plural.
+    private CurrencyPluralInfo currencyPluralInfo = null;
+
+    /**
+     * Unit is an immutable class for the textual representation of a unit, in
+     * particular its prefix and suffix.
+     *
+     * @author rocketman
+     *
+     */
+    static class Unit {
+        private final String prefix;
+        private final String suffix;
+
+        public Unit(String prefix, String suffix) {
+            this.prefix = prefix;
+            this.suffix = suffix;
+        }
+
+        public void writeSuffix(StringBuffer toAppendTo) {
+            toAppendTo.append(suffix);
+        }
+
+        public void writePrefix(StringBuffer toAppendTo) {
+            toAppendTo.append(prefix);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (!(obj instanceof Unit)) {
+                return false;
+            }
+            Unit other = (Unit) obj;
+            return prefix.equals(other.prefix) && suffix.equals(other.suffix);
+        }
+        @Override
+        public String toString() {
+            return prefix + "/" + suffix;
+        }
+    }
+
+    static final Unit NULL_UNIT = new Unit("", "");
+
+    // Note about rounding implementation
+    //
+    // The original design intended to skip rounding operation when roundingIncrement is not
+    // set. However, rounding may need to occur when fractional digits exceed the width of
+    // fractional part of pattern.
+    //
+    // DigitList class has built-in rounding mechanism, using ROUND_HALF_EVEN. This implementation
+    // forces non-null roundingIncrement if the setting is other than ROUND_HALF_EVEN, otherwise,
+    // when rounding occurs in DigitList by pattern's fractional digits' width, the result
+    // does not match the rounding mode.
+    //
+    // Ideally, all rounding operation should be done in one place like ICU4C trunk does
+    // (ICU4C rounding implementation was rewritten recently). This is intrim implemetation
+    // to fix various issues. In the future, we should entire implementation of rounding
+    // in this class, like ICU4C did.
+    //
+    // Once we fully implement rounding logic in DigitList, then following fields and methods
+    // should be gone.
+
+    private transient BigDecimal actualRoundingIncrementICU = null;
+    private transient java.math.BigDecimal actualRoundingIncrement = null;
+
+    /*
+     * The actual rounding increment as a double.
+     */
+    private transient double roundingDouble = 0.0;
+
+    /*
+     * If the roundingDouble is the reciprocal of an integer (the most common case!), this
+     * is set to be that integer.  Otherwise it is 0.0.
+     */
+    private transient double roundingDoubleReciprocal = 0.0;
+
+    /*
+     * Set roundingDouble, roundingDoubleReciprocal and actualRoundingIncrement
+     * based on rounding mode and width of fractional digits. Whenever setting affecting
+     * rounding mode, rounding increment and maximum width of fractional digits, then
+     * this method must be called.
+     *
+     * roundingIncrementICU is the field storing the custom rounding increment value,
+     * while actual rounding increment could be larger.
+     */
+    private void resetActualRounding() {
+        if (roundingIncrementICU != null) {
+            BigDecimal byWidth = getMaximumFractionDigits() > 0 ?
+                    BigDecimal.ONE.movePointLeft(getMaximumFractionDigits()) : BigDecimal.ONE;
+            if (roundingIncrementICU.compareTo(byWidth) >= 0) {
+                actualRoundingIncrementICU = roundingIncrementICU;
+            } else {
+                actualRoundingIncrementICU = byWidth.equals(BigDecimal.ONE) ? null : byWidth;
+            }
+        } else {
+            if (roundingMode == BigDecimal.ROUND_HALF_EVEN || isScientificNotation()) {
+                // This rounding fix is irrelevant if mode is ROUND_HALF_EVEN as DigitList
+                // does ROUND_HALF_EVEN for us.  This rounding fix won't work at all for
+                // scientific notation.
+                actualRoundingIncrementICU = null;
+            } else {
+                if (getMaximumFractionDigits() > 0) {
+                    actualRoundingIncrementICU = BigDecimal.ONE.movePointLeft(getMaximumFractionDigits());
+                }  else {
+                    actualRoundingIncrementICU = BigDecimal.ONE;
+                }
+            }
+        }
+
+        if (actualRoundingIncrementICU == null) {
+            setRoundingDouble(0.0d);
+            actualRoundingIncrement = null;
+        } else {
+            setRoundingDouble(actualRoundingIncrementICU.doubleValue());
+            actualRoundingIncrement = actualRoundingIncrementICU.toBigDecimal();
+        }
+    }
+
+    static final double roundingIncrementEpsilon = 0.000000001;
+
+    private void setRoundingDouble(double newValue) {
+        roundingDouble = newValue;
+        if (roundingDouble > 0.0d) {
+            double rawRoundedReciprocal = 1.0d / roundingDouble;
+            roundingDoubleReciprocal = Math.rint(rawRoundedReciprocal);
+            if (Math.abs(rawRoundedReciprocal - roundingDoubleReciprocal) > roundingIncrementEpsilon) {
+                roundingDoubleReciprocal = 0.0d;
+            }
+        } else {
+            roundingDoubleReciprocal = 0.0d;
+        }
+    }
+}
+
+// eof
index 6facd54e4bdff0223c01c4238f51327acfa487fd..ad4fa241989db4e88c806e088798d8a3df2332bb 100644 (file)
@@ -56,13 +56,13 @@ import com.ibm.icu.util.UResourceBundle;
  * <p>To format a Measure object, first create a formatter
  * object using a MeasureFormat factory method.  Then use that
  * object's format or formatMeasures methods.
- * 
+ *
  * Here is sample code:
  * <pre>
  *      MeasureFormat fmtFr = MeasureFormat.getInstance(
  *              ULocale.FRENCH, FormatWidth.SHORT);
  *      Measure measure = new Measure(23, MeasureUnit.CELSIUS);
- *      
+ *
  *      // Output: 23 °C
  *      System.out.println(fmtFr.format(measure));
  *
@@ -70,29 +70,29 @@ import com.ibm.icu.util.UResourceBundle;
  *
  *      // Output: 70 °F
  *      System.out.println(fmtFr.format(measureF));
- *     
+ *
  *      MeasureFormat fmtFrFull = MeasureFormat.getInstance(
  *              ULocale.FRENCH, FormatWidth.WIDE);
  *      // Output: 70 pieds et 5,3 pouces
  *      System.out.println(fmtFrFull.formatMeasures(
  *              new Measure(70, MeasureUnit.FOOT),
  *              new Measure(5.3, MeasureUnit.INCH)));
- *              
+ *
  *      // Output: 1 pied et 1 pouce
  *      System.out.println(fmtFrFull.formatMeasures(
  *              new Measure(1, MeasureUnit.FOOT),
  *              new Measure(1, MeasureUnit.INCH)));
- *  
+ *
  *      MeasureFormat fmtFrNarrow = MeasureFormat.getInstance(
                 ULocale.FRENCH, FormatWidth.NARROW);
  *      // Output: 1′ 1″
  *      System.out.println(fmtFrNarrow.formatMeasures(
  *              new Measure(1, MeasureUnit.FOOT),
  *              new Measure(1, MeasureUnit.INCH)));
- *      
- *      
+ *
+ *
  *      MeasureFormat fmtEn = MeasureFormat.getInstance(ULocale.ENGLISH, FormatWidth.WIDE);
- *      
+ *
  *      // Output: 1 inch, 2 feet
  *      fmtEn.formatMeasures(
  *              new Measure(1, MeasureUnit.INCH),
@@ -105,7 +105,7 @@ import com.ibm.icu.util.UResourceBundle;
  * This class is immutable and thread-safe so long as its deprecated subclass,
  * TimeUnitFormat, is never used. TimeUnitFormat is not thread-safe, and is
  * mutable. Although this class has existing subclasses, this class does not support new
- * sub-classes.   
+ * sub-classes.
  *
  * @see com.ibm.icu.text.UFormat
  * @author Alan Liu
@@ -154,7 +154,7 @@ public class MeasureFormat extends UFormat {
 
     /**
      * Formatting width enum.
-     * 
+     *
      * @stable ICU 53
      */
     // Be sure to update MeasureUnitTest.TestSerialFormatWidthEnum
@@ -163,21 +163,21 @@ public class MeasureFormat extends UFormat {
 
         /**
          * Spell out everything.
-         * 
+         *
          * @stable ICU 53
          */
-        WIDE(ListFormatter.Style.DURATION, NumberFormat.PLURALCURRENCYSTYLE), 
+        WIDE(ListFormatter.Style.DURATION, NumberFormat.PLURALCURRENCYSTYLE),
 
         /**
          * Abbreviate when possible.
-         * 
+         *
          * @stable ICU 53
          */
-        SHORT(ListFormatter.Style.DURATION_SHORT, NumberFormat.ISOCURRENCYSTYLE), 
+        SHORT(ListFormatter.Style.DURATION_SHORT, NumberFormat.ISOCURRENCYSTYLE),
 
         /**
          * Brief. Use only a symbol for the unit when possible.
-         * 
+         *
          * @stable ICU 53
          */
         NARROW(ListFormatter.Style.DURATION_NARROW, NumberFormat.CURRENCYSTYLE),
@@ -186,7 +186,7 @@ public class MeasureFormat extends UFormat {
          * Identical to NARROW except when formatMeasures is called with
          * an hour and minute; minute and second; or hour, minute, and second Measures.
          * In these cases formatMeasures formats as 5:37:23 instead of 5h, 37m, 23s.
-         * 
+         *
          * @stable ICU 53
          */
         NUMERIC(ListFormatter.Style.DURATION_NARROW, NumberFormat.CURRENCYSTYLE);
@@ -294,16 +294,16 @@ public class MeasureFormat extends UFormat {
      * If the pos argument identifies a NumberFormat field,
      * then its indices are set to the beginning and end of the first such field
      * encountered. MeasureFormat itself does not supply any fields.
-     * 
+     *
      * Calling a
      * <code>formatMeasures</code> method is preferred over calling
      * this method as they give better performance.
-     * 
+     *
      * @param obj must be a Collection&lt;? extends Measure&gt;, Measure[], or Measure object.
      * @param toAppendTo Formatted string appended here.
      * @param pos Identifies a field in the formatted text.
      * @see java.text.Format#format(java.lang.Object, java.lang.StringBuffer, java.text.FieldPosition)
-     * 
+     *
      * @stable ICU53
      */
     @Override
@@ -327,7 +327,7 @@ public class MeasureFormat extends UFormat {
         } else if (obj instanceof Measure){
             toAppendTo.append(formatMeasure((Measure) obj, numberFormat, new StringBuilder(), fpos));
         } else {
-            throw new IllegalArgumentException(obj.toString());            
+            throw new IllegalArgumentException(obj.toString());
         }
         if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) {
             pos.setBeginIndex(fpos.getBeginIndex() + prevLength);
@@ -356,7 +356,7 @@ public class MeasureFormat extends UFormat {
      * and using the appropriate Number values. Typically the units should be
      * in descending order, with all but the last Measure having integer values
      * (eg, not “3.2 feet, 2 inches”).
-     * 
+     *
      * @param measures a sequence of one or more measures.
      * @return the formatted string.
      * @stable ICU 53
@@ -375,7 +375,7 @@ public class MeasureFormat extends UFormat {
      * <br>Note: If the format doesn’t have enough decimals, or lowValue ≥ highValue,
      * the result will be a degenerate range, like “5-5 meters”.
      * <br>Currency Units are not yet supported.
-     * 
+     *
      * @param lowValue low value in range
      * @param highValue high value in range
      * @return the formatted string.
@@ -416,11 +416,11 @@ public class MeasureFormat extends UFormat {
         }
 
         final double lowDouble = lowNumber.doubleValue();
-        String keywordLow = rules.select(new PluralRules.FixedDecimal(lowDouble, 
+        String keywordLow = rules.select(new PluralRules.FixedDecimal(lowDouble,
                 lowFpos.getCountVisibleFractionDigits(), lowFpos.getFractionDigits()));
 
         final double highDouble = highNumber.doubleValue();
-        String keywordHigh = rules.select(new PluralRules.FixedDecimal(highDouble, 
+        String keywordHigh = rules.select(new PluralRules.FixedDecimal(highDouble,
                 highFpos.getCountVisibleFractionDigits(), highFpos.getFractionDigits()));
 
         final PluralRanges pluralRanges = Factory.getDefaultFactory().getPluralRanges(getLocale());
@@ -482,10 +482,10 @@ public class MeasureFormat extends UFormat {
             result.append(affix.substring(pos+replacement.length()));
         }
     }
-    
+
     /**
-     * Formats a single measure per unit. 
-     * 
+     * Formats a single measure per unit.
+     *
      * An example of such a formatted string is "3.5 meters per second."
      *
      * @param measure  the measure object. In above example, 3.5 meters.
@@ -521,11 +521,11 @@ public class MeasureFormat extends UFormat {
 
     /**
      * Formats a sequence of measures.
-     * 
+     *
      * If the fieldPosition argument identifies a NumberFormat field,
      * then its indices are set to the beginning and end of the first such field
      * encountered. MeasureFormat itself does not supply any fields.
-     * 
+     *
      * @param appendTo the formatted string appended here.
      * @param fieldPosition Identifies a field in the formatted text.
      * @param measures the measures to format.
@@ -612,8 +612,8 @@ public class MeasureFormat extends UFormat {
         }
         MeasureFormat rhs = (MeasureFormat) other;
         // A very slow but safe implementation.
-        return getWidth() == rhs.getWidth() 
-                && getLocale().equals(rhs.getLocale()) 
+        return getWidth() == rhs.getWidth()
+                && getLocale().equals(rhs.getLocale())
                 && getNumberFormat().equals(rhs.getNumberFormat());
     }
 
@@ -624,7 +624,7 @@ public class MeasureFormat extends UFormat {
     @Override
     public final int hashCode() {
         // A very slow but safe implementation.
-        return (getLocale().hashCode() * 31 
+        return (getLocale().hashCode() * 31
                 + getNumberFormat().hashCode()) * 31 + getWidth().hashCode();
     }
 
@@ -1020,7 +1020,12 @@ public class MeasureFormat extends UFormat {
         return pattern;
     }
 
-    private String getPluralFormatter(MeasureUnit unit, FormatWidth width, int index) {
+    /**
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    @Deprecated
+    public String getPluralFormatter(MeasureUnit unit, FormatWidth width, int index) {
         if (index != StandardPlural.OTHER_INDEX) {
             String pattern = getFormatterOrNull(unit, width, index);
             if (pattern != null) {
@@ -1171,6 +1176,7 @@ public class MeasureFormat extends UFormat {
                 suffix = pattern.substring(pos+3);
             }
         }
+        @Override
         public String toString() {
             return prefix + "; " + suffix;
         }
@@ -1202,7 +1208,7 @@ public class MeasureFormat extends UFormat {
             if (fieldPositionFoundIndex == -1) {
                 results[i] = formatMeasure(measures[i], nf, new StringBuilder(), fpos).toString();
                 if (fpos.getBeginIndex() != 0 || fpos.getEndIndex() != 0) {
-                    fieldPositionFoundIndex = i;    
+                    fieldPositionFoundIndex = i;
                 }
             } else {
                 results[i] = formatMeasure(measures[i], nf);
@@ -1283,7 +1289,7 @@ public class MeasureFormat extends UFormat {
         // if hour-minute-second
         if (startIndex == 0 && endIndex == 2) {
             return formatNumeric(
-                    d, 
+                    d,
                     numericFormatters.getHourMinuteSecond(),
                     DateFormat.Field.SECOND,
                     hms[endIndex],
@@ -1292,7 +1298,7 @@ public class MeasureFormat extends UFormat {
         // if minute-second
         if (startIndex == 1 && endIndex == 2) {
             return formatNumeric(
-                    d, 
+                    d,
                     numericFormatters.getMinuteSecond(),
                     DateFormat.Field.SECOND,
                     hms[endIndex],
@@ -1301,7 +1307,7 @@ public class MeasureFormat extends UFormat {
         // if hour-minute
         if (startIndex == 0 && endIndex == 1) {
             return formatNumeric(
-                    d, 
+                    d,
                     numericFormatters.getHourMinute(),
                     DateFormat.Field.MINUTE,
                     hms[endIndex],
@@ -1404,6 +1410,7 @@ public class MeasureFormat extends UFormat {
         public MeasureProxy() {
         }
 
+        @Override
         public void writeExternal(ObjectOutput out) throws IOException {
             out.writeByte(0); // version
             out.writeUTF(locale.toLanguageTag());
@@ -1413,6 +1420,7 @@ public class MeasureFormat extends UFormat {
             out.writeObject(keyValues);
         }
 
+        @Override
         @SuppressWarnings("unchecked")
         public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
             in.readByte(); // version.
index f31f5d52c029a2160d6e9e15a7d19ab426ca2359..08e5a9d2700cd1f52c01121c40aee813c6ff3969 100644 (file)
@@ -38,7 +38,7 @@ import com.ibm.icu.impl.PatternProps;
 import com.ibm.icu.impl.Utility;
 import com.ibm.icu.text.MessagePattern.ArgType;
 import com.ibm.icu.text.MessagePattern.Part;
-import com.ibm.icu.text.PluralRules.FixedDecimal;
+import com.ibm.icu.text.PluralRules.IFixedDecimal;
 import com.ibm.icu.text.PluralRules.PluralType;
 import com.ibm.icu.util.ICUUncheckedIOException;
 import com.ibm.icu.util.ULocale;
@@ -2108,7 +2108,7 @@ public class MessageFormat extends UFormat {
             assert context.number.doubleValue() == number;  // argument number minus the offset
             context.numberString = context.formatter.format(context.number);
             if(context.formatter instanceof DecimalFormat) {
-                FixedDecimal dec = ((DecimalFormat)context.formatter).getFixedDecimal(number);
+                IFixedDecimal dec = ((DecimalFormat)context.formatter).getFixedDecimal(number);
                 return rules.select(dec);
             } else {
                 return rules.select(number);
index 018d75f12b8db0b8b3bd1bc2240531dd08a74b21..74d1928a7bb4106b6355d03750300f3a73d7072e 100644 (file)
@@ -497,8 +497,13 @@ public abstract class NumberFormat extends UFormat {
     }
 
     /**
-     * {@icu} Sets whether strict parsing is in effect.  When this is true, the
-     * following conditions cause a parse failure (examples use the pattern "#,##0.#"):<ul>
+     * {@icu} Sets whether strict parsing is in effect.  When this is true, the string
+     * is required to be a stronger match to the pattern than when lenient parsing is in
+     * effect.  More specifically, the following conditions cause a parse failure relative
+     * to lenient mode (examples use the pattern "#,##0.#"):<ul>
+     * <li>The presence and position of special symbols, including currency, must match the
+     * pattern.<br>
+     * '123-' fails (the minus sign is expected in the prefix, not suffix)</li>
      * <li>Leading or doubled grouping separators<br>
      * ',123' and '1,,234" fail</li>
      * <li>Groups of incorrect length when grouping is used<br>
@@ -1113,8 +1118,13 @@ public abstract class NumberFormat extends UFormat {
     /**
      * Returns the maximum number of digits allowed in the integer portion of a
      * number.  The default value is 40, which subclasses can override.
-     * When formatting, the exact behavior when this value is exceeded is
-     * subclass-specific.  When parsing, this has no effect.
+     *
+     * When formatting, if the number of digits exceeds this value, the highest-
+     * significance digits are truncated until the limit is reached, in accordance
+     * with UTS#35.
+     *
+     * This setting has no effect on parsing.
+     *
      * @return the maximum number of integer digits
      * @see #setMaximumIntegerDigits
      * @stable ICU 2.0
@@ -1415,10 +1425,12 @@ public abstract class NumberFormat extends UFormat {
                 f.setDecimalSeparatorAlwaysShown(false);
                 f.setParseIntegerOnly(true);
             }
-
             if (choice == CASHCURRENCYSTYLE) {
                 f.setCurrencyUsage(CurrencyUsage.CASH);
             }
+            if (choice == PLURALCURRENCYSTYLE) {
+                f.setCurrencyPluralInfo(CurrencyPluralInfo.getInstance(desiredLocale));
+            }
             format = f;
        }
         // TODO: the actual locale of the *pattern* may differ from that
@@ -1449,8 +1461,11 @@ public abstract class NumberFormat extends UFormat {
      * @param choice the pattern format.
      * @return the pattern
      * @stable ICU 3.2
+     * @internal
+     * @deprecated This API is ICU internal only.
      */
-    protected static String getPattern(ULocale forLocale, int choice) {
+    @Deprecated
+    public static String getPattern(ULocale forLocale, int choice) {
         /* for ISOCURRENCYSTYLE and PLURALCURRENCYSTYLE,
          * the pattern is the same as the pattern of CURRENCYSTYLE
          * but by replacing the single currency sign with
@@ -1460,6 +1475,7 @@ public abstract class NumberFormat extends UFormat {
         switch (choice) {
         case NUMBERSTYLE:
         case INTEGERSTYLE:
+        case PLURALCURRENCYSTYLE:
             patternKey = "decimalFormat";
             break;
         case CURRENCYSTYLE:
@@ -1469,7 +1485,6 @@ public abstract class NumberFormat extends UFormat {
             break;
         case CASHCURRENCYSTYLE:
         case ISOCURRENCYSTYLE:
-        case PLURALCURRENCYSTYLE:
         case STANDARDCURRENCYSTYLE:
             patternKey = "currencyFormat";
             break;
index 0f90d00c3201781f8ba86bc1ea6b764a226cff32..c9dc1ff6ddce946b69bbc48671320a4c2efeb4cc 100644 (file)
@@ -18,6 +18,8 @@ import java.util.Map;
 
 import com.ibm.icu.impl.Utility;
 import com.ibm.icu.text.PluralRules.FixedDecimal;
+import com.ibm.icu.text.PluralRules.IFixedDecimal;
+import com.ibm.icu.text.PluralRules.Operand;
 import com.ibm.icu.text.PluralRules.PluralType;
 import com.ibm.icu.util.ULocale;
 import com.ibm.icu.util.ULocale.Category;
@@ -554,8 +556,8 @@ public class PluralFormat extends UFormat {
     private final class PluralSelectorAdapter implements PluralSelector {
         @Override
         public String select(Object context, double number) {
-            FixedDecimal dec = (FixedDecimal) context;
-            assert dec.source == (dec.isNegative ? -number : number);
+            IFixedDecimal dec = (IFixedDecimal) context;
+            assert dec.getPluralOperand(Operand.n) == Math.abs(number);
             return pluralRules.select(dec);
         }
     }
@@ -618,7 +620,7 @@ public class PluralFormat extends UFormat {
         } else {
             numberString = numberFormat.format(numberMinusOffset);
         }
-        FixedDecimal dec;
+        IFixedDecimal dec;
         if(numberFormat instanceof DecimalFormat) {
             dec = ((DecimalFormat) numberFormat).getFixedDecimal(numberMinusOffset);
         } else {
index 970f7ca04aaf22bcae5cbc4e16c7ffa832fa44e9..90a8eda81dd5e1b6093c7627ac251a7d45b75872 100644 (file)
@@ -356,7 +356,7 @@ public class PluralRules implements Serializable {
         private static final long serialVersionUID = 9163464945387899416L;
 
         @Override
-        public boolean isFulfilled(FixedDecimal n) {
+        public boolean isFulfilled(IFixedDecimal n) {
             return true;
         }
 
@@ -412,11 +412,21 @@ public class PluralRules implements Serializable {
      */
     public static final PluralRules DEFAULT = new PluralRules(new RuleList().addRule(DEFAULT_RULE));
 
-    private enum Operand {
+    /**
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    @Deprecated
+    public static enum Operand {
+        /** The double value of the entire number. */
         n,
+        /** The integer value, with the fraction digits truncated off. */
         i,
+        /** All visible fraction digits as an integer, including trailing zeros. */
         f,
+        /** Visible fraction digits, not including trailing zeros. */
         t,
+        /** Number of visible fraction digits. */
         v,
         w,
         /* deprecated */
@@ -428,7 +438,18 @@ public class PluralRules implements Serializable {
      * @deprecated This API is ICU internal only.
      */
     @Deprecated
-    public static class FixedDecimal extends Number implements Comparable<FixedDecimal> {
+    public static interface IFixedDecimal {
+        public double getPluralOperand(Operand operand);
+        public boolean isNaN();
+        public boolean isInfinite();
+    }
+
+    /**
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    @Deprecated
+    public static class FixedDecimal extends Number implements Comparable<FixedDecimal>, IFixedDecimal {
         private static final long serialVersionUID = -4756200506571685661L;
         /**
          * @internal
@@ -726,8 +747,9 @@ public class PluralRules implements Serializable {
          * @internal
          * @deprecated This API is ICU internal only.
          */
+        @Override
         @Deprecated
-        public double get(Operand operand) {
+        public double getPluralOperand(Operand operand) {
             switch(operand) {
             default: return source;
             case i: return integerValue;
@@ -881,6 +903,22 @@ public class PluralRules implements Serializable {
                 ) throws IOException, ClassNotFoundException {
             throw new NotSerializableException();
         }
+
+        /* (non-Javadoc)
+         * @see com.ibm.icu.text.PluralRules.IFixedDecimal#isNaN()
+         */
+        @Override
+        public boolean isNaN() {
+            return Double.isNaN(source);
+        }
+
+        /* (non-Javadoc)
+         * @see com.ibm.icu.text.PluralRules.IFixedDecimal#isInfinite()
+         */
+        @Override
+        public boolean isInfinite() {
+            return Double.isInfinite(source);
+        }
     }
 
     /**
@@ -1106,7 +1144,7 @@ public class PluralRules implements Serializable {
          * Returns true if the number fulfills the constraint.
          * @param n the number to test, >= 0.
          */
-        boolean isFulfilled(FixedDecimal n);
+        boolean isFulfilled(IFixedDecimal n);
 
         /*
          * Returns false if an unlimited number of values fulfills the
@@ -1463,10 +1501,10 @@ public class PluralRules implements Serializable {
         }
 
         @Override
-        public boolean isFulfilled(FixedDecimal number) {
-            double n = number.get(operand);
+        public boolean isFulfilled(IFixedDecimal number) {
+            double n = number.getPluralOperand(operand);
             if ((integersOnly && (n - (long)n) != 0.0
-                    || operand == Operand.j && number.visibleDecimalDigitCount != 0)) {
+                    || operand == Operand.j && number.getPluralOperand(Operand.v) != 0)) {
                 return !inRange;
             }
             if (mod != 0) {
@@ -1566,7 +1604,7 @@ public class PluralRules implements Serializable {
         }
 
         @Override
-        public boolean isFulfilled(FixedDecimal n) {
+        public boolean isFulfilled(IFixedDecimal n) {
             return a.isFulfilled(n)
                     && b.isFulfilled(n);
         }
@@ -1594,7 +1632,7 @@ public class PluralRules implements Serializable {
         }
 
         @Override
-        public boolean isFulfilled(FixedDecimal n) {
+        public boolean isFulfilled(IFixedDecimal n) {
             return a.isFulfilled(n)
                     || b.isFulfilled(n);
         }
@@ -1645,7 +1683,7 @@ public class PluralRules implements Serializable {
             return keyword;
         }
 
-        public boolean appliesTo(FixedDecimal n) {
+        public boolean appliesTo(IFixedDecimal n) {
             return constraint.isFulfilled(n);
         }
 
@@ -1708,7 +1746,7 @@ public class PluralRules implements Serializable {
             return this;
         }
 
-        private Rule selectRule(FixedDecimal n) {
+        private Rule selectRule(IFixedDecimal n) {
             for (Rule rule : rules) {
                 if (rule.appliesTo(n)) {
                     return rule;
@@ -1717,8 +1755,8 @@ public class PluralRules implements Serializable {
             return null;
         }
 
-        public String select(FixedDecimal n) {
-            if (Double.isInfinite(n.source) || Double.isNaN(n.source)) {
+        public String select(IFixedDecimal n) {
+            if (n.isInfinite() || n.isNaN()) {
                 return KEYWORD_OTHER;
             }
             Rule r = selectRule(n);
@@ -1780,7 +1818,7 @@ public class PluralRules implements Serializable {
             return null;
         }
 
-        public boolean select(FixedDecimal sample, String keyword) {
+        public boolean select(IFixedDecimal sample, String keyword) {
             for (Rule rule : rules) {
                 if (rule.getKeyword().equals(keyword) && rule.appliesTo(sample)) {
                     return true;
@@ -1800,9 +1838,9 @@ public class PluralRules implements Serializable {
     }
 
     @SuppressWarnings("unused")
-    private boolean addConditional(Set<FixedDecimal> toAddTo, Set<FixedDecimal> others, double trial) {
+    private boolean addConditional(Set<IFixedDecimal> toAddTo, Set<IFixedDecimal> others, double trial) {
         boolean added;
-        FixedDecimal toAdd = new FixedDecimal(trial);
+        IFixedDecimal toAdd = new FixedDecimal(trial);
         if (!toAddTo.contains(toAdd) && !others.contains(toAdd)) {
             others.add(toAdd);
             added = true;
@@ -1969,7 +2007,7 @@ public class PluralRules implements Serializable {
      * @deprecated This API is ICU internal only.
      */
     @Deprecated
-    public String select(FixedDecimal number) {
+    public String select(IFixedDecimal number) {
         return rules.select(number);
     }
 
index 9ec0ea47de48404d190d30452836a243e4d6fb3b..d54c16cd9c97cac380763e9cb8291cd6742db03a 100644 (file)
@@ -1267,7 +1267,7 @@ public class RuleBasedNumberFormat extends NumberFormat {
     public StringBuffer format(com.ibm.icu.math.BigDecimal number,
                                StringBuffer toAppendTo,
                                FieldPosition pos) {
-        if (MIN_VALUE.compareTo(number) >= 0 || MAX_VALUE.compareTo(number) <= 0) {
+        if (MIN_VALUE.compareTo(number) > 0 || MAX_VALUE.compareTo(number) < 0) {
             // We're outside of our normal range that this framework can handle.
             // The DecimalFormat will provide more accurate results.
             return getDecimalFormat().format(number, toAppendTo, pos);
index 527dd7ea934647993992073997575f810e5021ce..93bb25f8cea54c53e6eae70cd4822daeb750b04b 100644 (file)
@@ -13,6 +13,7 @@ import java.text.AttributedCharacterIterator.Attribute;
 import java.text.CharacterIterator;
 import java.util.Map;
 
+import com.ibm.icu.impl.number.Parse;
 import com.ibm.icu.lang.UCharacter;
 import com.ibm.icu.util.ULocale;
 
@@ -229,14 +230,14 @@ public final class ScientificNumberFormatter {
                     int start = iterator.getRunStart(NumberFormat.Field.EXPONENT_SIGN);
                     int limit = iterator.getRunLimit(NumberFormat.Field.EXPONENT_SIGN);
                     int aChar = char32AtAndAdvance(iterator);
-                    if (DecimalFormat.minusSigns.contains(aChar)) {
+                    if (Parse.UNISET_MINUS.contains(aChar)) {
                         append(
                                 iterator,
                                 copyFromOffset,
                                 start,
                                 result);
                         result.append(SUPERSCRIPT_MINUS_SIGN);
-                    } else if (DecimalFormat.plusSigns.contains(aChar)) {
+                    } else if (Parse.UNISET_PLUS.contains(aChar)) {
                         append(
                                 iterator,
                                 copyFromOffset,
index 06300ca3dfd02e927ff85ae2600c615c9f544dff..8437c96884cc1c67e02c68f56ea3cef8f9a14c8b 100644 (file)
@@ -674,19 +674,7 @@ public class Currency extends MeasureUnit {
      */
     @Deprecated
     public static String parse(ULocale locale, String text, int type, ParsePosition pos) {
-        List<TextTrieMap<CurrencyStringInfo>> currencyTrieVec = CURRENCY_NAME_CACHE.get(locale);
-        if (currencyTrieVec == null) {
-            TextTrieMap<CurrencyStringInfo> currencyNameTrie =
-                new TextTrieMap<CurrencyStringInfo>(true);
-            TextTrieMap<CurrencyStringInfo> currencySymbolTrie =
-                new TextTrieMap<CurrencyStringInfo>(false);
-            currencyTrieVec = new ArrayList<TextTrieMap<CurrencyStringInfo>>();
-            currencyTrieVec.add(currencySymbolTrie);
-            currencyTrieVec.add(currencyNameTrie);
-            setupCurrencyTrieVec(locale, currencyTrieVec);
-            CURRENCY_NAME_CACHE.put(locale, currencyTrieVec);
-        }
-
+        List<TextTrieMap<CurrencyStringInfo>> currencyTrieVec = getCurrencyTrieVec(locale);
         int maxLength = 0;
         String isoResult = null;
 
@@ -711,6 +699,37 @@ public class Currency extends MeasureUnit {
         return isoResult;
     }
 
+    /**
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    @Deprecated
+    public static TextTrieMap<CurrencyStringInfo>.ParseState openParseState(
+        ULocale locale, int startingCp, int type) {
+        List<TextTrieMap<CurrencyStringInfo>> currencyTrieVec = getCurrencyTrieVec(locale);
+        if (type == Currency.LONG_NAME) {
+            return currencyTrieVec.get(0).openParseState(startingCp);
+        } else {
+            return currencyTrieVec.get(1).openParseState(startingCp);
+        }
+    }
+
+    private static List<TextTrieMap<CurrencyStringInfo>> getCurrencyTrieVec(ULocale locale) {
+        List<TextTrieMap<CurrencyStringInfo>> currencyTrieVec = CURRENCY_NAME_CACHE.get(locale);
+        if (currencyTrieVec == null) {
+            TextTrieMap<CurrencyStringInfo> currencyNameTrie =
+                new TextTrieMap<CurrencyStringInfo>(true);
+            TextTrieMap<CurrencyStringInfo> currencySymbolTrie =
+                new TextTrieMap<CurrencyStringInfo>(false);
+            currencyTrieVec = new ArrayList<TextTrieMap<CurrencyStringInfo>>();
+            currencyTrieVec.add(currencySymbolTrie);
+            currencyTrieVec.add(currencyNameTrie);
+            setupCurrencyTrieVec(locale, currencyTrieVec);
+            CURRENCY_NAME_CACHE.put(locale, currencyTrieVec);
+        }
+        return currencyTrieVec;
+    }
+
     private static void setupCurrencyTrieVec(ULocale locale,
             List<TextTrieMap<CurrencyStringInfo>> trieVec) {
 
@@ -734,7 +753,12 @@ public class Currency extends MeasureUnit {
         }
     }
 
-    private static final class CurrencyStringInfo {
+    /**
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    @Deprecated
+    public static final class CurrencyStringInfo {
         private String isoCode;
         private String currencyString;
 
index f641fec1f2795ed729e576b2217a6f507ef88c11..3e90a851d67e7bdb6b5a54208a8810382599d1b8 100644 (file)
@@ -43,7 +43,7 @@ public class Measure {
      */
     public Measure(Number number, MeasureUnit unit) {
         if (number == null || unit == null) {
-            throw new NullPointerException();
+            throw new NullPointerException("Number and MeasureUnit must not be null");
         }
         this.number = number;
         this.unit = unit;
index 930b04ed34ea667d596d11c15fbdf0e6f7732996..e1a51ef5b8dd1648826c44b357647ef4d05e51dd 100644 (file)
@@ -17,8 +17,8 @@ set locale ar
 set pattern +0;-#
 begin
 format output  breaks
-6      \u200F+\u0666   JK
--6     \u200F-\u0666   JK
+6      \u061C+\u0666   JK
+-6     \u061C-\u0666   K
 
 test basic patterns
 set locale fr_FR
@@ -54,6 +54,75 @@ format       output
 12345  2345.000
 72.1234        72.1234
 
+test patterns with no '0' symbols
+set locale en_US
+begin
+pattern        format  output  breaks
+#      514.23  514
+#      0.23    0
+#      0       0
+#      1       1
+##.#   514.23  514.2
+##.#   0.23    0.2
+##.#   0       0
+##.#   1       1
+#.#    514.23  514.2
+#.#    0.23    0.2
+#.#    0       0
+#.#    1       1
+.#     514.23  514.2
+.#     0.23    .2
+.#     0       .0
+.#     1       1.0
+#.     514.23  514.
+#.     0.23    0.
+#.     0       0.
+#.     1       1.
+.      514.23  514.
+.      0.23    0.
+.      0       0.
+.      1       1.
+
+test behavior on numbers approaching zero
+set locale en
+begin
+pattern        format  output  breaks
+#.##   0.01    0.01
+#.##   0.001   0
+#.##   0       0
+#.00   0.01    .01
+#.00   0.001   .00
+#.00   0       .00
+0.00   0.01    0.01
+0.00   0.001   0.00
+0.00   0       0.00
+
+// Not in official spec, but needed for backwards compatibility
+test patterns with leading grouping separator
+set locale en_US
+begin
+pattern        format  output  breaks
+,##0   1234.56 1,235
+'#',## 3456    #34,56
+
+test patterns with valid and invalid quote marks
+set locale et
+begin
+pattern        format  output  breaks
+'#     1       fail
+''#    1       '1
+'''#   1       fail
+''''#  1       ''1
+'''''# 1       fail
+'-''-'#        1       -'-1
+// K doesn't know the locale symbol for et
+-'-'#  1       −-1   K
+'#'#   1       #1
+''#''  1       '1'
+''#-   1       '1−   K
+'-'#-  1       -1−   K
+-#'-'  1       −1-   K
+
 test int64
 set locale en
 begin
@@ -113,12 +182,36 @@ pattern   format  output  breaks
 0.05E0 12301.2 1,25E4  JK
 ##0.000#E0     0.17    170,0E-3
 // JDK doesn't support significant digits in exponents
+@@@E0  6.235   6,24E0  K
 @@@E0  6235    6,24E3  K
 @@@#E0 6200    6,20E3  K
 @@@#E0 6201    6,201E3 K
 @@@#E0 6201.7  6,202E3 K
 @@@#E00        6201.7  6,202E03        K
 @@@#E+00       6201.7  6,202E+03       K
+// If no zeros are specified, significant digits is fraction length plus 1
+#.##E0 52413   5,24E4
+###.##E0       52413   52,4E3  K
+#E0    52413   5,2413E4        K
+0E0    52413   5E4
+
+test scientific with grouping
+set locale en
+set pattern #,##0.000E0
+begin
+format output  breaks
+// J throws an IllegalArgumentException when parsing the pattern.
+1      1.000E0 J
+11     11.00E0 J
+111    111.0E0 J
+// K doesn't print the grouping separator ("1111E0")
+1111   1,111E0 JK
+// K prints too many digits ("1.1111E4")
+11111  1.111E4 JK
+111111 11.11E4 JK
+1111111        111.1E4 JK
+11111111       1,111E4 JK
+111111111      1.111E8 JK
 
 test percents
 set locale fr
@@ -165,7 +258,7 @@ $**####,##0 1234    $***1\u00a0234  K
 // In J ICU adds padding as if 'EUR' is only 2 chars (2 * 0xa4)
 \u00a4\u00a4 **####0.00        433.0   EUR *433,00     JK
 // In J ICU adds padding as if 'EUR' is only 2 chars (2 * 0xa4)
-\u00a4\u00a4 **#######0        433.0   EUR *433,00     JK
+\u00a4\u00a4 **#######0        433.0   EUR ****433     JK
 
 test padding and currencies
 begin
@@ -235,13 +328,16 @@ set pattern #E0
 set format 299792458.0
 begin
 minIntegerDigits       maxIntegerDigits        minFractionDigits       maxFractionDigits       output  breaks
+// JDK gives 2.99792458E8 (maxInt + maxFrac instead of minInt + maxFrac)
+1      1000    0       5       2.99792E8       K
 // JDK gives .3E9 instead of unlimited precision.
 0      1       0       0       2.99792458E8    K
 1      1       0       0       3E8
 // JDK gives E0 instead of allowing for unlimited precision
-0      0       0       0       2.99792458E8    K
-// JDK gives .299792E9
-0      1       0       5       2.9979E8        K
+// S obeys the maximum integer digits and returns .299792458E9
+0      0       0       0       2.99792458E8    KS
+// JDK and S give .299792E9
+0      1       0       5       2.9979E8        KS
 // JDK gives 300E6     
 0      3       0       0       299.792458E6    K
 // JDK gives 299.8E6 (maybe maxInt + maxFrac instead of minInt + maxFrac)?
@@ -256,11 +352,14 @@ minIntegerDigits  maxIntegerDigits        minFractionDigits       maxFractionDigits       output  bre
 4      4       0       0       2998E5
 0      0       1       5       .29979E9
 // JDK gives E0
-0      0       1       0       2.99792458E8    K
+// S obeys the maximum integer digits
+0      0       1       0       2.99792458E8    KS
 // JDK gives .2998E9
-0      0       0       4       2.998E8 K
+0      0       0       4       2.998E8 KS
+// S correctly formats this as 29.979246E7.
 // JDK uses 8 + 6 for significant digits instead of 2 + 6
-2      8       1       6       2.9979246E8     K
+// J and C return 2.9979246E8.
+2      8       1       6       29.979246E7     CJK
 // Treat max int digits > 8 as being the same as min int digits.
 // This behavior is not spelled out in the specification.
 // JDK fails here because it tries to use 9 + 6 = 15 sig digits.
@@ -290,7 +389,8 @@ set format 29979245.0
 begin
 minIntegerDigits       maxIntegerDigits        minFractionDigits       maxFractionDigits       output  breaks
 // JDK gives E0
-0      0       0       0       2.9979245E7     K
+// S obeys the max integer digits and prints 0.299...
+0      0       0       0       2.9979245E7     KS
 // JDK gives .3E8
 0      1       0       0       2.9979245E7     K
 // JDK gives 2998E4.
@@ -300,23 +400,27 @@ test ticket 11524
 set locale en
 set pattern #,##0.###
 begin
-format maxIntegerDigits        output
-123    1       3
-123    -2147483648     0
-12345  1       5
-12345  -2147483648     0
-5.3    1       5.3
-5.3    -2147483648     .3
+format maxIntegerDigits        output  breaks
+123    1       3       
+0      0       0
+// S ignores max integer if it is less than zero and prints "123"
+123    -2147483648     0       S
+12345  1       5       
+12345  -2147483648     0       S
+5.3    1       5.3     
+5.3    -2147483648     .3      S
 
 test patterns with zero
 set locale en
 set format 0
 begin
-pattern        output
+pattern        output  breaks
 #.#    0
 #.     0.
 .#     .0
 #      0
+#,##0.00       0.00
+#,###.00       .00
 00.000E00      00.000E00
 0.####E0       0E0
 ##0.######E000 0E000
@@ -334,8 +438,8 @@ format      output  breaks
 0.001234       0.001234        K
 0.0012345      0.0012345       K
 0.00123456     0.0012346       K
--43    -43.0   K
--43.7  -43.7   K
+-43    -43.0
+-43.7  -43.7
 -43.76 -43.76  K
 -43.762        -43.762 K
 -43.7626       -43.763 K
@@ -360,7 +464,7 @@ output      grouping        breaks  grouping2       minGroupingDigits
 1,2345,6789    4
 1,23,45,6789   4       K       2
 1,23,45,6789   4       K       2       2
-123,456789     6       K       6       3
+123,456789     6               6       3
 123456789      6       JK      6       4
 
 test multiplier setters
@@ -370,7 +474,7 @@ format      multiplier      output  breaks
 23     -12     -276
 23     -1      -23
 // ICU4J and JDK throw exception on zero multiplier. ICU4C does not.
-23     0       23      JK
+23     0       23      JKS
 23     1       23
 23     12      276
 -23    12      -276
@@ -394,7 +498,7 @@ set pattern bill0
 set format 1357
 begin
 padCharacter   formatWidth     output  breaks
-*      8       bill1357        K
+*      8       bill1357
 *      9       *bill1357       K
 ^      10      ^^bill1357      K
 
@@ -406,7 +510,7 @@ begin
 output breaks  useScientific
 186283.00
 1.86E5 K       1
-186283.00      K       0
+186283.00              0
 
 test rounding mode setters
 set locale en_US
@@ -423,19 +527,43 @@ format    roundingMode    output  breaks
 -1.49  down    -1      K
 1.01   up      1.5     K
 1.49   down    1       K
--1.01  ceiling -1      K
--1.49  floor   -1.5    K
+-1.01  ceiling -1
+-1.49  floor   -1.5
 
 test currency usage setters
-// TODO: Find a country and currency where standard and cash differ
 set locale CH
-set currency CHF
 set pattern \u00a4\u00a4 0
 begin
-format currencyUsage   output  breaks
-0.37   standard        CHF 0.37        K
-// TODO: Get the rounding data into ICU4C and ICU4J
-0.37   cash    CHF 0.35        CJK
+format currency        currencyUsage   output  breaks
+0.37   CHF     standard        CHF 0.37        K
+0.37   CHF     cash    CHF 0.35        CK
+1.234  CZK     standard        CZK 1.23        K
+1.234  CZK     cash    CZK 1
+
+test currency usage to pattern
+set locale en
+begin
+currency       currencyUsage   toPattern       breaks
+// These work in J, but it prepends an extra hash sign to the pattern.
+// K does not support this feature.
+USD    standard        0.00    JK
+CHF    standard        0.00    JK
+CZK    standard        0.00    JK
+USD    cash    0.00    JK
+CHF    cash    0.05    JK
+CZK    cash    0       JK
+
+test currency rounding
+set locale en
+set currency USD
+begin
+pattern        format  output  breaks
+#      123     123     S
+// Currency rounding should always override the pattern.
+// K prints the currency in ISO format for some reason.
+\u00a4#        123     $123.00 K
+\u00a4#.000    123     $123.00 K
+\u00a4#.##     123     $123.00 K
 
 test exponent parameter setters
 set locale en_US
@@ -445,12 +573,10 @@ begin
 decimalSeparatorAlwaysShown    exponentSignAlwaysShown minimumExponentDigits   output  breaks
 0      0       2       3E08    K
 0      1       3       3E+008  K
-// ICU DecimalFormat J does not honor decimalSeparatorAlwaysShown
-// for scientific notation. But JDK DecimalFormat does honor
 // decimalSeparatorAlwaysShown K=JDK; C=ICU4C; J=ICU4J
 // See ticket 11621
-1      0       2       3.E08   JK
-1      1       3       3.E+008 JK
+1      0       2       3.E08   K
+1      1       3       3.E+008 K
 1      0       1       3.E8
 0      0       1       3E8
 
@@ -462,7 +588,7 @@ format      output  breaks  decimalSeparatorAlwaysShown
 // decimalSeparatorAlwaysShown off by default
 299792458      3E8
 299000000      2.99E8
-299792458      3.E8    J       1
+299792458      3.E8            1
 
 test pad position setters
 set locale en_US
@@ -505,7 +631,7 @@ set locale en_US
 set pattern [0.00];(#)
 begin
 format output  breaks
-Inf    [\u221e]        K
+Inf    [\u221e]
 -Inf   (\u221e)        K
 NaN    NaN     K
 
@@ -539,36 +665,38 @@ begin
 locale pattern format  output  breaks
 en     #0%     0.4376  44%
 // This next test breaks JDK. JDK doesn't multiply by 100.
-// It also is now broken in ICU4J until #10368 is fixed.
-fa     \u0025\u00a0\u0023\u0030        0.4376  \u200e\u066a\u00a0\u06f4\u06f4  JK
+fa     \u0025\u00a0\u0023\u0030        0.4376  \u200e\u066a\u00a0\u06f4\u06f4  K
 
 test toPattern
 set locale en
 begin
 pattern        toPattern       breaks
+// All of the "S" failures in this section are because of functionally equivalent patterns
 // JDK doesn't support any patterns with padding or both negative prefix and suffix
 // Breaks ICU4J See ticket 11671
 **0,000        **0,000         JK
 **##0,000      **##0,000       K
 **###0,000     **###0,000      K
-**####0,000    **#,##0,000     K
+**####0,000    **#,##0,000     KS
 ###,000.       #,000.
-0,000  #0,000
+0,000  #0,000  S
 .00    #.00
-000    #000
-000,000        #,000,000
+000    #000    S
+000,000        #,000,000       S
 pp#,000        pp#,000
-00.##  #00.##
+00.##  #00.##  S
 #,#00.025      #,#00.025
 // No secondary grouping in JDK
 #,##,###.02500 #,##,###.02500  K
 pp#,000;(#)    pp#,000;(#,000) K
-**####,##,##0.0##;(#)  **#,##,##,##0.0##;**(##,##,##0.0##)     K
+**####,##,##0.0##;(#)  **#,##,##,##0.0##;**(##,##,##0.0##)     KS
 // No significant digits in JDK
 @@###  @@###   K
 @,@#,###       @,@#,###        K
 0.00E0 0.00E0
-@@@##E0        @@@##E0 K
+// The following one works in JDK, probably because
+// it just returns the same string
+@@@##E0        @@@##E0
 ###0.00#E0     ###0.00#E0
 ##00.00#E0     ##00.00#E0
 0.00E+00       0.00E+00        K
@@ -589,7 +717,8 @@ parse       output  breaks
 // J requires prefix and suffix for lenient parsing, but C doesn't
 5,347.25       5347.25 JK
 (5,347.25      -5347.25        J
--5,347.25      fail
+// S is successful at parsing this as -5347.25 in lenient mode
+-5,347.25      fail    S
 +3.52E4        35200
 (34.8E-3)      -0.0348
 // JDK stops parsing at the spaces. JDK doesn't see space as a grouping separator
@@ -598,7 +727,7 @@ parse       output  breaks
 // J doesn't allow trailing separators before E but C does
 (34,,25,E-1)   -342.5  J
 (34  25 E-1)   -342.5  JK
-(34,,25 E-1)   -3425   J
+(34,,25 E-1)   -342.5  JK
 // Spaces are not allowed after exponent symbol
 // C parses up to the E but J bails
 (34  25E -1)   -3425   JK
@@ -648,16 +777,16 @@ set locale en
 set pattern #,##0.0###+;#-
 begin
 parse  output  breaks
-// C sees this as -3426, don't understand why
-3426   -3426   JK
+// C sees this as -3426, don't understand why.
+// J and K just bail.
+3426   3426    JKC
 3426+  3426
-// J bails, but JDK will parse up to the space and get 34.
-// C sees -34
-34 d1+ -34     JK
+// J bails; C and K see -34
+34 d1+ 34      JKC
 // JDK sees this as -1234 for some reason
 // J bails b/c of trailing separators
 // C parses until trailing separators, but sees -1234
-1,234,,,+      -1234   JK
+1,234,,,+      1234    JKC
 1,234- -1234
 // J bails because of trailing separators
 1,234,-        -1234   J
@@ -668,55 +797,70 @@ parse     output  breaks
 
 test parse strict
 set locale en
-set pattern +#,##0.0###;(#)
+set pattern +#,##,##0.0###;(#)
 set lenient 0
+set minGroupingDigits 2
 begin
 parse  output  breaks
 +123d5 123
 +5347.25       5347.25
 // separators in wrong place cause failure, no separators ok.
-+5,347.25      5347.25
-(5347.25)      -5347.25
-(5,347.25)     -5347.25
++65,347.25     65347.25
+(65347.25)     -65347.25
+(65,347.25)    -65347.25
 // JDK does allow separators in the wrong place and parses as -5347.25
 (53,47.25)     fail    K
 // strict requires prefix or suffix
-5,347.25       fail
+65,347.25      fail
 +3.52E4        35200
 (34.8E-3)      -0.0348
 (3425E-1)      -342.5
 // Strict doesn't allow separators in sci notation.
-(3,425)        -3425
-// JDK allows separators in sci notation and parses as -342.5
-(3,425E-1)     fail    K
+(63,425)       -63425
+// JDK and S allow separators in sci notation and parses as -342.5
+(63,425E-1)    fail    KS
 // Both prefix and suffix needed for strict.
 // JDK accepts this and parses as -342.5
 (3425E-1       fail    K
 +3.52EE4       3.52
-+1,234,567.8901        1234567.8901
++12,34,567.8901        1234567.8901
 // With strict digit separators don't have to be the right type
 // JDK doesn't acknowledge space as a separator
-+1 234 567.8901        1234567.8901    K
++134 567.8901        1234567.8901    K
 // In general the grouping separators have to match their expected
 // location exactly. The only exception is when string being parsed
 // have no separators at all.
-+1,234,567.8901        1234567.8901
-// JDK doesn't require separators to be in the right place
++12,345.67     12345.67
+// JDK doesn't require separators to be in the right place.
 +1,23,4567.8901        fail    K
++1,234,567.8901        fail    K
 +1234,567.8901 fail    K
 +1,234567.8901 fail    K
 +1234567.8901  1234567.8901
+// Minimum grouping is not satisfied below, but that's ok
+// because minimum grouping is optional.
++1,234.5       1234.5
 // Comma after decimal means parse to a comma
-+123,456.78,9  123456.78
-// A decimal after a decimal means bail
-// JDK parses as 123456.78
-+123,456.78.9  fail    K
++1,23,456.78,9 123456.78
+// J fails upon seeing the second decimal point
++1,23,456.78.9 123456.78       J
 +79    79
 +79    79
 + 79   fail
 // JDK parses as -1945
 (1,945d1)      fail    K
 
+test parse strict without prefix/suffix
+set locale en
+set pattern #
+set lenient 0
+begin
+parse  output  breaks
+12.34  12.34
+-12.34 -12.34
++12.34 12.34   JK
+$12.34 fail
+
 test parse integer only
 set locale en
 set pattern 0.00
@@ -724,7 +868,8 @@ set parseIntegerOnly 1
 begin
 parse  output  breaks
 35     35
-+35    fail
+// S accepts leading plus signs
++35    35      CJK
 -35    -35
 2.63   2
 -39.99 -39
@@ -746,8 +891,8 @@ set pattern 0
 set locale en
 begin
 parse  output  outputCurrency  breaks
-// See ticket 11735
-53.45  fail    USD     J
+// Fixed in ticket 11735
+53.45  fail    USD
 
 test parse strange prefix
 set locale en
@@ -775,12 +920,13 @@ set negativePrefix
 set negativeSuffix 9N
 begin
 parse  output  breaks
+// S is the only implementation that passes these cases.
 // C consumes the '9' as a digit and assumes number is negative
 // J and JDK bail
-// 6549K       654     CJK
+6549K  654     CJK
 // C consumes the '9' as a digit and assumes number is negative
 // J and JDK bail
-// 6549N       -654    CJK
+6549N  -654    CJK
 
 test really strange prefix
 set locale en
@@ -791,6 +937,39 @@ parse      output
 8245   45
 2845   -45
 
+test parse pattern with quotes
+set locale en
+set pattern '-'#y
+begin
+parse  output
+-45y   45
+
+test parse with locale symbols
+// The grouping separator in it_CH is an apostrophe
+set locale it_CH
+set pattern #
+begin
+parse  output  breaks
+१३ 13      
+१३.३१‍       13.31   
+// J and K stop parsing at the apostrophe
+123'456        123456  JK
+524'1.3        5241.3  JK
+३'१‍     31      JK
+
+test parse with European-style comma/period
+set locale pt
+set pattern #
+begin
+parse  output  breaks
+// J and K get 123
+123.456        123456  JK
+123,456        123.456
+987,654.321    987.654
+987,654 321    987.654
+// J and K get 987
+987.654,321    987654.321      JK
+
 test select
 set locale sr
 begin
@@ -811,25 +990,28 @@ NaN       0.0     other
 
 test parse currency ISO
 set pattern 0.00 \u00a4\u00a4;(#) \u00a4\u00a4
-set locale en_US
+set locale en_GB
 begin
 parse  output  outputCurrency  breaks
-$53.45 53.45   USD
+53.45  fail    GBP
+£53.45        53.45   GBP
+$53.45 fail    USD
 53.45 USD      53.45   USD
+53.45 GBP      53.45   GBP
 USD 53.45      53.45   USD     J
-53.45USD       fail    USD
+53.45USD       53.45   USD     CJ
 USD53.45       53.45   USD
 (7.92) USD     -7.92   USD
-(7.92) EUR     -7.92   EUR
+(7.92) GBP     -7.92   GBP
 (7.926) USD    -7.926  USD
-(7.926 USD)    fail    USD
-(USD 7.926)    fail    USD
-USD (7.926)    fail    USD
-USD (7.92)     fail    USD
-(7.92)USD      fail    USD
-USD(7.92)      fail    USD
+(7.926 USD)    -7.926  USD     CJ
+(USD 7.926)    -7.926  USD     CJ
+USD (7.926)    -7.926  USD     CJ
+USD (7.92)     -7.92   USD     CJ
+(7.92)USD      -7.92   USD     CJ
+USD(7.92)      -7.92   USD     CJ
 (8) USD        -8      USD
--8 USD fail    USD
+-8 USD -8      USD     CJ
 67 USD 67      USD
 53.45$ fail    USD
 US Dollars 53.45       53.45   USD     J
@@ -837,37 +1019,41 @@ US Dollars 53.45 53.45   USD     J
 US Dollar 53.45        53.45   USD     J
 53.45 US Dollar        53.45   USD
 US Dollars53.45        53.45   USD
-53.45US Dollars        fail    USD
+53.45US Dollars        53.45   USD     CJ
 US Dollar53.45 53.45   USD
 US Dollat53.45 fail    USD
-53.45US Dollar fail    USD
-US Dollars (53.45)     fail    USD
+53.45US Dollar 53.45   USD     CJ
+US Dollars (53.45)     -53.45  USD     CJ
 (53.45) US Dollars     -53.45  USD
-US Dollar (53.45)      fail    USD
+(53.45) Euros  -53.45  EUR
+US Dollar (53.45)      -53.45  USD     CJ
 (53.45) US Dollar      -53.45  USD
-US Dollars(53.45)      fail    USD
-(53.45)US Dollars      fail    USD
-US Dollar(53.45)       fail    USD
+US Dollars(53.45)      -53.45  USD     CJ
+(53.45)US Dollars      -53.45  USD     CJ
+US Dollar(53.45)       -53.45  USD     CJ
 US Dollat(53.45)       fail    USD
-(53.45)US Dollar       fail    USD
+(53.45)US Dollar       -53.45  USD     CJ
 
 
 test parse currency ISO negative
 set pattern 0.00 \u00a4\u00a4;-# \u00a4\u00a4
-set locale en_US
+set locale en_GB
 begin
 parse  output  outputCurrency  breaks
-$53.45 53.45   USD
+53.45  fail    GBP
+£53.45        53.45   GBP
+$53.45 fail    USD
 53.45 USD      53.45   USD
+53.45 GBP      53.45   GBP
 USD 53.45      53.45   USD     J
-53.45USD       fail    USD
+53.45USD       53.45   USD     CJ
 USD53.45       53.45   USD
 -7.92 USD      -7.92   USD
--7.92 EUR      -7.92   EUR
+-7.92 GBP      -7.92   GBP
 -7.926 USD     -7.926  USD
-USD -7.926     fail    USD
--7.92USD       fail    USD
-USD-7.92       fail    USD
+USD -7.926     -7.926  USD     CJ
+-7.92USD       -7.92   USD     CJ
+USD-7.92       -7.92   USD     CJ
 -8 USD -8      USD
 67 USD 67      USD
 53.45$ fail    USD
@@ -876,70 +1062,75 @@ US Dollars 53.45 53.45   USD     J
 US Dollar 53.45        53.45   USD     J
 53.45 US Dollar        53.45   USD
 US Dollars53.45        53.45   USD
-53.45US Dollars        fail    USD
+53.45US Dollars        53.45   USD     CJ
 US Dollar53.45 53.45   USD
 US Dollat53.45 fail    USD
-53.45US Dollar fail    USD
+53.45US Dollar 53.45   USD     CJ
 
 
 test parse currency long
 set pattern 0.00 \u00a4\u00a4\u00a4;(#) \u00a4\u00a4\u00a4
-set locale en_US
+set locale en_GB
 begin
 parse  output  outputCurrency  breaks
-$53.45 53.45   USD
+// J throws a NullPointerException on the first case
+53.45  fail    GBP
+£53.45        53.45   GBP
+$53.45 fail    USD
 53.45 USD      53.45   USD
+53.45 GBP      53.45   GBP
 USD 53.45      53.45   USD     J
-// See ticket 11735
-53.45USD       fail    USD     J
+53.45USD       53.45   USD     CJ
 USD53.45       53.45   USD
 (7.92) USD     -7.92   USD
+(7.92) GBP     -7.92   GBP
 (7.926) USD    -7.926  USD
-(7.926 USD)    fail    USD
-(USD 7.926)    fail    USD
-USD (7.926)    fail    USD
-USD (7.92)     fail    USD
-(7.92)USD      fail    USD
-USD(7.92)      fail    USD
+(7.926 USD)    -7.926  USD     CJ
+(USD 7.926)    -7.926  USD     CJ
+USD (7.926)    -7.926  USD     CJ
+USD (7.92)     -7.92   USD     CJ
+(7.92)USD      -7.92   USD     CJ
+USD(7.92)      -7.92   USD     CJ
 (8) USD        -8      USD
-// See ticket 11735
--8 USD fail    USD     J
+-8 USD -8      USD     CJ
 67 USD 67      USD
-// See ticket 11735
-53.45$ fail    USD     J
+// J throws a NullPointerException on the next case
+53.45$ fail    USD
 US Dollars 53.45       53.45   USD     J
 53.45 US Dollars       53.45   USD
 US Dollar 53.45        53.45   USD     J
 53.45 US Dollar        53.45   USD
 US Dollars53.45        53.45   USD
-// See ticket 11735
-53.45US Dollars        fail    USD     J
+53.45US Dollars        53.45   USD     CJ
 US Dollar53.45 53.45   USD
 US Dollat53.45 fail    USD
-// See ticket 11735
-53.45US Dollar fail    USD     J
+53.45US Dollar 53.45   USD     CJ
 
 
 test parse currency short
 set pattern 0.00 \u00a4;(#) \u00a4
-set locale en_US
+set locale en_GB
 begin
 parse  output  outputCurrency  breaks
-$53.45 53.45   USD
+53.45  fail    GBP
+£53.45        53.45   GBP
+$53.45 fail    USD
 53.45 USD      53.45   USD
+53.45 GBP      53.45   GBP
 USD 53.45      53.45   USD     J
-53.45USD       fail    USD
+53.45USD       53.45   USD     CJ
 USD53.45       53.45   USD
 (7.92) USD     -7.92   USD
+(7.92) GBP     -7.92   GBP
 (7.926) USD    -7.926  USD
-(7.926 USD)    fail    USD
-(USD 7.926)    fail    USD
-USD (7.926)    fail    USD
-USD (7.92)     fail    USD
-(7.92)USD      fail    USD
-USD(7.92)      fail    USD
+(7.926 USD)    -7.926  USD     CJ
+(USD 7.926)    -7.926  USD     CJ
+USD (7.926)    -7.926  USD     CJ
+USD (7.92)     -7.92   USD     CJ
+(7.92)USD      -7.92   USD     CJ
+USD(7.92)      -7.92   USD     CJ
 (8) USD        -8      USD
--8 USD fail    USD
+-8 USD -8      USD     CJ
 67 USD 67      USD
 53.45$ fail    USD
 US Dollars 53.45       53.45   USD     J
@@ -947,45 +1138,51 @@ US Dollars 53.45 53.45   USD     J
 US Dollar 53.45        53.45   USD     J
 53.45 US Dollar        53.45   USD
 US Dollars53.45        53.45   USD
-53.45US Dollars        fail    USD
+53.45US Dollars        53.45   USD     CJ
 US Dollar53.45 53.45   USD
 US Dollat53.45 fail    USD
-53.45US Dollar fail    USD
+53.45US Dollar 53.45   USD     CJ
 
 
 test parse currency short prefix
 set pattern \u00a40.00;(\u00a4#)
-set locale en_US
+set locale en_GB
 begin
 parse  output  outputCurrency  breaks
-$53.45 53.45   USD
-53.45 USD      fail    USD
+53.45  fail    GBP
+£53.45        53.45   GBP
+$53.45 fail    USD
+53.45 USD      53.45   USD     CJ
+53.45 GBP      53.45   GBP     CJ
 USD 53.45      53.45   USD     J
-53.45USD       fail    USD
+53.45USD       53.45   USD     CJ
 USD53.45       53.45   USD
-(7.92) USD     fail    USD
-(7.926) USD    fail    USD
-(7.926 USD)    fail    USD
+// S fails these because '(' is an incomplete prefix.
+(7.92) USD     -7.92   USD     CJS
+(7.92) GBP     -7.92   GBP     CJS
+(7.926) USD    -7.926  USD     CJS
+(7.926 USD)    -7.926  USD     CJS
 (USD 7.926)    -7.926  USD     J
-USD (7.926)    fail    USD
-USD (7.92)     fail    USD
-(7.92)USD      fail    USD
-USD(7.92)      fail    USD
-(8) USD        fail    USD
--8 USD fail    USD
-67 USD fail    USD
+USD (7.926)    -7.926  USD     CJS
+USD (7.92)     -7.92   USD     CJS
+(7.92)USD      -7.92   USD     CJS
+USD(7.92)      -7.92   USD     CJS
+(8) USD        -8      USD     CJS
+-8 USD -8      USD     CJ
+67 USD 67      USD     CJ
 53.45$ fail    USD
 US Dollars 53.45       53.45   USD     J
 53.45 US Dollars       53.45   USD
 US Dollar 53.45        53.45   USD     J
 53.45 US Dollar        53.45   USD
 US Dollars53.45        53.45   USD
-53.45US Dollars        fail    USD
+53.45US Dollars        53.45   USD     CJ
 US Dollar53.45 53.45   USD
-53.45US Dollar fail    USD
+53.45US Dollar 53.45   USD     CJ
 
 test format foreign currency
 set locale fa_IR
+set currency IRR
 begin
 pattern        format  output  breaks
 \u00a4\u00a4\u00a4 0.00;\u00a4\u00a4\u00a4 #   1235    \u0631\u06cc\u0627\u0644 \u0627\u06cc\u0631\u0627\u0646 \u06F1\u06F2\u06F3\u06F5        K
@@ -1058,6 +1255,22 @@ EUR 7.82 7.82    EUR
 Euro 7.82      7.82    EUR
 Euros 7.82     7.82    EUR
 
+test parse currency without currency mode
+// Should accept a symbol associated with the currency specified by the API,
+// but should not traverse the full currency data.
+set locale en_US
+set pattern \u00a4#,##0.00
+begin
+parse  currency        output  breaks
+$52.41 USD     52.41
+USD52.41       USD     52.41   K
+\u20ac52.41    USD     fail
+EUR52.41       USD     fail
+$52.41 EUR     fail
+USD52.41       EUR     fail
+\u20ac52.41    EUR     52.41   K
+EUR52.41       EUR     52.41
+
 test parse currency ISO strict
 set pattern 0.00 \u00a4\u00a4;(#) \u00a4\u00a4
 set locale en_US
@@ -1110,3 +1323,107 @@ begin
 format output  breaks
 -0.99  -0      JK
 
+test parse decimalPatternMatchRequired
+set locale en
+set decimalPatternMatchRequired 1
+begin
+pattern        parse   output  breaks
+// K doesn't support this feature.
+0      123     123
+0      123.    fail    JK
+0      1.23    fail    JK
+0      -513    -513
+0      -513.   fail    JK
+0      -5.13   fail    JK
+0.0    123     fail    K
+0.0    123.    123
+0.0    1.23    1.23
+0.0    -513    fail    K
+0.0    -513.   -513
+0.0    -5.13   -5.13
+
+test parse minus sign
+set locale en
+set pattern #
+begin
+parse  output  breaks
+-123   -123
+- 123  -123    JK
+ -123  -123    JK
+ - 123 -123    JK
+123-   -123    JK
+123 -  -123    JK
+
+test parse case sensitive
+set locale en
+set lenient 1
+set pattern Aa#
+begin
+parse  parseCaseSensitive      output  breaks
+Aa1.23 1       1.23
+Aa1.23 0       1.23
+AA1.23 1       fail
+// J and K do not support case-insensitive parsing for prefix/suffix.
+// J supports it for the exponent separator, but not K.
+AA1.23 0       1.23    JK
+aa1.23 1       fail
+aa1.23 0       1.23    JK
+Aa1.23E3       1       1230
+Aa1.23E3       0       1230
+Aa1.23e3       1       1.23    J
+Aa1.23e3       0       1230    K
+NaN    1       NaN     K
+NaN    0       NaN     K
+nan    1       fail
+nan    0       NaN     JK
+
+test parse infinity and scientific notation overflow
+set locale en
+begin
+parse  output  breaks
+NaN    NaN     K
+// JDK returns zero
+1E999999999999999      Inf     K
+-1E999999999999999     -Inf    K
+1E-99999999999999      0.0
+// Note: The test suite code doesn't properly check for 0.0 vs. -0.0
+-1E-99999999999999     -0.0
+1E2147483648   Inf     K
+1E2147483647   Inf     K
+1E2147483646   1E2147483646
+1E-2147483649  0
+1E-2147483648  0
+// S returns zero here
+1E-2147483647  1E-2147483647   S
+1E-2147483646  1E-2147483646
+
+test format push limits
+set locale en
+set minFractionDigits 2
+set roundingMode halfDown
+begin
+maxFractionDigits      format  output  breaks
+100    987654321987654321      987654321987654321.00
+100    987654321.987654321     987654321.987654321
+100    9999999999999.9950000000001     9999999999999.9950000000001
+2      9999999999999.9950000000001     10000000000000.00
+2      9999999.99499999        9999999.99
+// K doesn't support halfDowm rounding mode?
+2      9999999.995     9999999.99      K
+2      9999999.99500001        10000000.00
+100    56565656565656565656565656565656565656565656565656565656565656  56565656565656565656565656565656565656565656565656565656565656.00
+100    454545454545454545454545454545.454545454545454545454545454545   454545454545454545454545454545.454545454545454545454545454545
+100    0.0000000000000000000123        0.0000000000000000000123
+100    -78787878787878787878787878787878       -78787878787878787878787878787878.00
+100    -8989898989898989898989.8989898989898989        -8989898989898989898989.8989898989898989
+
+test ticket 11230
+set locale en
+set pattern ###
+begin
+parse  output  breaks
+// K and J return null; S returns 99
+ 9 9   9       JKS
+// K and J return null
+ 9 999 9999    JK
+
index 641dc7f315ab96caff951c8e2df6cfeb9d4999ff..86946deea8a354df8051589f4c0deae8ce57e1f1 100644 (file)
@@ -48,11 +48,11 @@ public class BigNumberFormatTest extends TestFmwk {
         DecimalFormatSymbols US = new DecimalFormatSymbols(Locale.US);
         DecimalFormat f = new DecimalFormat("#,##,###", US);
         expect(f, new Long(123456789), "12,34,56,789");
-        expectPat(f, "#,##,###");
+        expectPat(f, "#,##,##0");
         f.applyPattern("#,###");
         f.setSecondaryGroupingSize(4);
         expect(f, new Long(123456789), "12,3456,789");
-        expectPat(f, "#,####,###");
+        expectPat(f, "#,####,##0");
 
         // On Sun JDK 1.2-1.3, the hi_IN locale uses '0' for a zero digit,
         // but on IBM JDK 1.2-1.3, the locale uses U+0966.
@@ -144,7 +144,7 @@ public class BigNumberFormatTest extends TestFmwk {
 
         fmt.setFormatWidth(16);
         //              12  34567890123456
-        expectPat(fmt, "AA*^#,###,##0.00ZZ");
+        expectPat(fmt, "AA*^#####,##0.00ZZ");
     }
 
     private void expectPat(DecimalFormat fmt, String exp) {
@@ -227,16 +227,16 @@ public class BigNumberFormatTest extends TestFmwk {
         expect(new DecimalFormat[] { new DecimalFormat("#E0", US),
                                      new DecimalFormat("##E0", US),
                                      new DecimalFormat("####E0", US),
-                                     new DecimalFormat("0E0", US),    
-                                     new DecimalFormat("00E0", US),   
-                                     new DecimalFormat("000E0", US), 
+                                     new DecimalFormat("0E0", US),
+                                     new DecimalFormat("00E0", US),
+                                     new DecimalFormat("000E0", US),
                                    },
                new Long(45678000),
                new String[] { "4.5678E7",
                               "45.678E6",
                               "4567.8E4",
                               "5E7",
-                              "46E6",  
+                              "46E6",
                               "457E5",
                             }
                );
@@ -285,13 +285,13 @@ public class BigNumberFormatTest extends TestFmwk {
                    new Long(-1000000000), "(1,000,000,000.00)",
                });
     }
-    
+
     private void expect(NumberFormat fmt, Object[] data) {
         for (int i=0; i<data.length; i+=2) {
             expect(fmt, (Number) data[i], (String) data[i+1]);
         }
     }
-    
+
     private void expect(Object fmto, Object numo, Object expo) {
         NumberFormat fmt = null, fmts[] = null;
         Number num = null, nums[] = null;
@@ -400,7 +400,7 @@ public class BigNumberFormatTest extends TestFmwk {
         if (!s.equals("-0.1")) {
             errln("FAIL");
         }
-    }        
+    }
 
     @Test
     public void TestBigDecimalJ28() {
index 4c82e878d4aa7223f0ad59b3b8e81d30e5cdeb83..0662ece4f3b434adac16ef4c57f3377e7f5e2aea 100644 (file)
@@ -20,20 +20,15 @@ import java.text.AttributedCharacterIterator;
 import java.text.CharacterIterator;
 import java.text.FieldPosition;
 import java.text.ParsePosition;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.LinkedHashSet;
 import java.util.Locale;
-import java.util.Map;
 
 import org.junit.Test;
 
 import com.ibm.icu.dev.test.TestFmwk;
 import com.ibm.icu.text.CompactDecimalFormat;
 import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
-import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.DecimalFormat;
 import com.ibm.icu.text.NumberFormat;
-import com.ibm.icu.text.PluralRules;
 import com.ibm.icu.util.Currency;
 import com.ibm.icu.util.CurrencyAmount;
 import com.ibm.icu.util.ULocale;
@@ -61,11 +56,13 @@ public class CompactDecimalFormatTest extends TestFmwk {
             {1234567890123f, "1.2T"},
             {12345678901234f, "12T"},
             {123456789012345f, "120T"},
-            {12345678901234567890f, "12000000T"},
+            {12345678901234567890f, "12,000,000T"},
     };
 
     Object[][] SerbianTestDataShort = {
-            {1234, "1,2\u00A0\u0445\u0438\u0459."},
+            {1, "1"},
+            {12, "12"},
+            {123, "120"},
             {12345, "12\u00a0хиљ."},
             {20789, "21\u00a0хиљ."},
             {123456, "120\u00a0хиљ."},
@@ -82,6 +79,9 @@ public class CompactDecimalFormatTest extends TestFmwk {
     };
 
     Object[][] SerbianTestDataLong = {
+            {1, "1"},
+            {12, "12"},
+            {123, "120"},
             {1234, "1,2 хиљаде"},
             {12345, "12 хиљада"},
             {21789, "22 хиљаде"},
@@ -102,6 +102,9 @@ public class CompactDecimalFormatTest extends TestFmwk {
     };
 
     Object[][] SerbianTestDataLongNegative = {
+            {-1, "-1"},
+            {-12, "-12"},
+            {-123, "-120"},
             {-1234, "-1,2 хиљаде"},
             {-12345, "-12 хиљада"},
             {-21789, "-22 хиљаде"},
@@ -122,6 +125,9 @@ public class CompactDecimalFormatTest extends TestFmwk {
     };
 
     Object[][] JapaneseTestData = {
+            {1f, "1"},
+            {12f, "12"},
+            {123f, "120"},
             {1234f, "1200"},
             {12345f, "1.2万"},
             {123456f, "12万"},
@@ -137,9 +143,9 @@ public class CompactDecimalFormatTest extends TestFmwk {
     };
 
     Object[][] ChineseCurrencyTestData = {
-            // The first one should really have a ¥ in front, but the CLDR data is
-            // incorrect.  See http://unicode.org/cldr/trac/ticket/9298 and update
-            // this test case when the CLDR ticket is fixed.
+            {new CurrencyAmount(1f, Currency.getInstance("CNY")), "¥1"},
+            {new CurrencyAmount(12f, Currency.getInstance("CNY")), "¥12"},
+            {new CurrencyAmount(123f, Currency.getInstance("CNY")), "¥120"},
             {new CurrencyAmount(1234f, Currency.getInstance("CNY")), "¥1.2千"},
             {new CurrencyAmount(12345f, Currency.getInstance("CNY")), "¥1.2万"},
             {new CurrencyAmount(123456f, Currency.getInstance("CNY")), "¥12万"},
@@ -154,6 +160,9 @@ public class CompactDecimalFormatTest extends TestFmwk {
             {new CurrencyAmount(123456789012345f, Currency.getInstance("CNY")), "¥120兆"},
     };
     Object[][] GermanCurrencyTestData = {
+            {new CurrencyAmount(1f, Currency.getInstance("EUR")), "1 €"},
+            {new CurrencyAmount(12f, Currency.getInstance("EUR")), "12 €"},
+            {new CurrencyAmount(123f, Currency.getInstance("EUR")), "120 €"},
             {new CurrencyAmount(1234f, Currency.getInstance("EUR")), "1,2 Tsd. €"},
             {new CurrencyAmount(12345f, Currency.getInstance("EUR")), "12 Tsd. €"},
             {new CurrencyAmount(123456f, Currency.getInstance("EUR")), "120 Tsd. €"},
@@ -168,6 +177,9 @@ public class CompactDecimalFormatTest extends TestFmwk {
             {new CurrencyAmount(123456789012345f, Currency.getInstance("EUR")), "120 Bio. €"},
     };
     Object[][] EnglishCurrencyTestData = {
+            {new CurrencyAmount(1f, Currency.getInstance("USD")), "$1"},
+            {new CurrencyAmount(12f, Currency.getInstance("USD")), "$12"},
+            {new CurrencyAmount(123f, Currency.getInstance("USD")), "$120"},
             {new CurrencyAmount(1234f, Currency.getInstance("USD")), "$1.2K"},
             {new CurrencyAmount(12345f, Currency.getInstance("USD")), "$12K"},
             {new CurrencyAmount(123456f, Currency.getInstance("USD")), "$120K"},
@@ -183,6 +195,9 @@ public class CompactDecimalFormatTest extends TestFmwk {
     };
 
     Object[][] SwahiliTestData = {
+            {1f, "1"},
+            {12f, "12"},
+            {123f, "120"},
             {1234f, "elfu\u00a01.2"},
             {12345f, "elfu\u00a012"},
             {123456f, "elfu\u00A0120"},
@@ -194,10 +209,13 @@ public class CompactDecimalFormatTest extends TestFmwk {
             {123456789012f, "B120"},
             {1234567890123f, "T1.2"},
             {12345678901234f, "T12"},
-            {12345678901234567890f, "T12000000"},
+            {12345678901234567890f, "T12,000,000"},
     };
 
     Object[][] CsTestDataShort = {
+            {1, "1"},
+            {12, "12"},
+            {123, "120"},
             {1000, "1\u00a0tis."},
             {1500, "1,5\u00a0tis."},
             {5000, "5\u00a0tis."},
@@ -221,18 +239,21 @@ public class CompactDecimalFormatTest extends TestFmwk {
     };
 
     Object[][] SwahiliTestDataNegative = {
-            {-1234f, "elfu\u00a0-1.2"},
-            {-12345f, "elfu\u00a0-12"},
-            {-123456f, "elfu\u00A0-120"},
-            {-1234567f, "M-1.2"},
-            {-12345678f, "M-12"},
-            {-123456789f, "M-120"},
-            {-1234567890f, "B-1.2"},
-            {-12345678901f, "B-12"},
-            {-123456789012f, "B-120"},
-            {-1234567890123f, "T-1.2"},
-            {-12345678901234f, "T-12"},
-            {-12345678901234567890f, "T-12000000"},
+            {-1f, "-1"},
+            {-12f, "-12"},
+            {-123f, "-120"},
+            {-1234f, "-elfu\u00a01.2"},
+            {-12345f, "-elfu\u00a012"},
+            {-123456f, "-elfu\u00A0120"},
+            {-1234567f, "-M1.2"},
+            {-12345678f, "-M12"},
+            {-123456789f, "-M120"},
+            {-1234567890f, "-B1.2"},
+            {-12345678901f, "-B12"},
+            {-123456789012f, "-B120"},
+            {-1234567890123f, "-T1.2"},
+            {-12345678901234f, "-T12"},
+            {-12345678901234567890f, "-T12,000,000"},
     };
 
     Object[][] TestACoreCompactFormatList = {
@@ -249,74 +270,78 @@ public class CompactDecimalFormatTest extends TestFmwk {
             {2000, "2Ks$s"},
     };
 
-    @Test
-    public void TestACoreCompactFormat() {
-        Map<String,String[][]> affixes = new HashMap();
-        affixes.put("one", new String[][] {
-                {"","",}, {"","",}, {"","",},
-                {"","K"}, {"","K"}, {"","K"},
-                {"","M"}, {"","M"}, {"","M"},
-                {"","B"}, {"","B"}, {"","B"},
-                {"","T"}, {"","T"}, {"","T"},
-        });
-        affixes.put("other", new String[][] {
-                {"","",}, {"","",}, {"","",},
-                {"","Ks"}, {"","Ks"}, {"","Ks"},
-                {"","Ms"}, {"","Ms"}, {"","Ms"},
-                {"","Bs"}, {"","Bs"}, {"","Bs"},
-                {"","Ts"}, {"","Ts"}, {"","Ts"},
-        });
-
-        Map<String,String[]> currencyAffixes = new HashMap();
-        currencyAffixes.put("one", new String[] {"", "$"});
-        currencyAffixes.put("other", new String[] {"", "$s"});
-
-        long[] divisors = new long[] {
-                0,0,0,
-                1000, 1000, 1000,
-                1000000, 1000000, 1000000,
-                1000000000L, 1000000000L, 1000000000L,
-                1000000000000L, 1000000000000L, 1000000000000L};
-        long[] divisors_err = new long[] {
-                0,0,0,
-                13, 13, 13,
-                1000000, 1000000, 1000000,
-                1000000000L, 1000000000L, 1000000000L,
-                1000000000000L, 1000000000000L, 1000000000000L};
-        checkCore(affixes, null, divisors, TestACoreCompactFormatList);
-        checkCore(affixes, currencyAffixes, divisors, TestACoreCompactFormatListCurrency);
-        try {
-            checkCore(affixes, null, divisors_err, TestACoreCompactFormatList);
-        } catch(AssertionError e) {
-            // Exception expected, thus return.
-            return;
-        }
-        fail("Error expected but passed");
-    }
-
-    private void checkCore(Map<String, String[][]> affixes, Map<String, String[]> currencyAffixes, long[] divisors, Object[][] testItems) {
-        Collection<String> debugCreationErrors = new LinkedHashSet();
-        CompactDecimalFormat cdf = new CompactDecimalFormat(
-                "#,###.00",
-                DecimalFormatSymbols.getInstance(new ULocale("fr")),
-                CompactStyle.SHORT, PluralRules.createRules("one: j is 1 or f is 1"),
-                divisors, affixes, currencyAffixes,
-                debugCreationErrors
-                );
-        if (debugCreationErrors.size() != 0) {
-            for (String s : debugCreationErrors) {
-                errln("Creation error: " + s);
-            }
-        } else {
-            checkCdf("special cdf ", cdf, testItems);
-        }
-    }
+    // TODO(sffc): Re-write these tests for the new CompactDecimalFormat pipeline
+
+//    @Test
+//    public void TestACoreCompactFormat() {
+//        Map<String,String[][]> affixes = new HashMap();
+//        affixes.put("one", new String[][] {
+//                {"","",}, {"","",}, {"","",},
+//                {"","K"}, {"","K"}, {"","K"},
+//                {"","M"}, {"","M"}, {"","M"},
+//                {"","B"}, {"","B"}, {"","B"},
+//                {"","T"}, {"","T"}, {"","T"},
+//        });
+//        affixes.put("other", new String[][] {
+//                {"","",}, {"","",}, {"","",},
+//                {"","Ks"}, {"","Ks"}, {"","Ks"},
+//                {"","Ms"}, {"","Ms"}, {"","Ms"},
+//                {"","Bs"}, {"","Bs"}, {"","Bs"},
+//                {"","Ts"}, {"","Ts"}, {"","Ts"},
+//        });
+//
+//        Map<String,String[]> currencyAffixes = new HashMap();
+//        currencyAffixes.put("one", new String[] {"", "$"});
+//        currencyAffixes.put("other", new String[] {"", "$s"});
+//
+//        long[] divisors = new long[] {
+//                0,0,0,
+//                1000, 1000, 1000,
+//                1000000, 1000000, 1000000,
+//                1000000000L, 1000000000L, 1000000000L,
+//                1000000000000L, 1000000000000L, 1000000000000L};
+//        long[] divisors_err = new long[] {
+//                0,0,0,
+//                13, 13, 13,
+//                1000000, 1000000, 1000000,
+//                1000000000L, 1000000000L, 1000000000L,
+//                1000000000000L, 1000000000000L, 1000000000000L};
+//        checkCore(affixes, null, divisors, TestACoreCompactFormatList);
+//        checkCore(affixes, currencyAffixes, divisors, TestACoreCompactFormatListCurrency);
+//        try {
+//            checkCore(affixes, null, divisors_err, TestACoreCompactFormatList);
+//        } catch(AssertionError e) {
+//            // Exception expected, thus return.
+//            return;
+//        }
+//        fail("Error expected but passed");
+//    }
+
+//    private void checkCore(Map<String, String[][]> affixes, Map<String, String[]> currencyAffixes, long[] divisors, Object[][] testItems) {
+//        Collection<String> debugCreationErrors = new LinkedHashSet();
+//        CompactDecimalFormat cdf = new CompactDecimalFormat(
+//                "#,###.00",
+//                DecimalFormatSymbols.getInstance(new ULocale("fr")),
+//                CompactStyle.SHORT, PluralRules.createRules("one: j is 1 or f is 1"),
+//                divisors, affixes, currencyAffixes,
+//                debugCreationErrors
+//                );
+//        if (debugCreationErrors.size() != 0) {
+//            for (String s : debugCreationErrors) {
+//                errln("Creation error: " + s);
+//            }
+//        } else {
+//            checkCdf("special cdf ", cdf, testItems);
+//        }
+//    }
 
     @Test
     public void TestDefaultSignificantDigits() {
-        // We are expecting two significant digits as default.
+        // We are expecting two significant digits for compact formats with one or two zeros,
+        // and rounded to the unit for compact formats with three or more zeros.
         CompactDecimalFormat cdf =
                 CompactDecimalFormat.getInstance(ULocale.ENGLISH, CompactStyle.SHORT);
+        assertEquals("Default significant digits", "120K", cdf.format(123456));
         assertEquals("Default significant digits", "12K", cdf.format(12345));
         assertEquals("Default significant digits", "1.2K", cdf.format(1234));
         assertEquals("Default significant digits", "120", cdf.format(123));
@@ -432,10 +457,11 @@ public class CompactDecimalFormatTest extends TestFmwk {
         CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(
                 ULocale.ENGLISH, CompactStyle.LONG);
         BigInteger source_int = new BigInteger("31415926535897932384626433");
-        assertEquals("BigInteger format wrong: ", "31,000,000,000,000 trillion",
+        cdf.setMaximumFractionDigits(0);
+        assertEquals("BigInteger format wrong: ", "31,415,926,535,898 trillion",
                      cdf.format(source_int));
         BigDecimal source_dec = new BigDecimal(source_int);
-        assertEquals("BigDecimal format wrong: ", "31,000,000,000,000 trillion",
+        assertEquals("BigDecimal format wrong: ", "31,415,926,535,898 trillion",
                      cdf.format(source_dec));
     }
 
@@ -548,7 +574,7 @@ public class CompactDecimalFormatTest extends TestFmwk {
         result = cdf.format(new CurrencyAmount(43000f, Currency.getInstance("USD")));
         assertEquals("CDF should correctly format 43000 with currency in 'ar'", "US$ ٤٣ ألف", result);
         result = cdf.format(new CurrencyAmount(-43000f, Currency.getInstance("USD")));
-        assertEquals("CDF should correctly format -43000 with currency in 'ar'", "US$ ؜-٤٣ ألف", result);
+        assertEquals("CDF should correctly format -43000 with currency in 'ar'", "؜-US$ ٤٣ ألف", result);
 
         // Extra locale with different positive/negative formats
         cdf = CompactDecimalFormat.getInstance(new ULocale("fi"), CompactDecimalFormat.CompactStyle.SHORT);
@@ -590,4 +616,42 @@ public class CompactDecimalFormatTest extends TestFmwk {
         result = cdf.format(new CurrencyAmount(123000, Currency.getInstance("EUR")));
         assertEquals("CDF should correctly format 123000 with currency in 'it'", "120000 €", result);
     }
+
+    @Test
+    public void TestBug11319() {
+        if (logKnownIssue("11319", "CDF does not fall back from zh-Hant-HK to zh-Hant")) {
+            return;
+        }
+
+        CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(new ULocale("zh-Hant-HK"), CompactStyle.SHORT);
+        String result = cdf.format(958000000L);
+        assertEquals("CDF should correctly format 958 million in zh-Hant-HK", "9.6億", result);
+    }
+
+    @Test
+    public void TestBug12975() {
+        ULocale locale = new ULocale("it");
+        CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(locale, CompactStyle.SHORT);
+        String resultCdf = cdf.format(120000);
+        DecimalFormat df = (DecimalFormat) DecimalFormat.getInstance(locale);
+        String resultDefault = df.format(120000);
+        assertEquals("CompactDecimalFormat should use default pattern when compact pattern is unavailable",
+                     resultDefault, resultCdf);
+    }
+
+    @Test
+    public void TestBug11534() {
+        ULocale locale = new ULocale("pt_PT");
+        CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(locale, CompactStyle.SHORT);
+        String result = cdf.format(1000);
+        assertEquals("pt_PT should fall back to pt", "1 mil", result);
+    }
+
+    @Test
+    public void TestBug12181() {
+        ULocale loc = ULocale.ENGLISH;
+        CompactDecimalFormat cdf = CompactDecimalFormat.getInstance(loc, CompactStyle.SHORT);
+        String s = cdf.format(-1500);
+        assertEquals("Should work with negative numbers", "-1.5K", s);
+    }
 }
similarity index 96%
rename from icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTestData.java
rename to icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DataDrivenNumberFormatTestData.java
index ac8e1d51245d30273943da10f144e5061f313d21..167fd0bdc51ef846f2ede013eabb9990f85ffddd 100644 (file)
@@ -43,45 +43,45 @@ import com.ibm.icu.util.ULocale;
  * <p>
  * In addition each attribute is listed in the fieldOrdering static array which specifies
  * The order that attributes are printed whenever there is a test failure.
- * <p> 
+ * <p>
  * To add a new attribute, first create a public field for it.
  * Next, add the attribute name to the fieldOrdering array.
  * Finally, create a setter method for it.
- * 
+ *
  * @author rocketman
  */
-public class NumberFormatTestData {
-    
+public class DataDrivenNumberFormatTestData {
+
     /**
      * The locale.
      */
     public ULocale locale = null;
-    
+
     /**
      * The currency.
      */
     public Currency currency = null;
-    
+
     /**
      * The pattern to initialize the formatter, for example 0.00"
      */
     public String pattern = null;
-    
+
     /**
      * The value to format as a string. For example 1234.5 would be "1234.5"
      */
     public String format = null;
-    
+
     /**
      * The formatted value.
      */
     public String output = null;
-    
+
     /**
      * Field for arbitrary comments.
      */
     public String comment = null;
-    
+
     public Integer minIntegerDigits = null;
     public Integer maxIntegerDigits = null;
     public Integer minFractionDigits = null;
@@ -117,21 +117,22 @@ public class NumberFormatTestData {
     public String plural = null;
     public Integer parseIntegerOnly = null;
     public Integer decimalPatternMatchRequired = null;
+    public Integer parseCaseSensitive = null;
     public Integer parseNoExponent = null;
     public String outputCurrency = null;
-    
-    
-    
+
+
+
     /**
      * nothing or empty means that test ought to work for both C and JAVA;
      * "C" means test is known to fail in C. "J" means test is known to fail in JAVA.
      * "CJ" means test is known to fail for both languages.
      */
     public String breaks = null;
-    
+
     private static Map<String, Integer> roundingModeMap =
             new HashMap<String, Integer>();
-    
+
     static {
         roundingModeMap.put("ceiling", BigDecimal.ROUND_CEILING);
         roundingModeMap.put("floor", BigDecimal.ROUND_FLOOR);
@@ -142,18 +143,18 @@ public class NumberFormatTestData {
         roundingModeMap.put("halfUp", BigDecimal.ROUND_HALF_UP);
         roundingModeMap.put("unnecessary", BigDecimal.ROUND_UNNECESSARY);
     }
-    
+
     private static Map<String, Currency.CurrencyUsage> currencyUsageMap =
             new HashMap<String, Currency.CurrencyUsage>();
-    
+
     static {
         currencyUsageMap.put("standard", Currency.CurrencyUsage.STANDARD);
         currencyUsageMap.put("cash", Currency.CurrencyUsage.CASH);
     }
-    
+
     private static Map<String, Integer> padPositionMap =
             new HashMap<String, Integer>();
-    
+
     static {
         // TODO: Fix so that it doesn't depend on DecimalFormat.
         padPositionMap.put("beforePrefix", DecimalFormat.PAD_BEFORE_PREFIX);
@@ -161,10 +162,10 @@ public class NumberFormatTestData {
         padPositionMap.put("beforeSuffix", DecimalFormat.PAD_BEFORE_SUFFIX);
         padPositionMap.put("afterSuffix", DecimalFormat.PAD_AFTER_SUFFIX);
     }
-    
+
     private static Map<String, Integer> formatStyleMap =
             new HashMap<String, Integer>();
-    
+
     static {
         formatStyleMap.put("decimal", NumberFormat.NUMBERSTYLE);
         formatStyleMap.put("currency", NumberFormat.CURRENCYSTYLE);
@@ -175,7 +176,7 @@ public class NumberFormatTestData {
         formatStyleMap.put("currencyAccounting", NumberFormat.ACCOUNTINGCURRENCYSTYLE);
         formatStyleMap.put("cashCurrency", NumberFormat.CASHCURRENCYSTYLE);
     }
-    
+
     // Add any new fields here. On test failures, fields are printed in the same order they
     // appear here.
     private static String[] fieldOrdering = {
@@ -224,16 +225,16 @@ public class NumberFormatTestData {
         "parseNoExponent",
         "outputCurrency"
     };
-    
+
     static {
         HashSet<String> set = new HashSet<String>();
         for (String s : fieldOrdering) {
             if (!set.add(s)) {
-                throw new ExceptionInInitializerError(s + "is a duplicate field.");    
+                throw new ExceptionInInitializerError(s + "is a duplicate field.");
             }
         }
     }
-    
+
     private static <T> T fromString(Map<String, T> map, String key) {
         T value = map.get(key);
         if (value == null) {
@@ -241,222 +242,226 @@ public class NumberFormatTestData {
         }
         return value;
     }
-    
+
     // start field setters.
     // add setter for each new field in this block.
-    
+
     public void setLocale(String value) {
         locale = new ULocale(value);
     }
-    
+
     public void setCurrency(String value) {
         currency = Currency.getInstance(value);
     }
-    
+
     public void setPattern(String value) {
         pattern = value;
     }
-    
+
     public void setFormat(String value) {
         format = value;
     }
-    
+
     public void setOutput(String value) {
         output = value;
     }
-    
+
     public void setComment(String value) {
         comment = value;
     }
-    
+
     public void setMinIntegerDigits(String value) {
         minIntegerDigits = Integer.valueOf(value);
     }
-    
+
     public void setMaxIntegerDigits(String value) {
         maxIntegerDigits = Integer.valueOf(value);
     }
-    
+
     public void setMinFractionDigits(String value) {
         minFractionDigits = Integer.valueOf(value);
     }
-    
+
     public void setMaxFractionDigits(String value) {
         maxFractionDigits = Integer.valueOf(value);
     }
-    
+
     public void setMinGroupingDigits(String value) {
         minGroupingDigits = Integer.valueOf(value);
     }
-    
+
     public void setBreaks(String value) {
         breaks = value;
     }
-    
+
     public void setUseSigDigits(String value) {
         useSigDigits = Integer.valueOf(value);
     }
-    
+
     public void setMinSigDigits(String value) {
         minSigDigits = Integer.valueOf(value);
     }
-    
+
     public void setMaxSigDigits(String value) {
         maxSigDigits = Integer.valueOf(value);
     }
-    
+
     public void setUseGrouping(String value) {
         useGrouping = Integer.valueOf(value);
     }
-    
+
     public void setMultiplier(String value) {
         multiplier = Integer.valueOf(value);
     }
-    
+
     public void setRoundingIncrement(String value) {
         roundingIncrement = Double.valueOf(value);
     }
-    
+
     public void setFormatWidth(String value) {
         formatWidth = Integer.valueOf(value);
     }
-    
+
     public void setPadCharacter(String value) {
         padCharacter = value;
     }
-    
+
     public void setUseScientific(String value) {
         useScientific = Integer.valueOf(value);
     }
-    
+
     public void setGrouping(String value) {
         grouping = Integer.valueOf(value);
     }
-    
+
     public void setGrouping2(String value) {
         grouping2 = Integer.valueOf(value);
     }
-    
+
     public void setRoundingMode(String value) {
         roundingMode = fromString(roundingModeMap, value);
     }
-    
+
     public void setCurrencyUsage(String value) {
         currencyUsage = fromString(currencyUsageMap, value);
     }
-    
+
     public void setMinimumExponentDigits(String value) {
         minimumExponentDigits = Integer.valueOf(value);
     }
-    
+
     public void setExponentSignAlwaysShown(String value) {
         exponentSignAlwaysShown = Integer.valueOf(value);
     }
-    
+
     public void setDecimalSeparatorAlwaysShown(String value) {
         decimalSeparatorAlwaysShown = Integer.valueOf(value);
     }
-    
+
     public void setPadPosition(String value) {
         padPosition = fromString(padPositionMap, value);
     }
-    
+
     public void setPositivePrefix(String value) {
         positivePrefix = value;
     }
-    
+
     public void setPositiveSuffix(String value) {
         positiveSuffix = value;
     }
-    
+
     public void setNegativePrefix(String value) {
         negativePrefix = value;
     }
-    
+
     public void setNegativeSuffix(String value) {
         negativeSuffix = value;
     }
-    
+
     public void setLocalizedPattern(String value) {
         localizedPattern = value;
     }
-    
+
     public void setToPattern(String value) {
         toPattern = value;
     }
-    
+
     public void setToLocalizedPattern(String value) {
         toLocalizedPattern = value;
     }
-    
+
     public void setStyle(String value) {
         style = fromString(formatStyleMap, value);
     }
-    
+
     public void setParse(String value) {
         parse = value;
     }
-    
+
     public void setLenient(String value) {
         lenient = Integer.valueOf(value);
     }
-    
+
     public void setPlural(String value) {
         plural = value;
     }
-    
+
     public void setParseIntegerOnly(String value) {
         parseIntegerOnly = Integer.valueOf(value);
     }
-    
+
+    public void setParseCaseSensitive(String value) {
+        parseCaseSensitive = Integer.valueOf(value);
+    }
+
     public void setDecimalPatternMatchRequired(String value) {
         decimalPatternMatchRequired = Integer.valueOf(value);
     }
-    
+
     public void setParseNoExponent(String value) {
         parseNoExponent = Integer.valueOf(value);
     }
-    
+
     public void setOutputCurrency(String value) {
         outputCurrency = value;
     }
-    
+
     // end field setters.
-    
+
     // start of field clearers
     // Add clear methods that can be set in one test and cleared
     // in the next i.e the breaks field.
-    
+
     public void clearBreaks() {
         breaks = null;
     }
-    
+
     public void clearUseGrouping() {
         useGrouping = null;
     }
-    
+
     public void clearGrouping2() {
         grouping2 = null;
     }
-    
+
     public void clearGrouping() {
         grouping = null;
     }
-    
+
     public void clearMinGroupingDigits() {
         minGroupingDigits = null;
     }
-    
+
     public void clearUseScientific() {
         useScientific = null;
     }
-    
+
     public void clearDecimalSeparatorAlwaysShown() {
         decimalSeparatorAlwaysShown = null;
     }
-    
+
     // end field clearers
-    
+
     public void setField(String fieldName, String valueString)
             throws NoSuchMethodException {
         Method m = getClass().getMethod(
@@ -469,7 +474,7 @@ public class NumberFormatTestData {
             throw new RuntimeException(e);
         }
     }
-    
+
     public void clearField(String fieldName)
             throws NoSuchMethodException {
         Method m = getClass().getMethod(fieldToClearer(fieldName));
@@ -481,8 +486,9 @@ public class NumberFormatTestData {
             throw new RuntimeException(e);
         }
     }
-    
-    public String toString() {
+
+    @Override
+  public String toString() {
         StringBuilder result = new StringBuilder();
         result.append("{");
         boolean first = true;
@@ -517,7 +523,7 @@ public class NumberFormatTestData {
                 + Character.toUpperCase(fieldName.charAt(0))
                 + fieldName.substring(1);
     }
-    
+
     private static String fieldToClearer(String fieldName) {
         return "clear"
                 + Character.toUpperCase(fieldName.charAt(0))
index 66fc0fc013f85c9c5e491b88fd992f5d5bba3636..70a1fd49747ef7af5d3c411533fcb5e3c6bf18a4 100644 (file)
@@ -9,7 +9,9 @@
 package com.ibm.icu.dev.test.format;
 
 import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -21,12 +23,12 @@ import com.ibm.icu.impl.Utility;
  * A collection of methods to run the data driven number format test suite.
  */
 public class DataDrivenNumberFormatTestUtility {
-    
+
     /**
      * Base class for code under test.
      */
     public static abstract class CodeUnderTest {
-        
+
         /**
          * Returns the ID of the code under test. This ID is used to identify
          * tests that are known to fail for this particular code under test.
@@ -37,86 +39,92 @@ public class DataDrivenNumberFormatTestUtility {
         public Character Id() {
             return null;
         }
-        
+
         /**
          *  Runs a single formatting test. On success, returns null.
          *  On failure, returns the error. This implementation just returns null.
          *  Subclasses should override.
          *  @param tuple contains the parameters of the format test.
          */
-        public String format(NumberFormatTestData tuple) {
+        public String format(DataDrivenNumberFormatTestData tuple) {
+            if (tuple.output != null && tuple.output.equals("fail")) return "fail";
             return null;
         }
-        
+
         /**
          *  Runs a single toPattern test. On success, returns null.
          *  On failure, returns the error. This implementation just returns null.
          *  Subclasses should override.
          *  @param tuple contains the parameters of the format test.
          */
-        public String toPattern(NumberFormatTestData tuple) {
+        public String toPattern(DataDrivenNumberFormatTestData tuple) {
+            if (tuple.output != null && tuple.output.equals("fail")) return "fail";
             return null;
         }
-        
+
         /**
          *  Runs a single parse test. On success, returns null.
          *  On failure, returns the error. This implementation just returns null.
          *  Subclasses should override.
          *  @param tuple contains the parameters of the format test.
          */
-        public String parse(NumberFormatTestData tuple) {
+        public String parse(DataDrivenNumberFormatTestData tuple) {
+            if (tuple.output != null && tuple.output.equals("fail")) return "fail";
             return null;
         }
-        
+
         /**
          *  Runs a single parse currency test. On success, returns null.
          *  On failure, returns the error. This implementation just returns null.
          *  Subclasses should override.
          *  @param tuple contains the parameters of the format test.
          */
-        public String parseCurrency(NumberFormatTestData tuple) {
+        public String parseCurrency(DataDrivenNumberFormatTestData tuple) {
+            if (tuple.output != null && tuple.output.equals("fail")) return "fail";
             return null;
         }
-        
+
         /**
          * Runs a single select test. On success, returns null.
          *  On failure, returns the error. This implementation just returns null.
          *  Subclasses should override.
          * @param tuple contains the parameters of the format test.
          */
-        public String select(NumberFormatTestData tuple) {
+        public String select(DataDrivenNumberFormatTestData tuple) {
+            if (tuple.output != null && tuple.output.equals("fail")) return "fail";
             return null;
         }
     }
-    
+
     private static enum RunMode {
         SKIP_KNOWN_FAILURES,
-        INCLUDE_KNOWN_FAILURES     
+        INCLUDE_KNOWN_FAILURES
     }
-    
+
     private final CodeUnderTest codeUnderTest;
     private String fileLine = null;
     private int fileLineNumber = 0;
-    private String fileTestName = "";   
-    private NumberFormatTestData tuple = new NumberFormatTestData();
-      
+    private String fileTestName = "";
+    private DataDrivenNumberFormatTestData tuple = new DataDrivenNumberFormatTestData();
+
     /**
      * Runs all the tests in the data driven test suite against codeUnderTest.
      * @param fileName The name of the test file. A relative file name under
      *   com/ibm/icu/dev/data such as "data.txt"
      * @param codeUnderTest the code under test
      */
-    
+
     static void runSuite(
             String fileName, CodeUnderTest codeUnderTest) {
         new DataDrivenNumberFormatTestUtility(codeUnderTest)
                 .run(fileName, RunMode.SKIP_KNOWN_FAILURES);
     }
-    
+
     /**
      * Runs every format test in data driven test suite including those
-     * that are known to fail.
-     * 
+     * that are known to fail.  If a test is supposed to fail but actually
+     * passes, an error is printed.
+     *
      * @param fileName The name of the test file. A relative file name under
      *   com/ibm/icu/dev/data such as "data.txt"
      * @param codeUnderTest the code under test
@@ -126,12 +134,12 @@ public class DataDrivenNumberFormatTestUtility {
         new DataDrivenNumberFormatTestUtility(codeUnderTest)
                 .run(fileName, RunMode.INCLUDE_KNOWN_FAILURES);
     }
-    
+
     private DataDrivenNumberFormatTestUtility(
             CodeUnderTest codeUnderTest) {
         this.codeUnderTest = codeUnderTest;
     }
-       
+
     private void run(String fileName, RunMode runMode) {
         Character codeUnderTestIdObj = codeUnderTest.Id();
         char codeUnderTestId =
@@ -144,7 +152,7 @@ public class DataDrivenNumberFormatTestUtility {
             if (fileLine != null && fileLine.charAt(0) == '\uFEFF') {
                 fileLine = fileLine.substring(1);
             }
-            
+
             int state = 0;
             List<String> columnValues;
             List<String> columnNames = null;
@@ -166,7 +174,7 @@ public class DataDrivenNumberFormatTestUtility {
                 if (state == 0) {
                     if (fileLine.startsWith("test ")) {
                         fileTestName = fileLine;
-                        tuple = new NumberFormatTestData();
+                        tuple = new DataDrivenNumberFormatTestData();
                     } else if (fileLine.startsWith("set ")) {
                         if (!setTupleField()) {
                             return;
@@ -196,19 +204,41 @@ public class DataDrivenNumberFormatTestUtility {
                             return;
                         }
                     }
-                    if (runMode == RunMode.INCLUDE_KNOWN_FAILURES
-                            || !breaks(codeUnderTestId)) {
-                        String errorMessage = isPass(tuple);
-                        if (errorMessage != null) {
-                            showError(errorMessage);
+                    if (runMode == RunMode.INCLUDE_KNOWN_FAILURES || !breaks(codeUnderTestId)) {
+                        String errorMessage;
+                        Exception err = null;
+                        boolean shouldFail = (tuple.output != null && tuple.output.equals("fail"))
+                                ? !breaks(codeUnderTestId)
+                                : breaks(codeUnderTestId);
+                        try {
+                            errorMessage = isPass(tuple);
+                        } catch (Exception e) {
+                            err = e;
+                            errorMessage = "Exception: " + e + ": " + e.getCause();
+                        }
+                        if (shouldFail && errorMessage == null) {
+                            showError("Expected failure, but passed");
+                        } else if (!shouldFail && errorMessage != null) {
+                            if (err != null) {
+                                ByteArrayOutputStream os = new ByteArrayOutputStream();
+                                PrintStream ps = new PrintStream(os);
+                                err.printStackTrace(ps);
+                                String stackTrace = os.toString();
+                                showError(errorMessage + "     Stack trace: " + stackTrace.substring(0, 500));
+                            } else {
+                                showError(errorMessage);
+                            }
                         }
                     }
                 }
                 fileLine = null;
             }
         } catch (Exception e) {
-            showError(e.toString());
-            e.printStackTrace();
+            ByteArrayOutputStream os = new ByteArrayOutputStream();
+            PrintStream ps = new PrintStream(os);
+            e.printStackTrace(ps);
+            String stackTrace = os.toString();
+            showError("MAJOR ERROR: " + e.toString() + "     Stack trace: " + stackTrace.substring(0,500));
         } finally {
             try {
                 if (in != null) {
@@ -228,7 +258,7 @@ public class DataDrivenNumberFormatTestUtility {
     private static boolean isSpace(char c) {
         return (c == 0x09 || c == 0x20 || c == 0x3000);
     }
-    
+
     private boolean setTupleField() {
         List<String> parts = splitBy(3, (char) 0x20);
         if (parts.size() < 3) {
@@ -237,7 +267,7 @@ public class DataDrivenNumberFormatTestUtility {
         }
         return setField(parts.get(1), parts.get(2));
     }
-    
+
     private boolean setField(String name, String value) {
         try {
             tuple.setField(name,  Utility.unescape(value));
@@ -247,7 +277,7 @@ public class DataDrivenNumberFormatTestUtility {
             return false;
         }
     }
-    
+
     private boolean clearField(String name) {
         try {
             tuple.clearField(name);
@@ -257,17 +287,17 @@ public class DataDrivenNumberFormatTestUtility {
             return false;
         }
     }
-    
+
     private void showError(String message) {
         TestFmwk.errln(String.format("line %d: %s\n%s\n%s", fileLineNumber, Utility.escape(message), fileTestName,fileLine));
     }
-   
+
     private List<String> splitBy(char delimiter) {
         return splitBy(Integer.MAX_VALUE, delimiter);
     }
-      
+
     private List<String> splitBy(int max, char delimiter) {
-        ArrayList<String> result = new ArrayList<String>();    
+        ArrayList<String> result = new ArrayList<String>();
         int colIdx = 0;
         int colStart = 0;
         int len = fileLine.length();
@@ -282,7 +312,7 @@ public class DataDrivenNumberFormatTestUtility {
         }
         result.add(fileLine.substring(colStart, len));
         return result;
-    }  
+    }
 
     private boolean readLine(BufferedReader in) throws IOException {
         String line = in.readLine();
@@ -301,8 +331,8 @@ public class DataDrivenNumberFormatTestUtility {
         fileLine = idx == 0 ? "" : line;
         return true;
     }
-    
-    private String isPass(NumberFormatTestData tuple) {
+
+    private String isPass(DataDrivenNumberFormatTestData tuple) {
         StringBuilder result = new StringBuilder();
         if (tuple.format != null && tuple.output != null) {
             String errorMessage = codeUnderTest.format(tuple);
index ad0a462509e2841df4aa458be8c334e39ca4e27b..973e71e1a6594a5b61330d2d795b34ed016166af 100644 (file)
@@ -6,11 +6,11 @@
  *   Corporation and others.  All Rights Reserved.
  **/
 
-/** 
+/**
  * Port From:   JDK 1.4b1 : java.text.Format.IntlTestDecimalFormatAPI
  * Source File: java/text/format/IntlTestDecimalFormatAPI.java
  **/
+
 /*
     @test 1.4 98/03/06
     @summary test International Decimal Format API
@@ -35,18 +35,18 @@ import com.ibm.icu.text.NumberFormat;
 public class IntlTestDecimalFormatAPI extends com.ibm.icu.dev.test.TestFmwk
 {
     /**
-     * Problem 1: simply running 
-     * decF4.setRoundingMode(java.math.BigDecimal.ROUND_HALF_UP) does not work 
+     * Problem 1: simply running
+     * decF4.setRoundingMode(java.math.BigDecimal.ROUND_HALF_UP) does not work
      * as decF4.setRoundingIncrement(.0001) must also be run.
-     * Problem 2: decF4.format(8.88885) does not return 8.8889 as expected. 
-     * You must run decF4.format(new BigDecimal(Double.valueOf(8.88885))) in 
+     * Problem 2: decF4.format(8.88885) does not return 8.8889 as expected.
+     * You must run decF4.format(new BigDecimal(Double.valueOf(8.88885))) in
      * order for this to work as expected.
-     * Problem 3: There seems to be no way to set half up to be the default 
+     * Problem 3: There seems to be no way to set half up to be the default
      * rounding mode.
-     * We solved the problem with the code at the bottom of this page however 
+     * We solved the problem with the code at the bottom of this page however
      * this is not quite general purpose enough to include in icu4j. A static
-     * setDefaultRoundingMode function would solve the problem nicely. Also 
-     * decimal places past 20 are not handled properly. A small ammount of work 
+     * setDefaultRoundingMode function would solve the problem nicely. Also
+     * decimal places past 20 are not handled properly. A small ammount of work
      * would make bring this up to snuff.
      */
     @Test
@@ -55,7 +55,7 @@ public class IntlTestDecimalFormatAPI extends com.ibm.icu.dev.test.TestFmwk
         // problem 2
         double number = 8.88885;
         String expected = "8.8889";
-        
+
         String pat = ",##0.0000";
         DecimalFormat dec = new DecimalFormat(pat);
         dec.setRoundingMode(BigDecimal.ROUND_HALF_UP);
@@ -65,7 +65,7 @@ public class IntlTestDecimalFormatAPI extends com.ibm.icu.dev.test.TestFmwk
         if (!str.equals(expected)) {
             errln("Fail: " + number + " x \"" + pat + "\" = \"" +
                   str + "\", expected \"" + expected + "\"");
-        }   
+        }
 
         pat = ",##0.0001";
         dec = new DecimalFormat(pat);
@@ -74,25 +74,25 @@ public class IntlTestDecimalFormatAPI extends com.ibm.icu.dev.test.TestFmwk
         if (!str.equals(expected)) {
             errln("Fail: " + number + " x \"" + pat + "\" = \"" +
                   str + "\", expected \"" + expected + "\"");
-        }  
-        
+        }
+
         // testing 20 decimal places
         pat = ",##0.00000000000000000001";
         dec = new DecimalFormat(pat);
         BigDecimal bignumber = new BigDecimal("8.888888888888888888885");
         expected = "8.88888888888888888889";
-        
+
         dec.setRoundingMode(BigDecimal.ROUND_HALF_UP);
-        str = dec.format(bignumber); 
+        str = dec.format(bignumber);
         if (!str.equals(expected)) {
             errln("Fail: " + bignumber + " x \"" + pat + "\" = \"" +
                   str + "\", expected \"" + expected + "\"");
-        }   
-        
+        }
+
     }
 
-    /** 
-     * This test checks various generic API methods in DecimalFormat to achieve 
+    /**
+     * This test checks various generic API methods in DecimalFormat to achieve
      * 100% API coverage.
      */
     @Test
@@ -298,7 +298,7 @@ public class IntlTestDecimalFormatAPI extends com.ibm.icu.dev.test.TestFmwk
         DecimalFormat decfmt = new DecimalFormat();
         MathContext resultICU;
 
-        MathContext comp1 = new MathContext(0, MathContext.PLAIN);
+        MathContext comp1 = new MathContext(0, MathContext.PLAIN, false, MathContext.ROUND_HALF_EVEN);
         resultICU = decfmt.getMathContextICU();
         if ((comp1.getDigits() != resultICU.getDigits()) ||
             (comp1.getForm() != resultICU.getForm()) ||
@@ -309,7 +309,7 @@ public class IntlTestDecimalFormatAPI extends com.ibm.icu.dev.test.TestFmwk
                 " / expected: " + comp1.toString());
         }
 
-        MathContext comp2 = new MathContext(5, MathContext.ENGINEERING);
+        MathContext comp2 = new MathContext(5, MathContext.ENGINEERING, false, MathContext.ROUND_HALF_EVEN);
         decfmt.setMathContextICU(comp2);
         resultICU = decfmt.getMathContextICU();
         if ((comp2.getDigits() != resultICU.getDigits()) ||
@@ -344,7 +344,7 @@ public class IntlTestDecimalFormatAPI extends com.ibm.icu.dev.test.TestFmwk
         // get default rounding increment
         r1 = pat.getRoundingIncrement();
 
-        // set rounding mode with zero increment.  Rounding 
+        // set rounding mode with zero increment.  Rounding
         // increment should be set by this operation
         pat.setRoundingMode(BigDecimal.ROUND_UP);
         r2 = pat.getRoundingIncrement();
@@ -358,43 +358,43 @@ public class IntlTestDecimalFormatAPI extends com.ibm.icu.dev.test.TestFmwk
             }
         }
     }
-    
+
     @Test
     public void testJB6648()
     {
         DecimalFormat df = new DecimalFormat();
         df.setParseStrict(true);
-        
+
         String numstr = new String();
-        
+
         String[] patterns = {
             "0",
             "00",
             "000",
             "0,000",
             "0.0",
-            "#000.0"          
+            "#000.0"
         };
-        
+
         for(int i=0; i < patterns.length; i++) {
             df.applyPattern(patterns[i]);
-            numstr = df.format(5);        
+            numstr = df.format(5);
             try {
                 Number n = df.parse(numstr);
                 logln("INFO: Parsed " + numstr + " -> " + n);
             } catch (ParseException pe) {
                 errln("ERROR: Failed round trip with strict parsing.");
-            }           
+            }
         }
-        
+
         df.applyPattern(patterns[1]);
-        numstr = "005";        
+        numstr = "005";
         try {
             Number n = df.parse(numstr);
             logln("INFO: Successful parse for " + numstr + " with strict parse enabled. Number is " + n);
         } catch (ParseException pe) {
             errln("ERROR: Parse Exception encountered in strict mode: numstr -> " + numstr);
-        }  
-        
+        }
+
     }
 }
index 5a8a5448f8080b94ccef9cbbd402e4bc3a3301e0..f0fe8ea5fcb7bc98b5cf82de9e8dfacd416da115 100644 (file)
@@ -228,7 +228,7 @@ public class IntlTestDecimalFormatAPIC extends com.ibm.icu.dev.test.TestFmwk {
         s2 = pat.toPattern();
         logln("Extracted pattern is " + s2);
         if (!s2.equals(p1)) {
-            errln("ERROR: toPattern() result did not match pattern applied");
+            errln("ERROR: toPattern() result did not match pattern applied: " + p1 + " vs " + s2);
         }
 
         String p2 = new String("#,##0.0# FF;(#,##0.0# FF)");
@@ -237,9 +237,7 @@ public class IntlTestDecimalFormatAPIC extends com.ibm.icu.dev.test.TestFmwk {
         String s3;
         s3 = pat.toLocalizedPattern();
         logln("Extracted pattern is " + s3);
-        if (!s3.equals(p2)) {
-            errln("ERROR: toLocalizedPattern() result did not match pattern applied");
-        }
+        assertEquals("ERROR: toLocalizedPattern() result did not match pattern applied", p2, s3);
 
         // ======= Test getStaticClassID()
 
index f8b4eaa2e39f31bae2c55b96e59721f3073d3766..77495f41d342271d3025ef47eb6d4cb4e0f39be6 100644 (file)
@@ -7,7 +7,7 @@
  *******************************************************************************
  */
 
-/** 
+/**
  * Port From:   ICU4C v1.8.1 : format : IntlTestDecimalFormatSymbols
  * Source File: $ICU4CRoot/source/test/intltest/tsdcfmsy.cpp
  **/
@@ -30,105 +30,105 @@ public class IntlTestDecimalFormatSymbolsC extends com.ibm.icu.dev.test.TestFmwk
      * Test the API of DecimalFormatSymbols; primarily a simple get/set set.
      */
     @Test
-    public void TestSymbols() {    
-        DecimalFormatSymbols fr = new DecimalFormatSymbols(Locale.FRENCH);    
+    public void TestSymbols() {
+        DecimalFormatSymbols fr = new DecimalFormatSymbols(Locale.FRENCH);
         DecimalFormatSymbols en = new DecimalFormatSymbols(Locale.ENGLISH);
-    
+
         if (en.equals(fr)) {
             errln("ERROR: English DecimalFormatSymbols equal to French");
         }
-    
+
         // just do some VERY basic tests to make sure that get/set work
-    
+
         char zero = en.getZeroDigit();
         fr.setZeroDigit(zero);
         if (fr.getZeroDigit() != en.getZeroDigit()) {
             errln("ERROR: get/set ZeroDigit failed");
         }
-    
+
         char group = en.getGroupingSeparator();
         fr.setGroupingSeparator(group);
         if (fr.getGroupingSeparator() != en.getGroupingSeparator()) {
             errln("ERROR: get/set GroupingSeparator failed");
         }
-    
+
         char decimal = en.getDecimalSeparator();
         fr.setDecimalSeparator(decimal);
         if (fr.getDecimalSeparator() != en.getDecimalSeparator()) {
             errln("ERROR: get/set DecimalSeparator failed");
         }
-    
+
         char perMill = en.getPerMill();
         fr.setPerMill(perMill);
         if (fr.getPerMill() != en.getPerMill()) {
             errln("ERROR: get/set PerMill failed");
         }
-    
+
         char percent = en.getPercent();
         fr.setPercent(percent);
         if (fr.getPercent() != en.getPercent()) {
             errln("ERROR: get/set Percent failed");
         }
-    
+
         char digit = en.getDigit();
         fr.setDigit(digit);
         if (fr.getPercent() != en.getPercent()) {
             errln("ERROR: get/set Percent failed");
         }
-    
+
         char patternSeparator = en.getPatternSeparator();
         fr.setPatternSeparator(patternSeparator);
         if (fr.getPatternSeparator() != en.getPatternSeparator()) {
             errln("ERROR: get/set PatternSeparator failed");
         }
-    
+
         String infinity = en.getInfinity();
         fr.setInfinity(infinity);
         String infinity2 = fr.getInfinity();
         if (!infinity.equals(infinity2)) {
             errln("ERROR: get/set Infinity failed");
         }
-    
+
         String nan = en.getNaN();
         fr.setNaN(nan);
         String nan2 = fr.getNaN();
         if (!nan.equals(nan2)) {
             errln("ERROR: get/set NaN failed");
         }
-    
+
         char minusSign = en.getMinusSign();
         fr.setMinusSign(minusSign);
         if (fr.getMinusSign() != en.getMinusSign()) {
             errln("ERROR: get/set MinusSign failed");
         }
-    
+
         //        char exponential = en.getExponentialSymbol();
         //        fr.setExponentialSymbol(exponential);
         //        if(fr.getExponentialSymbol() != en.getExponentialSymbol()) {
         //            errln("ERROR: get/set Exponential failed");
         //        }
-    
+
         //DecimalFormatSymbols foo = new DecimalFormatSymbols(); //The variable is never used
-    
+
         en = (DecimalFormatSymbols) fr.clone();
-    
+
         if (!en.equals(fr)) {
             errln("ERROR: Clone failed");
         }
-        
+
         DecimalFormatSymbols sym = new DecimalFormatSymbols(Locale.US);
-    
+
         verify(34.5, "00.00", sym, "34.50");
         sym.setDecimalSeparator('S');
         verify(34.5, "00.00", sym, "34S50");
         sym.setPercent('P');
         verify(34.5, "00 %", sym, "3450 P");
         sym.setCurrencySymbol("D");
-        verify(34.5, "\u00a4##.##", sym, "D34.5");
+        verify(34.5, "\u00a4##.##", sym, "D34.50");
         sym.setGroupingSeparator('|');
         verify(3456.5, "0,000.##", sym, "3|456S5");
     }
-    
+
     /** helper functions**/
     public void verify(double value, String pattern, DecimalFormatSymbols sym, String expected) {
         DecimalFormat df = new DecimalFormat(pattern, sym);
@@ -136,7 +136,7 @@ public class IntlTestDecimalFormatSymbolsC extends com.ibm.icu.dev.test.TestFmwk
         FieldPosition pos = new FieldPosition(-1);
         buffer = df.format(value, buffer, pos);
         if(!buffer.toString().equals(expected)){
-            errln("ERROR: format failed after setSymbols()\n Expected" + 
+            errln("ERROR: format failed after setSymbols()\n Expected" +
                 expected + ", Got " + buffer);
         }
     }
index d4fbe45f2df078e271ffd0e79901c63fe1b132ea..ddfd80c95ca26f68d7d2948d1f06fc075c253fdb 100644 (file)
@@ -30,6 +30,7 @@ import java.util.TreeMap;
 import org.junit.Test;
 
 import com.ibm.icu.dev.test.TestFmwk;
+import com.ibm.icu.dev.test.serializable.FormatHandler;
 import com.ibm.icu.dev.test.serializable.SerializableTestUtility;
 import com.ibm.icu.impl.Pair;
 import com.ibm.icu.impl.Utility;
@@ -2035,6 +2036,13 @@ public class MeasureUnitTest extends TestFmwk {
         }
     }
 
+    @Test
+    public void testBug11966() {
+        Locale locale = new Locale("en", "AU");
+        MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.WIDE);
+        // Should not throw an exception.
+    }
+
     // DO NOT DELETE THIS FUNCTION! It may appear as dead code, but we use this to generate code
     // for MeasureFormat during the release process.
     static Map<MeasureUnit, Pair<MeasureUnit, MeasureUnit>> getUnitsToPerParts() {
@@ -2528,6 +2536,8 @@ public class MeasureUnitTest extends TestFmwk {
 
     public static class MeasureFormatHandler  implements SerializableTestUtility.Handler
     {
+        FormatHandler.NumberFormatHandler nfh = new FormatHandler.NumberFormatHandler();
+
         @Override
         public Object[] getTestObjects()
         {
@@ -2547,8 +2557,7 @@ public class MeasureUnitTest extends TestFmwk {
             MeasureFormat b1 = (MeasureFormat) b;
             return a1.getLocale().equals(b1.getLocale())
                     && a1.getWidth().equals(b1.getWidth())
-                    && a1.getNumberFormat().equals(b1.getNumberFormat())
-                    ;
+                    && nfh.hasSameBehavior(a1.getNumberFormat(), b1.getNumberFormat());
         }
     }
 }
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatDataDrivenTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatDataDrivenTest.java
new file mode 100644 (file)
index 0000000..6532e7e
--- /dev/null
@@ -0,0 +1,433 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.dev.test.format;
+
+import java.math.BigDecimal;
+import java.text.ParsePosition;
+
+import org.junit.Test;
+
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.DecimalFormat_ICU58;
+import com.ibm.icu.util.CurrencyAmount;
+import com.ibm.icu.util.ULocale;
+
+public class NumberFormatDataDrivenTest {
+
+  private static ULocale EN = new ULocale("en");
+
+  private static Number toNumber(String s) {
+    if (s.equals("NaN")) {
+      return Double.NaN;
+    } else if (s.equals("-Inf")) {
+      return Double.NEGATIVE_INFINITY;
+    } else if (s.equals("Inf")) {
+      return Double.POSITIVE_INFINITY;
+    }
+    return new BigDecimal(s);
+  }
+
+  private DataDrivenNumberFormatTestUtility.CodeUnderTest ICU58 =
+      new DataDrivenNumberFormatTestUtility.CodeUnderTest() {
+        @Override
+        public Character Id() {
+          return 'J';
+        }
+
+        @Override
+        public String format(DataDrivenNumberFormatTestData tuple) {
+          DecimalFormat_ICU58 fmt = createDecimalFormat(tuple);
+          String actual = fmt.format(toNumber(tuple.format));
+          String expected = tuple.output;
+          if (!expected.equals(actual)) {
+            return "Expected " + expected + ", got " + actual;
+          }
+          return null;
+        }
+
+        @Override
+        public String toPattern(DataDrivenNumberFormatTestData tuple) {
+          DecimalFormat_ICU58 fmt = createDecimalFormat(tuple);
+          StringBuilder result = new StringBuilder();
+          if (tuple.toPattern != null) {
+            String expected = tuple.toPattern;
+            String actual = fmt.toPattern();
+            if (!expected.equals(actual)) {
+              result.append("Expected toPattern=" + expected + ", got " + actual);
+            }
+          }
+          if (tuple.toLocalizedPattern != null) {
+            String expected = tuple.toLocalizedPattern;
+            String actual = fmt.toLocalizedPattern();
+            if (!expected.equals(actual)) {
+              result.append("Expected toLocalizedPattern=" + expected + ", got " + actual);
+            }
+          }
+          return result.length() == 0 ? null : result.toString();
+        }
+
+        @Override
+        public String parse(DataDrivenNumberFormatTestData tuple) {
+          DecimalFormat_ICU58 fmt = createDecimalFormat(tuple);
+          ParsePosition ppos = new ParsePosition(0);
+          Number actual = fmt.parse(tuple.parse, ppos);
+          if (ppos.getIndex() == 0) {
+            return "Parse failed; got " + actual + ", but expected " + tuple.output;
+          }
+          if (tuple.output.equals("fail")) {
+            return null;
+          }
+          Number expected = toNumber(tuple.output);
+          // number types cannot be compared, this is the best we can do.
+          if (expected.doubleValue() != actual.doubleValue()
+              && !Double.isNaN(expected.doubleValue())
+              && !Double.isNaN(expected.doubleValue())) {
+            return "Expected: " + expected + ", got: " + actual;
+          }
+          return null;
+        }
+
+        @Override
+        public String parseCurrency(DataDrivenNumberFormatTestData tuple) {
+          DecimalFormat_ICU58 fmt = createDecimalFormat(tuple);
+          ParsePosition ppos = new ParsePosition(0);
+          CurrencyAmount currAmt = fmt.parseCurrency(tuple.parse, ppos);
+          if (ppos.getIndex() == 0) {
+            return "Parse failed; got " + currAmt + ", but expected " + tuple.output;
+          }
+          if (tuple.output.equals("fail")) {
+            return null;
+          }
+          Number expected = toNumber(tuple.output);
+          Number actual = currAmt.getNumber();
+          // number types cannot be compared, this is the best we can do.
+          if (expected.doubleValue() != actual.doubleValue()
+              && !Double.isNaN(expected.doubleValue())
+              && !Double.isNaN(expected.doubleValue())) {
+            return "Expected: " + expected + ", got: " + actual;
+          }
+
+          if (!tuple.outputCurrency.equals(currAmt.getCurrency().toString())) {
+            return "Expected currency: " + tuple.outputCurrency + ", got: " + currAmt.getCurrency();
+          }
+          return null;
+        }
+
+        /**
+         * @param tuple
+         * @return
+         */
+        private DecimalFormat_ICU58 createDecimalFormat(DataDrivenNumberFormatTestData tuple) {
+
+          DecimalFormat_ICU58 fmt =
+              new DecimalFormat_ICU58(
+                  tuple.pattern == null ? "0" : tuple.pattern,
+                  new DecimalFormatSymbols(tuple.locale == null ? EN : tuple.locale));
+          adjustDecimalFormat(tuple, fmt);
+          return fmt;
+        }
+        /**
+         * @param tuple
+         * @param fmt
+         */
+        private void adjustDecimalFormat(
+            DataDrivenNumberFormatTestData tuple, DecimalFormat_ICU58 fmt) {
+          if (tuple.minIntegerDigits != null) {
+            fmt.setMinimumIntegerDigits(tuple.minIntegerDigits);
+          }
+          if (tuple.maxIntegerDigits != null) {
+            fmt.setMaximumIntegerDigits(tuple.maxIntegerDigits);
+          }
+          if (tuple.minFractionDigits != null) {
+            fmt.setMinimumFractionDigits(tuple.minFractionDigits);
+          }
+          if (tuple.maxFractionDigits != null) {
+            fmt.setMaximumFractionDigits(tuple.maxFractionDigits);
+          }
+          if (tuple.currency != null) {
+            fmt.setCurrency(tuple.currency);
+          }
+          if (tuple.minGroupingDigits != null) {
+            // Oops we don't support this.
+          }
+          if (tuple.useSigDigits != null) {
+            fmt.setSignificantDigitsUsed(tuple.useSigDigits != 0);
+          }
+          if (tuple.minSigDigits != null) {
+            fmt.setMinimumSignificantDigits(tuple.minSigDigits);
+          }
+          if (tuple.maxSigDigits != null) {
+            fmt.setMaximumSignificantDigits(tuple.maxSigDigits);
+          }
+          if (tuple.useGrouping != null) {
+            fmt.setGroupingUsed(tuple.useGrouping != 0);
+          }
+          if (tuple.multiplier != null) {
+            fmt.setMultiplier(tuple.multiplier);
+          }
+          if (tuple.roundingIncrement != null) {
+            fmt.setRoundingIncrement(tuple.roundingIncrement.doubleValue());
+          }
+          if (tuple.formatWidth != null) {
+            fmt.setFormatWidth(tuple.formatWidth);
+          }
+          if (tuple.padCharacter != null && tuple.padCharacter.length() > 0) {
+            fmt.setPadCharacter(tuple.padCharacter.charAt(0));
+          }
+          if (tuple.useScientific != null) {
+            fmt.setScientificNotation(tuple.useScientific != 0);
+          }
+          if (tuple.grouping != null) {
+            fmt.setGroupingSize(tuple.grouping);
+          }
+          if (tuple.grouping2 != null) {
+            fmt.setSecondaryGroupingSize(tuple.grouping2);
+          }
+          if (tuple.roundingMode != null) {
+            fmt.setRoundingMode(tuple.roundingMode);
+          }
+          if (tuple.currencyUsage != null) {
+            fmt.setCurrencyUsage(tuple.currencyUsage);
+          }
+          if (tuple.minimumExponentDigits != null) {
+            fmt.setMinimumExponentDigits(tuple.minimumExponentDigits.byteValue());
+          }
+          if (tuple.exponentSignAlwaysShown != null) {
+            fmt.setExponentSignAlwaysShown(tuple.exponentSignAlwaysShown != 0);
+          }
+          if (tuple.decimalSeparatorAlwaysShown != null) {
+            fmt.setDecimalSeparatorAlwaysShown(tuple.decimalSeparatorAlwaysShown != 0);
+          }
+          if (tuple.padPosition != null) {
+            fmt.setPadPosition(tuple.padPosition);
+          }
+          if (tuple.positivePrefix != null) {
+            fmt.setPositivePrefix(tuple.positivePrefix);
+          }
+          if (tuple.positiveSuffix != null) {
+            fmt.setPositiveSuffix(tuple.positiveSuffix);
+          }
+          if (tuple.negativePrefix != null) {
+            fmt.setNegativePrefix(tuple.negativePrefix);
+          }
+          if (tuple.negativeSuffix != null) {
+            fmt.setNegativeSuffix(tuple.negativeSuffix);
+          }
+          if (tuple.localizedPattern != null) {
+            fmt.applyLocalizedPattern(tuple.localizedPattern);
+          }
+          int lenient = tuple.lenient == null ? 1 : tuple.lenient.intValue();
+          fmt.setParseStrict(lenient == 0);
+          if (tuple.parseIntegerOnly != null) {
+            fmt.setParseIntegerOnly(tuple.parseIntegerOnly != 0);
+          }
+          if (tuple.parseCaseSensitive != null) {
+            // Not supported.
+          }
+          if (tuple.decimalPatternMatchRequired != null) {
+            fmt.setDecimalPatternMatchRequired(tuple.decimalPatternMatchRequired != 0);
+          }
+          if (tuple.parseNoExponent != null) {
+            // Oops, not supported for now
+          }
+        }
+      };
+
+  private DataDrivenNumberFormatTestUtility.CodeUnderTest JDK =
+      new DataDrivenNumberFormatTestUtility.CodeUnderTest() {
+        @Override
+        public Character Id() {
+          return 'K';
+        }
+
+        @Override
+        public String format(DataDrivenNumberFormatTestData tuple) {
+          java.text.DecimalFormat fmt = createDecimalFormat(tuple);
+          String actual = fmt.format(toNumber(tuple.format));
+          String expected = tuple.output;
+          if (!expected.equals(actual)) {
+            return "Expected " + expected + ", got " + actual;
+          }
+          return null;
+        }
+
+        @Override
+        public String toPattern(DataDrivenNumberFormatTestData tuple) {
+          java.text.DecimalFormat fmt = createDecimalFormat(tuple);
+          StringBuilder result = new StringBuilder();
+          if (tuple.toPattern != null) {
+            String expected = tuple.toPattern;
+            String actual = fmt.toPattern();
+            if (!expected.equals(actual)) {
+              result.append("Expected toPattern=" + expected + ", got " + actual);
+            }
+          }
+          if (tuple.toLocalizedPattern != null) {
+            String expected = tuple.toLocalizedPattern;
+            String actual = fmt.toLocalizedPattern();
+            if (!expected.equals(actual)) {
+              result.append("Expected toLocalizedPattern=" + expected + ", got " + actual);
+            }
+          }
+          return result.length() == 0 ? null : result.toString();
+        }
+
+        @Override
+        public String parse(DataDrivenNumberFormatTestData tuple) {
+          java.text.DecimalFormat fmt = createDecimalFormat(tuple);
+          ParsePosition ppos = new ParsePosition(0);
+          Number actual = fmt.parse(tuple.parse, ppos);
+          if (ppos.getIndex() == 0) {
+            return "Parse failed; got " + actual + ", but expected " + tuple.output;
+          }
+          if (tuple.output.equals("fail")) {
+            return null;
+          }
+          Number expected = toNumber(tuple.output);
+          // number types cannot be compared, this is the best we can do.
+          if (expected.doubleValue() != actual.doubleValue()
+              && !Double.isNaN(expected.doubleValue())
+              && !Double.isNaN(expected.doubleValue())) {
+            return "Expected: " + expected + ", got: " + actual;
+          }
+          return null;
+        }
+
+        /**
+         * @param tuple
+         * @return
+         */
+        private java.text.DecimalFormat createDecimalFormat(DataDrivenNumberFormatTestData tuple) {
+          java.text.DecimalFormat fmt =
+              new java.text.DecimalFormat(
+                  tuple.pattern == null ? "0" : tuple.pattern,
+                  new java.text.DecimalFormatSymbols(
+                      (tuple.locale == null ? EN : tuple.locale).toLocale()));
+          adjustDecimalFormat(tuple, fmt);
+          return fmt;
+        }
+
+        /**
+         * @param tuple
+         * @param fmt
+         */
+        private void adjustDecimalFormat(
+            DataDrivenNumberFormatTestData tuple, java.text.DecimalFormat fmt) {
+          if (tuple.minIntegerDigits != null) {
+            fmt.setMinimumIntegerDigits(tuple.minIntegerDigits);
+          }
+          if (tuple.maxIntegerDigits != null) {
+            fmt.setMaximumIntegerDigits(tuple.maxIntegerDigits);
+          }
+          if (tuple.minFractionDigits != null) {
+            fmt.setMinimumFractionDigits(tuple.minFractionDigits);
+          }
+          if (tuple.maxFractionDigits != null) {
+            fmt.setMaximumFractionDigits(tuple.maxFractionDigits);
+          }
+          if (tuple.currency != null) {
+            fmt.setCurrency(java.util.Currency.getInstance(tuple.currency.toString()));
+          }
+          if (tuple.minGroupingDigits != null) {
+            // Oops we don't support this.
+          }
+          if (tuple.useSigDigits != null) {
+            // Oops we don't support this
+          }
+          if (tuple.minSigDigits != null) {
+            // Oops we don't support this
+          }
+          if (tuple.maxSigDigits != null) {
+            // Oops we don't support this
+          }
+          if (tuple.useGrouping != null) {
+            fmt.setGroupingUsed(tuple.useGrouping != 0);
+          }
+          if (tuple.multiplier != null) {
+            fmt.setMultiplier(tuple.multiplier);
+          }
+          if (tuple.roundingIncrement != null) {
+            // Not supported
+          }
+          if (tuple.formatWidth != null) {
+            // Not supported
+          }
+          if (tuple.padCharacter != null && tuple.padCharacter.length() > 0) {
+            // Not supported
+          }
+          if (tuple.useScientific != null) {
+            // Not supported
+          }
+          if (tuple.grouping != null) {
+            fmt.setGroupingSize(tuple.grouping);
+          }
+          if (tuple.grouping2 != null) {
+            // Not supported
+          }
+          if (tuple.roundingMode != null) {
+            // Not supported
+          }
+          if (tuple.currencyUsage != null) {
+            // Not supported
+          }
+          if (tuple.minimumExponentDigits != null) {
+            // Not supported
+          }
+          if (tuple.exponentSignAlwaysShown != null) {
+            // Not supported
+          }
+          if (tuple.decimalSeparatorAlwaysShown != null) {
+            fmt.setDecimalSeparatorAlwaysShown(tuple.decimalSeparatorAlwaysShown != 0);
+          }
+          if (tuple.padPosition != null) {
+            // Not supported
+          }
+          if (tuple.positivePrefix != null) {
+            fmt.setPositivePrefix(tuple.positivePrefix);
+          }
+          if (tuple.positiveSuffix != null) {
+            fmt.setPositiveSuffix(tuple.positiveSuffix);
+          }
+          if (tuple.negativePrefix != null) {
+            fmt.setNegativePrefix(tuple.negativePrefix);
+          }
+          if (tuple.negativeSuffix != null) {
+            fmt.setNegativeSuffix(tuple.negativeSuffix);
+          }
+          if (tuple.localizedPattern != null) {
+            fmt.applyLocalizedPattern(tuple.localizedPattern);
+          }
+
+          // lenient parsing not supported by JDK
+          if (tuple.parseIntegerOnly != null) {
+            fmt.setParseIntegerOnly(tuple.parseIntegerOnly != 0);
+          }
+          if (tuple.parseCaseSensitive != null) {
+            // Not supported.
+          }
+          if (tuple.decimalPatternMatchRequired != null) {
+            // Oops, not supported
+          }
+          if (tuple.parseNoExponent != null) {
+            // Oops, not supported for now
+          }
+        }
+      };
+
+  @Test
+  public void TestDataDrivenICU58() {
+    DataDrivenNumberFormatTestUtility.runFormatSuiteIncludingKnownFailures(
+        "numberformattestspecification.txt", ICU58);
+  }
+
+  @Test
+  public void TestDataDrivenJDK() {
+    DataDrivenNumberFormatTestUtility.runFormatSuiteIncludingKnownFailures(
+        "numberformattestspecification.txt", JDK);
+  }
+
+  @Test
+  public void TestDataDrivenShane() {
+    ShanesDataDrivenTester.run();
+  }
+}
index 6b9aa25988b3eeeae8ab243f8c86bd75b88488e3..4a983398051dc6607f0fd10163e1bd5ad7950f7e 100644 (file)
@@ -7,7 +7,7 @@
  *******************************************************************************
  */
 
-/** 
+/**
  * Port From:   ICU4C v1.8.1 : format : NumberFormatRegressionTest
  * Source File: $ICU4CRoot/source/test/intltest/numrgts.cpp
  **/
@@ -31,7 +31,7 @@ import com.ibm.icu.text.NumberFormat;
 import com.ibm.icu.util.Calendar;
 import com.ibm.icu.util.ULocale;
 
-/** 
+/**
  * Performs regression test for MessageFormat
  **/
 public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk {
@@ -51,26 +51,26 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk {
             errln("FAIL");
         }
     }
-    
+
     /**
      * DateFormat should call setIntegerParseOnly(TRUE) on adopted
      * NumberFormat objects.
      */
     @Test
     public void TestJ691() {
-        
+
         Locale loc = new Locale("fr", "CH");
-    
+
         // set up the input date string & expected output
         String udt = "11.10.2000";
         String exp = "11.10.00";
-    
+
         // create a Calendar for this locale
         Calendar cal = Calendar.getInstance(loc);
-    
+
         // create a NumberFormat for this locale
         NumberFormat nf = NumberFormat.getInstance(loc);
-    
+
         // *** Here's the key: We don't want to have to do THIS:
         //nf.setParseIntegerOnly(true);
         // However with changes to fr_CH per cldrbug:9370 we have to do the following:
@@ -78,10 +78,10 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk {
     
         // create the DateFormat
         DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, loc);
-    
+
         df.setCalendar(cal);
         df.setNumberFormat(nf);
-    
+
         // set parsing to lenient & parse
         Date ulocdat = new Date();
         df.setLenient(true);
@@ -92,32 +92,32 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk {
         }
         // format back to a string
         String outString = df.format(ulocdat);
-    
+
         if (!outString.equals(exp)) {
             errln("FAIL: " + udt + " => " + outString);
         }
     }
-    
+
     /**
      * Test getIntegerInstance();
      */
     @Test
     public void Test4408066() {
-        
+
         NumberFormat nf1 = NumberFormat.getIntegerInstance();
         NumberFormat nf2 = NumberFormat.getIntegerInstance(Locale.CHINA);
-    
+
         //test isParseIntegerOnly
         if (!nf1.isParseIntegerOnly() || !nf2.isParseIntegerOnly()) {
             errln("Failed : Integer Number Format Instance should set setParseIntegerOnly(true)");
         }
-    
+
         //Test format
         {
             double[] data = {
-                -3.75, -2.5, -1.5, 
-                -1.25, 0,    1.0, 
-                1.25,  1.5,  2.5, 
+                -3.75, -2.5, -1.5,
+                -1.25, 0,    1.0,
+                1.25,  1.5,  2.5,
                 3.75,  10.0, 255.5
                 };
             String[] expected = {
@@ -126,11 +126,11 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk {
                 "1",  "2",  "2",
                 "4",  "10", "256"
                 };
-    
+
             for (int i = 0; i < data.length; ++i) {
                 String result = nf1.format(data[i]);
                 if (!result.equals(expected[i])) {
-                    errln("Failed => Source: " + Double.toString(data[i]) 
+                    errln("Failed => Source: " + Double.toString(data[i])
                         + ";Formatted : " + result
                         + ";but expectted: " + expected[i]);
                 }
@@ -139,9 +139,9 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk {
         //Test parse, Parsing should stop at "."
         {
             String data[] = {
-                "-3.75", "-2.5", "-1.5", 
-                "-1.25", "0",    "1.0", 
-                "1.25",  "1.5",  "2.5", 
+                "-3.75", "-2.5", "-1.5",
+                "-1.25", "0",    "1.0",
+                "1.25",  "1.5",  "2.5",
                 "3.75",  "10.0", "255.5"
                 };
             long[] expected = {
@@ -150,7 +150,7 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk {
                 1,  1,  2,
                 3,  10, 255
                 };
-            
+
             for (int i = 0; i < data.length; ++i) {
                 Number n = null;
                 try {
@@ -162,31 +162,27 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk {
                     errln("Failed: Integer Number Format should parse string to Long/Integer");
                 }
                 if (n.longValue() != expected[i]) {
-                    errln("Failed=> Source: " + data[i] 
+                    errln("Failed=> Source: " + data[i]
                         + ";result : " + n.toString()
                         + ";expected :" + Long.toString(expected[i]));
                 }
             }
         }
     }
-    
+
     //Test New serialized DecimalFormat(2.0) read old serialized forms of DecimalFormat(1.3.1.1)
     @Test
     public void TestSerialization() throws IOException{
         byte[][] contents = NumberFormatSerialTestData.getContent();
         double data = 1234.56;
         String[] expected = {
-            "1,234.56", "$1,234.56", "123,456%", "1.23456E3"};
+            "1,234.56", "$1,234.56", "1.23456E3", "1,234.56"};
         for (int i = 0; i < 4; ++i) {
             ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(contents[i]));
             try {
                 NumberFormat format = (NumberFormat) ois.readObject();
                 String result = format.format(data);
-                if (result.equals(expected[i])) {
-                    logln("OK: Deserialized bogus NumberFormat(new version read old version)");
-                } else {
-                    errln("FAIL: the test data formats are not euqal");
-                }
+                assertEquals("Deserialization new version should read old version", expected[i], result);
             } catch (Exception e) {
                 warnln("FAIL: " + e.getMessage());
             }
@@ -284,7 +280,7 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk {
             try {
                 Number n = nfmt.parse(data[i]);
                 if (expected[i] != n.doubleValue()) {
-                    errln("Failed: Parsed result for " + data[i] + ": " 
+                    errln("Failed: Parsed result for " + data[i] + ": "
                             + n.doubleValue() + " / expected: " + expected[i]);
                 }
             } catch (ParseException pe) {
@@ -295,8 +291,8 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk {
     @Test
     public void TestSurrogatesParsing() { // Test parsing of numbers that use digits from the supplemental planes.
         final String[] data = {
-                "1\ud801\udca2,3\ud801\udca45.67", // 
-                "\ud801\udca1\ud801\udca2,\ud801\udca3\ud801\udca4\ud801\udca5.\ud801\udca6\ud801\udca7\ud801\udca8", // 
+                "1\ud801\udca2,3\ud801\udca45.67", //
+                "\ud801\udca1\ud801\udca2,\ud801\udca3\ud801\udca4\ud801\udca5.\ud801\udca6\ud801\udca7\ud801\udca8", //
                 "\ud835\udfd2.\ud835\udfd7E-\ud835\udfd1",
                 "\ud835\udfd3.8E-0\ud835\udfd0"
                 };
@@ -313,7 +309,7 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk {
             try {
                 Number n = nfmt.parse(data[i]);
                 if (expected[i] != n.doubleValue()) {
-                    errln("Failed: Parsed result for " + data[i] + ": " 
+                    errln("Failed: Parsed result for " + data[i] + ": "
                             + n.doubleValue() + " / expected: " + expected[i]);
                 }
             } catch (ParseException pe) {
@@ -324,7 +320,7 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk {
 
     void checkNBSPPatternRtNum(String testcase, NumberFormat nf, double myNumber) {
         String myString = nf.format(myNumber);
-        
+
             double aNumber;
             try {
                 aNumber = nf.parse(myString).doubleValue();
@@ -349,19 +345,19 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk {
     public void TestNBSPInPattern() {
     NumberFormat nf = null;
     String testcase;
-    
-    
+
+
     testcase="ar_AE UNUM_CURRENCY";
     nf = NumberFormat.getCurrencyInstance(new ULocale("ar_AE"));
     checkNBSPPatternRT(testcase, nf);
-    // if we don't have CLDR 1.6 data, bring out the problem anyways 
-    
+    // if we don't have CLDR 1.6 data, bring out the problem anyways
+
     String SPECIAL_PATTERN = "\u00A4\u00A4'\u062f.\u0625.\u200f\u00a0'###0.00";
     testcase = "ar_AE special pattern: " + SPECIAL_PATTERN;
     nf = new DecimalFormat();
     ((DecimalFormat)nf).applyPattern(SPECIAL_PATTERN);
     checkNBSPPatternRT(testcase, nf);
-    
+
     }
 
     /*
@@ -385,4 +381,14 @@ public class NumberFormatRegressionTest extends com.ibm.icu.dev.test.TestFmwk {
             errln("FAIL: Parsed result: " + num + " - expected: " + val);
         }
     }
+
+    @Test
+    public void TestAffixesNoCurrency() {
+        ULocale locale = new ULocale("en");
+        DecimalFormat nf = (DecimalFormat) NumberFormat.getInstance(locale, NumberFormat.PLURALCURRENCYSTYLE);
+        assertEquals(
+            "Positive suffix should contain the single currency sign when no currency is set",
+            " \u00A4",
+            nf.getPositiveSuffix());
+    }
 }
index 95680965dc80dc50c62cad514785b0f193d3baad..ff180aab78c9bf927182a029a60c0382870c1f1d 100644 (file)
@@ -12,7 +12,7 @@ package com.ibm.icu.dev.test.format;
 public class NumberFormatSerialTestData {
     //get Content
     public static byte[][] getContent() {
-            return content;
+        return content;
     }
 
     //NumberFormat.getInstance(Locale.US)
@@ -160,78 +160,6 @@ public class NumberFormatSerialTestData {
         
     };
 
-    //NumberFormat.getPercentInstance(Locale.US)
-    static byte[] percentInstance = new byte[]{ 
-        -84, -19, 0, 5, 115, 114, 0, 30, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 
-        116, 101, 120, 116, 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 11, -1, 
-        3, 98, -40, 114, 48, 58, 2, 0, 22, 90, 0, 27, 100, 101, 99, 105, 109, 97, 108, 83, 
-        101, 112, 97, 114, 97, 116, 111, 114, 65, 108, 119, 97, 121, 115, 83, 104, 111, 119, 110, 90, 
-        0, 23, 101, 120, 112, 111, 110, 101, 110, 116, 83, 105, 103, 110, 65, 108, 119, 97, 121, 115, 
-        83, 104, 111, 119, 110, 73, 0, 11, 102, 111, 114, 109, 97, 116, 87, 105, 100, 116, 104, 66, 
-        0, 12, 103, 114, 111, 117, 112, 105, 110, 103, 83, 105, 122, 101, 66, 0, 13, 103, 114, 111, 
-        117, 112, 105, 110, 103, 83, 105, 122, 101, 50, 66, 0, 17, 109, 105, 110, 69, 120, 112, 111, 
-        110, 101, 110, 116, 68, 105, 103, 105, 116, 115, 73, 0, 10, 109, 117, 108, 116, 105, 112, 108, 
-        105, 101, 114, 67, 0, 3, 112, 97, 100, 73, 0, 11, 112, 97, 100, 80, 111, 115, 105, 116, 
-        105, 111, 110, 73, 0, 12, 114, 111, 117, 110, 100, 105, 110, 103, 77, 111, 100, 101, 73, 0, 
-        21, 115, 101, 114, 105, 97, 108, 86, 101, 114, 115, 105, 111, 110, 79, 110, 83, 116, 114, 101, 
-        97, 109, 90, 0, 22, 117, 115, 101, 69, 120, 112, 111, 110, 101, 110, 116, 105, 97, 108, 78, 
-        111, 116, 97, 116, 105, 111, 110, 76, 0, 16, 110, 101, 103, 80, 114, 101, 102, 105, 120, 80, 
-        97, 116, 116, 101, 114, 110, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 
-        83, 116, 114, 105, 110, 103, 59, 76, 0, 16, 110, 101, 103, 83, 117, 102, 102, 105, 120, 80, 
-        97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 1, 76, 0, 14, 110, 101, 103, 97, 116, 105, 
-        118, 101, 80, 114, 101, 102, 105, 120, 113, 0, 126, 0, 1, 76, 0, 14, 110, 101, 103, 97, 
-        116, 105, 118, 101, 83, 117, 102, 102, 105, 120, 113, 0, 126, 0, 1, 76, 0, 16, 112, 111, 
-        115, 80, 114, 101, 102, 105, 120, 80, 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 1, 76, 
-        0, 16, 112, 111, 115, 83, 117, 102, 102, 105, 120, 80, 97, 116, 116, 101, 114, 110, 113, 0, 
-        126, 0, 1, 76, 0, 14, 112, 111, 115, 105, 116, 105, 118, 101, 80, 114, 101, 102, 105, 120, 
-        113, 0, 126, 0, 1, 76, 0, 14, 112, 111, 115, 105, 116, 105, 118, 101, 83, 117, 102, 102, 
-        105, 120, 113, 0, 126, 0, 1, 76, 0, 17, 114, 111, 117, 110, 100, 105, 110, 103, 73, 110, 
-        99, 114, 101, 109, 101, 110, 116, 116, 0, 22, 76, 106, 97, 118, 97, 47, 109, 97, 116, 104, 
-        47, 66, 105, 103, 68, 101, 99, 105, 109, 97, 108, 59, 76, 0, 7, 115, 121, 109, 98, 111, 
-        108, 115, 116, 0, 39, 76, 99, 111, 109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 116, 101, 
-        120, 116, 47, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 83, 121, 109, 98, 
-        111, 108, 115, 59, 120, 114, 0, 29, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 
-        116, 101, 120, 116, 46, 78, 117, 109, 98, 101, 114, 70, 111, 114, 109, 97, 116, -33, -10, -77, 
-        -65, 19, 125, 7, -24, 3, 0, 11, 90, 0, 12, 103, 114, 111, 117, 112, 105, 110, 103, 85, 
-        115, 101, 100, 66, 0, 17, 109, 97, 120, 70, 114, 97, 99, 116, 105, 111, 110, 68, 105, 103, 
-        105, 116, 115, 66, 0, 16, 109, 97, 120, 73, 110, 116, 101, 103, 101, 114, 68, 105, 103, 105, 
-        116, 115, 73, 0, 21, 109, 97, 120, 105, 109, 117, 109, 70, 114, 97, 99, 116, 105, 111, 110, 
-        68, 105, 103, 105, 116, 115, 73, 0, 20, 109, 97, 120, 105, 109, 117, 109, 73, 110, 116, 101, 
-        103, 101, 114, 68, 105, 103, 105, 116, 115, 66, 0, 17, 109, 105, 110, 70, 114, 97, 99, 116, 
-        105, 111, 110, 68, 105, 103, 105, 116, 115, 66, 0, 16, 109, 105, 110, 73, 110, 116, 101, 103, 
-        101, 114, 68, 105, 103, 105, 116, 115, 73, 0, 21, 109, 105, 110, 105, 109, 117, 109, 70, 114, 
-        97, 99, 116, 105, 111, 110, 68, 105, 103, 105, 116, 115, 73, 0, 20, 109, 105, 110, 105, 109, 
-        117, 109, 73, 110, 116, 101, 103, 101, 114, 68, 105, 103, 105, 116, 115, 90, 0, 16, 112, 97, 
-        114, 115, 101, 73, 110, 116, 101, 103, 101, 114, 79, 110, 108, 121, 73, 0, 21, 115, 101, 114, 
-        105, 97, 108, 86, 101, 114, 115, 105, 111, 110, 79, 110, 83, 116, 114, 101, 97, 109, 120, 114, 
-        0, 16, 106, 97, 118, 97, 46, 116, 101, 120, 116, 46, 70, 111, 114, 109, 97, 116, -5, -40, 
-        -68, 18, -23, 15, 24, 67, 2, 0, 0, 120, 112, 1, 0, 127, 0, 0, 0, 0, 0, 0, 
-        1, 53, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 120, 0, 0, 
-        0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 100, 0, 32, 0, 0, 0, 0, 0, 0, 0, 
-        6, 0, 0, 0, 2, 0, 116, 0, 1, 45, 116, 0, 1, 37, 116, 0, 1, 45, 116, 0, 
-        1, 37, 116, 0, 0, 113, 0, 126, 0, 8, 116, 0, 0, 116, 0, 1, 37, 112, 115, 114, 
-        0, 37, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, 101, 120, 116, 46, 68, 
-        101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 83, 121, 109, 98, 111, 108, 115, 80, 
-        29, 23, -103, 8, 104, -109, -100, 2, 0, 18, 67, 0, 16, 100, 101, 99, 105, 109, 97, 108, 
-        83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 5, 100, 105, 103, 105, 116, 67, 0, 11, 
-        101, 120, 112, 111, 110, 101, 110, 116, 105, 97, 108, 67, 0, 17, 103, 114, 111, 117, 112, 105, 
-        110, 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 9, 109, 105, 110, 117, 115, 83, 
-        105, 103, 110, 67, 0, 17, 109, 111, 110, 101, 116, 97, 114, 121, 83, 101, 112, 97, 114, 97, 
-        116, 111, 114, 67, 0, 9, 112, 97, 100, 69, 115, 99, 97, 112, 101, 67, 0, 16, 112, 97, 
-        116, 116, 101, 114, 110, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 7, 112, 101, 114, 
-        77, 105, 108, 108, 67, 0, 7, 112, 101, 114, 99, 101, 110, 116, 67, 0, 8, 112, 108, 117, 
-        115, 83, 105, 103, 110, 73, 0, 21, 115, 101, 114, 105, 97, 108, 86, 101, 114, 115, 105, 111, 
-        110, 79, 110, 83, 116, 114, 101, 97, 109, 67, 0, 9, 122, 101, 114, 111, 68, 105, 103, 105, 
-        116, 76, 0, 3, 78, 97, 78, 113, 0, 126, 0, 1, 76, 0, 14, 99, 117, 114, 114, 101, 
-        110, 99, 121, 83, 121, 109, 98, 111, 108, 113, 0, 126, 0, 1, 76, 0, 17, 101, 120, 112, 
-        111, 110, 101, 110, 116, 83, 101, 112, 97, 114, 97, 116, 111, 114, 113, 0, 126, 0, 1, 76, 
-        0, 8, 105, 110, 102, 105, 110, 105, 116, 121, 113, 0, 126, 0, 1, 76, 0, 18, 105, 110, 
-        116, 108, 67, 117, 114, 114, 101, 110, 99, 121, 83, 121, 109, 98, 111, 108, 113, 0, 126, 0, 
-        1, 120, 112, 0, 46, 0, 35, 0, 0, 0, 44, 0, 45, 0, 46, 0, 42, 0, 59, 32, 
-        48, 0, 37, 0, 43, 0, 0, 0, 2, 0, 48, 116, 0, 3, -17, -65, -67, 116, 0, 1, 
-        36, 116, 0, 1, 69, 116, 0, 3, -30, -120, -98, 116, 0, 3, 85, 83, 68, 
-    };
-
     //NumberFormat.getScientificInstance(Locale.US)
     static byte[] scientificInstance = new byte[]{ 
         -84, -19, 0, 5, 115, 114, 0, 30, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 
@@ -304,5 +232,291 @@ public class NumberFormatSerialTestData {
         1, 69, 116, 0, 3, -30, -120, -98, 116, 0, 3, 85, 83, 68, 
     };
 
-    final static byte[][] content = {generalInstance, currencyInstance, percentInstance, scientificInstance};
+    static byte[] icu58Latest = {
+        -84, -19, 0, 5, 115, 114, 0, 30, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, 101,
+        120, 116, 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 11, -1, 3, 98, -40,
+        114, 48, 58, 3, 0, 36, 73, 0, 18, 80, 65, 82, 83, 69, 95, 77, 65, 88, 95, 69, 88, 80, 79, 78,
+        69, 78, 84, 73, 0, 17, 99, 117, 114, 114, 101, 110, 99, 121, 83, 105, 103, 110, 67, 111, 117,
+        110, 116, 90, 0, 27, 100, 101, 99, 105, 109, 97, 108, 83, 101, 112, 97, 114, 97, 116, 111, 114,
+        65, 108, 119, 97, 121, 115, 83, 104, 111, 119, 110, 90, 0, 23, 101, 120, 112, 111, 110, 101,
+        110, 116, 83, 105, 103, 110, 65, 108, 119, 97, 121, 115, 83, 104, 111, 119, 110, 73, 0, 11, 102,
+        111, 114, 109, 97, 116, 87, 105, 100, 116, 104, 66, 0, 12, 103, 114, 111, 117, 112, 105, 110,
+        103, 83, 105, 122, 101, 66, 0, 13, 103, 114, 111, 117, 112, 105, 110, 103, 83, 105, 122, 101,
+        50, 73, 0, 20, 109, 97, 120, 83, 105, 103, 110, 105, 102, 105, 99, 97, 110, 116, 68, 105, 103,
+        105, 116, 115, 66, 0, 17, 109, 105, 110, 69, 120, 112, 111, 110, 101, 110, 116, 68, 105, 103,
+        105, 116, 115, 73, 0, 20, 109, 105, 110, 83, 105, 103, 110, 105, 102, 105, 99, 97, 110, 116, 68,
+        105, 103, 105, 116, 115, 73, 0, 10, 109, 117, 108, 116, 105, 112, 108, 105, 101, 114, 67, 0, 3,
+        112, 97, 100, 73, 0, 11, 112, 97, 100, 80, 111, 115, 105, 116, 105, 111, 110, 90, 0, 15, 112,
+        97, 114, 115, 101, 66, 105, 103, 68, 101, 99, 105, 109, 97, 108, 90, 0, 24, 112, 97, 114, 115,
+        101, 82, 101, 113, 117, 105, 114, 101, 68, 101, 99, 105, 109, 97, 108, 80, 111, 105, 110, 116,
+        73, 0, 12, 114, 111, 117, 110, 100, 105, 110, 103, 77, 111, 100, 101, 73, 0, 21, 115, 101, 114,
+        105, 97, 108, 86, 101, 114, 115, 105, 111, 110, 79, 110, 83, 116, 114, 101, 97, 109, 73, 0, 5,
+        115, 116, 121, 108, 101, 90, 0, 22, 117, 115, 101, 69, 120, 112, 111, 110, 101, 110, 116, 105,
+        97, 108, 78, 111, 116, 97, 116, 105, 111, 110, 90, 0, 20, 117, 115, 101, 83, 105, 103, 110, 105,
+        102, 105, 99, 97, 110, 116, 68, 105, 103, 105, 116, 115, 76, 0, 10, 97, 116, 116, 114, 105, 98,
+        117, 116, 101, 115, 116, 0, 21, 76, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 65, 114, 114,
+        97, 121, 76, 105, 115, 116, 59, 76, 0, 14, 99, 117, 114, 114, 101, 110, 99, 121, 67, 104, 111,
+        105, 99, 101, 116, 0, 24, 76, 106, 97, 118, 97, 47, 116, 101, 120, 116, 47, 67, 104, 111, 105,
+        99, 101, 70, 111, 114, 109, 97, 116, 59, 76, 0, 18, 99, 117, 114, 114, 101, 110, 99, 121, 80,
+        108, 117, 114, 97, 108, 73, 110, 102, 111, 116, 0, 37, 76, 99, 111, 109, 47, 105, 98, 109, 47,
+        105, 99, 117, 47, 116, 101, 120, 116, 47, 67, 117, 114, 114, 101, 110, 99, 121, 80, 108, 117,
+        114, 97, 108, 73, 110, 102, 111, 59, 76, 0, 13, 99, 117, 114, 114, 101, 110, 99, 121, 85, 115,
+        97, 103, 101, 116, 0, 41, 76, 99, 111, 109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 117, 116,
+        105, 108, 47, 67, 117, 114, 114, 101, 110, 99, 121, 36, 67, 117, 114, 114, 101, 110, 99, 121,
+        85, 115, 97, 103, 101, 59, 76, 0, 13, 102, 111, 114, 109, 97, 116, 80, 97, 116, 116, 101, 114,
+        110, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103,
+        59, 76, 0, 11, 109, 97, 116, 104, 67, 111, 110, 116, 101, 120, 116, 116, 0, 30, 76, 99, 111,
+        109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 109, 97, 116, 104, 47, 77, 97, 116, 104, 67, 111,
+        110, 116, 101, 120, 116, 59, 76, 0, 16, 110, 101, 103, 80, 114, 101, 102, 105, 120, 80, 97, 116,
+        116, 101, 114, 110, 113, 0, 126, 0, 5, 76, 0, 16, 110, 101, 103, 83, 117, 102, 102, 105, 120,
+        80, 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 5, 76, 0, 14, 110, 101, 103, 97, 116, 105, 118,
+        101, 80, 114, 101, 102, 105, 120, 113, 0, 126, 0, 5, 76, 0, 14, 110, 101, 103, 97, 116, 105,
+        118, 101, 83, 117, 102, 102, 105, 120, 113, 0, 126, 0, 5, 76, 0, 16, 112, 111, 115, 80, 114,
+        101, 102, 105, 120, 80, 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 5, 76, 0, 16, 112, 111,
+        115, 83, 117, 102, 102, 105, 120, 80, 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 5, 76, 0, 14,
+        112, 111, 115, 105, 116, 105, 118, 101, 80, 114, 101, 102, 105, 120, 113, 0, 126, 0, 5, 76, 0,
+        14, 112, 111, 115, 105, 116, 105, 118, 101, 83, 117, 102, 102, 105, 120, 113, 0, 126, 0, 5, 76,
+        0, 17, 114, 111, 117, 110, 100, 105, 110, 103, 73, 110, 99, 114, 101, 109, 101, 110, 116, 116,
+        0, 22, 76, 106, 97, 118, 97, 47, 109, 97, 116, 104, 47, 66, 105, 103, 68, 101, 99, 105, 109, 97,
+        108, 59, 76, 0, 7, 115, 121, 109, 98, 111, 108, 115, 116, 0, 39, 76, 99, 111, 109, 47, 105, 98,
+        109, 47, 105, 99, 117, 47, 116, 101, 120, 116, 47, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114,
+        109, 97, 116, 83, 121, 109, 98, 111, 108, 115, 59, 120, 114, 0, 29, 99, 111, 109, 46, 105, 98,
+        109, 46, 105, 99, 117, 46, 116, 101, 120, 116, 46, 78, 117, 109, 98, 101, 114, 70, 111, 114,
+        109, 97, 116, -33, -10, -77, -65, 19, 125, 7, -24, 3, 0, 14, 90, 0, 12, 103, 114, 111, 117, 112,
+        105, 110, 103, 85, 115, 101, 100, 66, 0, 17, 109, 97, 120, 70, 114, 97, 99, 116, 105, 111, 110,
+        68, 105, 103, 105, 116, 115, 66, 0, 16, 109, 97, 120, 73, 110, 116, 101, 103, 101, 114, 68, 105,
+        103, 105, 116, 115, 73, 0, 21, 109, 97, 120, 105, 109, 117, 109, 70, 114, 97, 99, 116, 105, 111,
+        110, 68, 105, 103, 105, 116, 115, 73, 0, 20, 109, 97, 120, 105, 109, 117, 109, 73, 110, 116,
+        101, 103, 101, 114, 68, 105, 103, 105, 116, 115, 66, 0, 17, 109, 105, 110, 70, 114, 97, 99, 116,
+        105, 111, 110, 68, 105, 103, 105, 116, 115, 66, 0, 16, 109, 105, 110, 73, 110, 116, 101, 103,
+        101, 114, 68, 105, 103, 105, 116, 115, 73, 0, 21, 109, 105, 110, 105, 109, 117, 109, 70, 114,
+        97, 99, 116, 105, 111, 110, 68, 105, 103, 105, 116, 115, 73, 0, 20, 109, 105, 110, 105, 109,
+        117, 109, 73, 110, 116, 101, 103, 101, 114, 68, 105, 103, 105, 116, 115, 90, 0, 16, 112, 97,
+        114, 115, 101, 73, 110, 116, 101, 103, 101, 114, 79, 110, 108, 121, 90, 0, 11, 112, 97, 114,
+        115, 101, 83, 116, 114, 105, 99, 116, 73, 0, 21, 115, 101, 114, 105, 97, 108, 86, 101, 114, 115,
+        105, 111, 110, 79, 110, 83, 116, 114, 101, 97, 109, 76, 0, 21, 99, 97, 112, 105, 116, 97, 108,
+        105, 122, 97, 116, 105, 111, 110, 83, 101, 116, 116, 105, 110, 103, 116, 0, 33, 76, 99, 111,
+        109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 116, 101, 120, 116, 47, 68, 105, 115, 112, 108, 97,
+        121, 67, 111, 110, 116, 101, 120, 116, 59, 76, 0, 8, 99, 117, 114, 114, 101, 110, 99, 121, 116,
+        0, 27, 76, 99, 111, 109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 117, 116, 105, 108, 47, 67,
+        117, 114, 114, 101, 110, 99, 121, 59, 120, 114, 0, 24, 99, 111, 109, 46, 105, 98, 109, 46, 105,
+        99, 117, 46, 116, 101, 120, 116, 46, 85, 70, 111, 114, 109, 97, 116, -69, 26, -15, 32, -39, 7,
+        93, -64, 2, 0, 2, 76, 0, 12, 97, 99, 116, 117, 97, 108, 76, 111, 99, 97, 108, 101, 116, 0, 26,
+        76, 99, 111, 109, 47, 105, 98, 109, 47, 105, 99, 117, 47, 117, 116, 105, 108, 47, 85, 76, 111,
+        99, 97, 108, 101, 59, 76, 0, 11, 118, 97, 108, 105, 100, 76, 111, 99, 97, 108, 101, 113, 0, 126,
+        0, 13, 120, 114, 0, 16, 106, 97, 118, 97, 46, 116, 101, 120, 116, 46, 70, 111, 114, 109, 97,
+        116, -5, -40, -68, 18, -23, 15, 24, 67, 2, 0, 0, 120, 112, 112, 112, 1, 3, 127, 0, 0, 0, 3, 0,
+        0, 1, 53, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2, 126, 114, 0, 31, 99, 111, 109, 46,
+        105, 98, 109, 46, 105, 99, 117, 46, 116, 101, 120, 116, 46, 68, 105, 115, 112, 108, 97, 121, 67,
+        111, 110, 116, 101, 120, 116, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 120, 114, 0, 14, 106, 97, 118,
+        97, 46, 108, 97, 110, 103, 46, 69, 110, 117, 109, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 120, 112,
+        116, 0, 19, 67, 65, 80, 73, 84, 65, 76, 73, 90, 65, 84, 73, 79, 78, 95, 78, 79, 78, 69, 115,
+        114, 0, 45, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 117, 116, 105, 108, 46, 77,
+        101, 97, 115, 117, 114, 101, 85, 110, 105, 116, 36, 77, 101, 97, 115, 117, 114, 101, 85, 110,
+        105, 116, 80, 114, 111, 120, 121, -55, -70, 119, -8, -15, 121, 121, -30, 12, 0, 0, 120, 112,
+        119, 18, 0, 0, 8, 99, 117, 114, 114, 101, 110, 99, 121, 0, 3, 85, 83, 68, 0, 0, 120, 120, 0, 0,
+        3, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 6, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 32, 0, 0,
+        0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 115, 114, 0, 19, 106, 97, 118, 97, 46,
+        117, 116, 105, 108, 46, 65, 114, 114, 97, 121, 76, 105, 115, 116, 120, -127, -46, 29, -103, -57,
+        97, -99, 3, 0, 1, 73, 0, 4, 115, 105, 122, 101, 120, 112, 0, 0, 0, 0, 119, 4, 0, 0, 0, 0, 120,
+        112, 112, 126, 114, 0, 39, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 117, 116, 105,
+        108, 46, 67, 117, 114, 114, 101, 110, 99, 121, 36, 67, 117, 114, 114, 101, 110, 99, 121, 85,
+        115, 97, 103, 101, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 120, 113, 0, 126, 0, 17, 116, 0, 8, 83, 84,
+        65, 78, 68, 65, 82, 68, 116, 0, 9, 35, 44, 35, 35, 48, 46, 35, 35, 35, 115, 114, 0, 28, 99, 111,
+        109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 109, 97, 116, 104, 46, 77, 97, 116, 104, 67, 111,
+        110, 116, 101, 120, 116, 99, 105, 109, 109, 99, 49, 48, 48, 2, 0, 4, 73, 0, 6, 100, 105, 103,
+        105, 116, 115, 73, 0, 4, 102, 111, 114, 109, 90, 0, 10, 108, 111, 115, 116, 68, 105, 103, 105,
+        116, 115, 73, 0, 12, 114, 111, 117, 110, 100, 105, 110, 103, 77, 111, 100, 101, 120, 112, 0, 0,
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 116, 0, 1, 45, 116, 0, 0, 116, 0, 1, 45, 116, 0, 0, 116, 0, 0,
+        113, 0, 126, 0, 31, 116, 0, 0, 116, 0, 0, 112, 115, 114, 0, 37, 99, 111, 109, 46, 105, 98, 109,
+        46, 105, 99, 117, 46, 116, 101, 120, 116, 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109,
+        97, 116, 83, 121, 109, 98, 111, 108, 115, 80, 29, 23, -103, 8, 104, -109, -100, 2, 0, 38, 67, 0,
+        16, 100, 101, 99, 105, 109, 97, 108, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 5, 100,
+        105, 103, 105, 116, 67, 0, 11, 101, 120, 112, 111, 110, 101, 110, 116, 105, 97, 108, 67, 0, 17,
+        103, 114, 111, 117, 112, 105, 110, 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 9, 109,
+        105, 110, 117, 115, 83, 105, 103, 110, 67, 0, 25, 109, 111, 110, 101, 116, 97, 114, 121, 71,
+        114, 111, 117, 112, 105, 110, 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 17, 109,
+        111, 110, 101, 116, 97, 114, 121, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 9, 112, 97,
+        100, 69, 115, 99, 97, 112, 101, 67, 0, 16, 112, 97, 116, 116, 101, 114, 110, 83, 101, 112, 97,
+        114, 97, 116, 111, 114, 67, 0, 7, 112, 101, 114, 77, 105, 108, 108, 67, 0, 7, 112, 101, 114, 99,
+        101, 110, 116, 67, 0, 8, 112, 108, 117, 115, 83, 105, 103, 110, 73, 0, 21, 115, 101, 114, 105,
+        97, 108, 86, 101, 114, 115, 105, 111, 110, 79, 110, 83, 116, 114, 101, 97, 109, 67, 0, 8, 115,
+        105, 103, 68, 105, 103, 105, 116, 67, 0, 9, 122, 101, 114, 111, 68, 105, 103, 105, 116, 76, 0,
+        3, 78, 97, 78, 113, 0, 126, 0, 5, 76, 0, 12, 97, 99, 116, 117, 97, 108, 76, 111, 99, 97, 108,
+        101, 113, 0, 126, 0, 13, 76, 0, 15, 99, 117, 114, 114, 101, 110, 99, 121, 80, 97, 116, 116, 101,
+        114, 110, 113, 0, 126, 0, 5, 91, 0, 19, 99, 117, 114, 114, 101, 110, 99, 121, 83, 112, 99, 65,
+        102, 116, 101, 114, 83, 121, 109, 116, 0, 19, 91, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103,
+        47, 83, 116, 114, 105, 110, 103, 59, 91, 0, 20, 99, 117, 114, 114, 101, 110, 99, 121, 83, 112,
+        99, 66, 101, 102, 111, 114, 101, 83, 121, 109, 113, 0, 126, 0, 38, 76, 0, 14, 99, 117, 114, 114,
+        101, 110, 99, 121, 83, 121, 109, 98, 111, 108, 113, 0, 126, 0, 5, 76, 0, 22, 100, 101, 99, 105,
+        109, 97, 108, 83, 101, 112, 97, 114, 97, 116, 111, 114, 83, 116, 114, 105, 110, 103, 113, 0,
+        126, 0, 5, 91, 0, 12, 100, 105, 103, 105, 116, 83, 116, 114, 105, 110, 103, 115, 113, 0, 126, 0,
+        38, 91, 0, 6, 100, 105, 103, 105, 116, 115, 116, 0, 2, 91, 67, 76, 0, 26, 101, 120, 112, 111,
+        110, 101, 110, 116, 77, 117, 108, 116, 105, 112, 108, 105, 99, 97, 116, 105, 111, 110, 83, 105,
+        103, 110, 113, 0, 126, 0, 5, 76, 0, 17, 101, 120, 112, 111, 110, 101, 110, 116, 83, 101, 112,
+        97, 114, 97, 116, 111, 114, 113, 0, 126, 0, 5, 76, 0, 23, 103, 114, 111, 117, 112, 105, 110,
+        103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 5,
+        76, 0, 8, 105, 110, 102, 105, 110, 105, 116, 121, 113, 0, 126, 0, 5, 76, 0, 18, 105, 110, 116,
+        108, 67, 117, 114, 114, 101, 110, 99, 121, 83, 121, 109, 98, 111, 108, 113, 0, 126, 0, 5, 76, 0,
+        11, 109, 105, 110, 117, 115, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 5, 76, 0, 31, 109,
+        111, 110, 101, 116, 97, 114, 121, 71, 114, 111, 117, 112, 105, 110, 103, 83, 101, 112, 97, 114,
+        97, 116, 111, 114, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 5, 76, 0, 23, 109, 111, 110,
+        101, 116, 97, 114, 121, 83, 101, 112, 97, 114, 97, 116, 111, 114, 83, 116, 114, 105, 110, 103,
+        113, 0, 126, 0, 5, 76, 0, 13, 112, 101, 114, 77, 105, 108, 108, 83, 116, 114, 105, 110, 103,
+        113, 0, 126, 0, 5, 76, 0, 13, 112, 101, 114, 99, 101, 110, 116, 83, 116, 114, 105, 110, 103,
+        113, 0, 126, 0, 5, 76, 0, 10, 112, 108, 117, 115, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0,
+        5, 76, 0, 15, 114, 101, 113, 117, 101, 115, 116, 101, 100, 76, 111, 99, 97, 108, 101, 116, 0,
+        18, 76, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 76, 111, 99, 97, 108, 101, 59, 76, 0, 7,
+        117, 108, 111, 99, 97, 108, 101, 113, 0, 126, 0, 13, 76, 0, 11, 118, 97, 108, 105, 100, 76, 111,
+        99, 97, 108, 101, 113, 0, 126, 0, 13, 120, 112, 0, 46, 0, 35, 0, 0, 0, 44, 0, 45, 0, 44, 0, 46,
+        0, 42, 0, 59, 32, 48, 0, 37, 0, 43, 0, 0, 0, 8, 0, 64, 0, 48, 116, 0, 3, 78, 97, 78, 115, 114,
+        0, 24, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 117, 116, 105, 108, 46, 85, 76,
+        111, 99, 97, 108, 101, 51, -114, -10, 104, 70, -48, 11, -31, 2, 0, 1, 76, 0, 8, 108, 111, 99,
+        97, 108, 101, 73, 68, 113, 0, 126, 0, 5, 120, 112, 116, 0, 5, 101, 110, 95, 85, 83, 112, 117,
+        114, 0, 19, 91, 76, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 83, 116, 114, 105, 110, 103,
+        59, -83, -46, 86, -25, -23, 29, 123, 71, 2, 0, 0, 120, 112, 0, 0, 0, 3, 116, 0, 6, 91, 58, 94,
+        83, 58, 93, 116, 0, 9, 91, 58, 100, 105, 103, 105, 116, 58, 93, 116, 0, 2, -62, -96, 117, 113,
+        0, 126, 0, 46, 0, 0, 0, 3, 113, 0, 126, 0, 48, 113, 0, 126, 0, 49, 113, 0, 126, 0, 50, 116, 0,
+        1, 36, 116, 0, 1, 46, 117, 113, 0, 126, 0, 46, 0, 0, 0, 10, 116, 0, 1, 48, 116, 0, 1, 49, 116,
+        0, 1, 50, 116, 0, 1, 51, 116, 0, 1, 52, 116, 0, 1, 53, 116, 0, 1, 54, 116, 0, 1, 55, 116, 0, 1,
+        56, 116, 0, 1, 57, 117, 114, 0, 2, 91, 67, -80, 38, 102, -80, -30, 93, -124, -84, 2, 0, 0, 120,
+        112, 0, 0, 0, 10, 0, 48, 0, 49, 0, 50, 0, 51, 0, 52, 0, 53, 0, 54, 0, 55, 0, 56, 0, 57, 116, 0,
+        2, -61, -105, 116, 0, 1, 69, 116, 0, 1, 44, 116, 0, 3, -30, -120, -98, 116, 0, 3, 85, 83, 68,
+        116, 0, 1, 45, 113, 0, 126, 0, 69, 113, 0, 126, 0, 53, 116, 0, 3, -30, -128, -80, 116, 0, 1, 37,
+        116, 0, 1, 43, 115, 114, 0, 16, 106, 97, 118, 97, 46, 117, 116, 105, 108, 46, 76, 111, 99, 97,
+        108, 101, 126, -8, 17, 96, -100, 48, -7, -20, 3, 0, 6, 73, 0, 8, 104, 97, 115, 104, 99, 111,
+        100, 101, 76, 0, 7, 99, 111, 117, 110, 116, 114, 121, 113, 0, 126, 0, 5, 76, 0, 10, 101, 120,
+        116, 101, 110, 115, 105, 111, 110, 115, 113, 0, 126, 0, 5, 76, 0, 8, 108, 97, 110, 103, 117, 97,
+        103, 101, 113, 0, 126, 0, 5, 76, 0, 6, 115, 99, 114, 105, 112, 116, 113, 0, 126, 0, 5, 76, 0, 7,
+        118, 97, 114, 105, 97, 110, 116, 113, 0, 126, 0, 5, 120, 112, -1, -1, -1, -1, 116, 0, 2, 85, 83,
+        116, 0, 0, 116, 0, 2, 101, 110, 113, 0, 126, 0, 79, 113, 0, 126, 0, 79, 120, 115, 113, 0, 126,
+        0, 43, 113, 0, 126, 0, 45, 113, 0, 126, 0, 44, 120
+    };
+
+    static byte[] newFromPattern = {
+      -84, -19, 0, 5, 115, 114, 0, 30, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, 101,
+      120, 116, 46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 11, -1, 3, 98, -40,
+      114, 48, 58, 3, 0, 1, 73, 0, 18, 105, 99, 117, 77, 97, 116, 104, 67, 111, 110, 116, 101, 120,
+      116, 70, 111, 114, 109, 120, 114, 0, 29, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46,
+      116, 101, 120, 116, 46, 78, 117, 109, 98, 101, 114, 70, 111, 114, 109, 97, 116, -33, -10, -77,
+      -65, 19, 125, 7, -24, 3, 0, 14, 90, 0, 12, 103, 114, 111, 117, 112, 105, 110, 103, 85, 115, 101,
+      100, 66, 0, 17, 109, 97, 120, 70, 114, 97, 99, 116, 105, 111, 110, 68, 105, 103, 105, 116, 115,
+      66, 0, 16, 109, 97, 120, 73, 110, 116, 101, 103, 101, 114, 68, 105, 103, 105, 116, 115, 73, 0,
+      21, 109, 97, 120, 105, 109, 117, 109, 70, 114, 97, 99, 116, 105, 111, 110, 68, 105, 103, 105,
+      116, 115, 73, 0, 20, 109, 97, 120, 105, 109, 117, 109, 73, 110, 116, 101, 103, 101, 114, 68,
+      105, 103, 105, 116, 115, 66, 0, 17, 109, 105, 110, 70, 114, 97, 99, 116, 105, 111, 110, 68, 105,
+      103, 105, 116, 115, 66, 0, 16, 109, 105, 110, 73, 110, 116, 101, 103, 101, 114, 68, 105, 103,
+      105, 116, 115, 73, 0, 21, 109, 105, 110, 105, 109, 117, 109, 70, 114, 97, 99, 116, 105, 111,
+      110, 68, 105, 103, 105, 116, 115, 73, 0, 20, 109, 105, 110, 105, 109, 117, 109, 73, 110, 116,
+      101, 103, 101, 114, 68, 105, 103, 105, 116, 115, 90, 0, 16, 112, 97, 114, 115, 101, 73, 110,
+      116, 101, 103, 101, 114, 79, 110, 108, 121, 90, 0, 11, 112, 97, 114, 115, 101, 83, 116, 114,
+      105, 99, 116, 73, 0, 21, 115, 101, 114, 105, 97, 108, 86, 101, 114, 115, 105, 111, 110, 79, 110,
+      83, 116, 114, 101, 97, 109, 76, 0, 21, 99, 97, 112, 105, 116, 97, 108, 105, 122, 97, 116, 105,
+      111, 110, 83, 101, 116, 116, 105, 110, 103, 116, 0, 33, 76, 99, 111, 109, 47, 105, 98, 109, 47,
+      105, 99, 117, 47, 116, 101, 120, 116, 47, 68, 105, 115, 112, 108, 97, 121, 67, 111, 110, 116,
+      101, 120, 116, 59, 76, 0, 8, 99, 117, 114, 114, 101, 110, 99, 121, 116, 0, 27, 76, 99, 111, 109,
+      47, 105, 98, 109, 47, 105, 99, 117, 47, 117, 116, 105, 108, 47, 67, 117, 114, 114, 101, 110, 99,
+      121, 59, 120, 114, 0, 24, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, 101, 120,
+      116, 46, 85, 70, 111, 114, 109, 97, 116, -69, 26, -15, 32, -39, 7, 93, -64, 2, 0, 2, 76, 0, 12,
+      97, 99, 116, 117, 97, 108, 76, 111, 99, 97, 108, 101, 116, 0, 26, 76, 99, 111, 109, 47, 105, 98,
+      109, 47, 105, 99, 117, 47, 117, 116, 105, 108, 47, 85, 76, 111, 99, 97, 108, 101, 59, 76, 0, 11,
+      118, 97, 108, 105, 100, 76, 111, 99, 97, 108, 101, 113, 0, 126, 0, 5, 120, 114, 0, 16, 106, 97,
+      118, 97, 46, 116, 101, 120, 116, 46, 70, 111, 114, 109, 97, 116, -5, -40, -68, 18, -23, 15, 24,
+      67, 2, 0, 0, 120, 112, 112, 112, 1, 3, 40, 0, 0, 0, 3, 0, 0, 0, 40, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+      1, 0, 0, 0, 0, 0, 2, 126, 114, 0, 31, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116,
+      101, 120, 116, 46, 68, 105, 115, 112, 108, 97, 121, 67, 111, 110, 116, 101, 120, 116, 0, 0, 0,
+      0, 0, 0, 0, 0, 18, 0, 0, 120, 114, 0, 14, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 69, 110,
+      117, 109, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 120, 112, 116, 0, 19, 67, 65, 80, 73, 84, 65, 76,
+      73, 90, 65, 84, 73, 79, 78, 95, 78, 79, 78, 69, 112, 120, 0, 0, 0, 0, 119, 4, 0, 0, 0, 0, 115,
+      114, 0, 34, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 105, 109, 112, 108, 46, 110,
+      117, 109, 98, 101, 114, 46, 80, 114, 111, 112, 101, 114, 116, 105, 101, 115, 56, -42, 52, -54,
+      -104, -87, -46, 123, 3, 0, 0, 120, 112, 119, 8, 0, 0, 0, 0, 0, 0, 0, 7, 116, 0, 12, 103, 114,
+      111, 117, 112, 105, 110, 103, 83, 105, 122, 101, 115, 114, 0, 17, 106, 97, 118, 97, 46, 108, 97,
+      110, 103, 46, 73, 110, 116, 101, 103, 101, 114, 18, -30, -96, -92, -9, -127, -121, 56, 2, 0, 1,
+      73, 0, 5, 118, 97, 108, 117, 101, 120, 114, 0, 16, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46,
+      78, 117, 109, 98, 101, 114, -122, -84, -107, 29, 11, -108, -32, -117, 2, 0, 0, 120, 112, 0, 0,
+      0, 3, 116, 0, 20, 109, 105, 110, 105, 109, 117, 109, 73, 110, 116, 101, 103, 101, 114, 68, 105,
+      103, 105, 116, 115, 115, 113, 0, 126, 0, 15, 0, 0, 0, 2, 116, 0, 15, 112, 97, 100, 100, 105,
+      110, 103, 76, 111, 99, 97, 116, 105, 111, 110, 126, 114, 0, 64, 99, 111, 109, 46, 105, 98, 109,
+      46, 105, 99, 117, 46, 105, 109, 112, 108, 46, 110, 117, 109, 98, 101, 114, 46, 102, 111, 114,
+      109, 97, 116, 116, 101, 114, 115, 46, 80, 97, 100, 100, 105, 110, 103, 70, 111, 114, 109, 97,
+      116, 36, 80, 97, 100, 100, 105, 110, 103, 76, 111, 99, 97, 116, 105, 111, 110, 0, 0, 0, 0, 0, 0,
+      0, 0, 18, 0, 0, 120, 113, 0, 126, 0, 9, 116, 0, 12, 65, 70, 84, 69, 82, 95, 80, 82, 69, 70, 73,
+      88, 116, 0, 13, 112, 97, 100, 100, 105, 110, 103, 83, 116, 114, 105, 110, 103, 116, 0, 1, 42,
+      116, 0, 12, 112, 97, 100, 100, 105, 110, 103, 87, 105, 100, 116, 104, 115, 113, 0, 126, 0, 15,
+      0, 0, 0, 16, 116, 0, 21, 112, 111, 115, 105, 116, 105, 118, 101, 80, 114, 101, 102, 105, 120,
+      80, 97, 116, 116, 101, 114, 110, 116, 0, 2, 65, 45, 116, 0, 21, 112, 111, 115, 105, 116, 105,
+      118, 101, 83, 117, 102, 102, 105, 120, 80, 97, 116, 116, 101, 114, 110, 116, 0, 3, 98, -62, -92,
+      120, 115, 114, 0, 37, 99, 111, 109, 46, 105, 98, 109, 46, 105, 99, 117, 46, 116, 101, 120, 116,
+      46, 68, 101, 99, 105, 109, 97, 108, 70, 111, 114, 109, 97, 116, 83, 121, 109, 98, 111, 108, 115,
+      80, 29, 23, -103, 8, 104, -109, -100, 2, 0, 38, 67, 0, 16, 100, 101, 99, 105, 109, 97, 108, 83,
+      101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 5, 100, 105, 103, 105, 116, 67, 0, 11, 101, 120,
+      112, 111, 110, 101, 110, 116, 105, 97, 108, 67, 0, 17, 103, 114, 111, 117, 112, 105, 110, 103,
+      83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 9, 109, 105, 110, 117, 115, 83, 105, 103, 110,
+      67, 0, 25, 109, 111, 110, 101, 116, 97, 114, 121, 71, 114, 111, 117, 112, 105, 110, 103, 83,
+      101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 17, 109, 111, 110, 101, 116, 97, 114, 121, 83, 101,
+      112, 97, 114, 97, 116, 111, 114, 67, 0, 9, 112, 97, 100, 69, 115, 99, 97, 112, 101, 67, 0, 16,
+      112, 97, 116, 116, 101, 114, 110, 83, 101, 112, 97, 114, 97, 116, 111, 114, 67, 0, 7, 112, 101,
+      114, 77, 105, 108, 108, 67, 0, 7, 112, 101, 114, 99, 101, 110, 116, 67, 0, 8, 112, 108, 117,
+      115, 83, 105, 103, 110, 73, 0, 21, 115, 101, 114, 105, 97, 108, 86, 101, 114, 115, 105, 111,
+      110, 79, 110, 83, 116, 114, 101, 97, 109, 67, 0, 8, 115, 105, 103, 68, 105, 103, 105, 116, 67,
+      0, 9, 122, 101, 114, 111, 68, 105, 103, 105, 116, 76, 0, 3, 78, 97, 78, 116, 0, 18, 76, 106, 97,
+      118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 76, 0, 12, 97, 99, 116,
+      117, 97, 108, 76, 111, 99, 97, 108, 101, 113, 0, 126, 0, 5, 76, 0, 15, 99, 117, 114, 114, 101,
+      110, 99, 121, 80, 97, 116, 116, 101, 114, 110, 113, 0, 126, 0, 33, 91, 0, 19, 99, 117, 114, 114,
+      101, 110, 99, 121, 83, 112, 99, 65, 102, 116, 101, 114, 83, 121, 109, 116, 0, 19, 91, 76, 106,
+      97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 91, 0, 20, 99, 117,
+      114, 114, 101, 110, 99, 121, 83, 112, 99, 66, 101, 102, 111, 114, 101, 83, 121, 109, 113, 0,
+      126, 0, 34, 76, 0, 14, 99, 117, 114, 114, 101, 110, 99, 121, 83, 121, 109, 98, 111, 108, 113, 0,
+      126, 0, 33, 76, 0, 22, 100, 101, 99, 105, 109, 97, 108, 83, 101, 112, 97, 114, 97, 116, 111,
+      114, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 91, 0, 12, 100, 105, 103, 105, 116, 83,
+      116, 114, 105, 110, 103, 115, 113, 0, 126, 0, 34, 91, 0, 6, 100, 105, 103, 105, 116, 115, 116,
+      0, 2, 91, 67, 76, 0, 26, 101, 120, 112, 111, 110, 101, 110, 116, 77, 117, 108, 116, 105, 112,
+      108, 105, 99, 97, 116, 105, 111, 110, 83, 105, 103, 110, 113, 0, 126, 0, 33, 76, 0, 17, 101,
+      120, 112, 111, 110, 101, 110, 116, 83, 101, 112, 97, 114, 97, 116, 111, 114, 113, 0, 126, 0, 33,
+      76, 0, 23, 103, 114, 111, 117, 112, 105, 110, 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 83,
+      116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 8, 105, 110, 102, 105, 110, 105, 116, 121,
+      113, 0, 126, 0, 33, 76, 0, 18, 105, 110, 116, 108, 67, 117, 114, 114, 101, 110, 99, 121, 83,
+      121, 109, 98, 111, 108, 113, 0, 126, 0, 33, 76, 0, 11, 109, 105, 110, 117, 115, 83, 116, 114,
+      105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 31, 109, 111, 110, 101, 116, 97, 114, 121, 71, 114,
+      111, 117, 112, 105, 110, 103, 83, 101, 112, 97, 114, 97, 116, 111, 114, 83, 116, 114, 105, 110,
+      103, 113, 0, 126, 0, 33, 76, 0, 23, 109, 111, 110, 101, 116, 97, 114, 121, 83, 101, 112, 97,
+      114, 97, 116, 111, 114, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 13, 112, 101,
+      114, 77, 105, 108, 108, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 13, 112, 101,
+      114, 99, 101, 110, 116, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 10, 112, 108,
+      117, 115, 83, 116, 114, 105, 110, 103, 113, 0, 126, 0, 33, 76, 0, 15, 114, 101, 113, 117, 101,
+      115, 116, 101, 100, 76, 111, 99, 97, 108, 101, 116, 0, 18, 76, 106, 97, 118, 97, 47, 117, 116,
+      105, 108, 47, 76, 111, 99, 97, 108, 101, 59, 76, 0, 7, 117, 108, 111, 99, 97, 108, 101, 113, 0,
+      126, 0, 5, 76, 0, 11, 118, 97, 108, 105, 100, 76, 111, 99, 97, 108, 101, 113, 0, 126, 0, 5, 120,
+      112, 0, 46, 0, 35, 0, 0, 0, 44, 0, 45, 0, 44, 0, 46, 0, 42, 0, 59, 32, 48, 0, 37, 0, 43, 0, 0,
+      0, 8, 0, 64, 0, 48, 116, 0, 3, 78, 97, 78, 115, 114, 0, 24, 99, 111, 109, 46, 105, 98, 109, 46,
+      105, 99, 117, 46, 117, 116, 105, 108, 46, 85, 76, 111, 99, 97, 108, 101, 51, -114, -10, 104, 70,
+      -48, 11, -31, 2, 0, 1, 76, 0, 8, 108, 111, 99, 97, 108, 101, 73, 68, 113, 0, 126, 0, 33, 120,
+      112, 116, 0, 5, 101, 110, 95, 85, 83, 112, 117, 114, 0, 19, 91, 76, 106, 97, 118, 97, 46, 108,
+      97, 110, 103, 46, 83, 116, 114, 105, 110, 103, 59, -83, -46, 86, -25, -23, 29, 123, 71, 2, 0, 0,
+      120, 112, 0, 0, 0, 3, 116, 0, 6, 91, 58, 94, 83, 58, 93, 116, 0, 9, 91, 58, 100, 105, 103, 105,
+      116, 58, 93, 116, 0, 2, -62, -96, 117, 113, 0, 126, 0, 42, 0, 0, 0, 3, 113, 0, 126, 0, 44, 113,
+      0, 126, 0, 45, 113, 0, 126, 0, 46, 116, 0, 1, 36, 116, 0, 1, 46, 117, 113, 0, 126, 0, 42, 0, 0,
+      0, 10, 116, 0, 1, 48, 116, 0, 1, 49, 116, 0, 1, 50, 116, 0, 1, 51, 116, 0, 1, 52, 116, 0, 1, 53,
+      116, 0, 1, 54, 116, 0, 1, 55, 116, 0, 1, 56, 116, 0, 1, 57, 117, 114, 0, 2, 91, 67, -80, 38,
+      102, -80, -30, 93, -124, -84, 2, 0, 0, 120, 112, 0, 0, 0, 10, 0, 48, 0, 49, 0, 50, 0, 51, 0, 52,
+      0, 53, 0, 54, 0, 55, 0, 56, 0, 57, 116, 0, 2, -61, -105, 116, 0, 1, 69, 116, 0, 1, 44, 116, 0,
+      3, -30, -120, -98, 116, 0, 3, 85, 83, 68, 116, 0, 1, 45, 113, 0, 126, 0, 65, 113, 0, 126, 0, 49,
+      116, 0, 3, -30, -128, -80, 116, 0, 1, 37, 116, 0, 1, 43, 115, 114, 0, 16, 106, 97, 118, 97, 46,
+      117, 116, 105, 108, 46, 76, 111, 99, 97, 108, 101, 126, -8, 17, 96, -100, 48, -7, -20, 3, 0, 6,
+      73, 0, 8, 104, 97, 115, 104, 99, 111, 100, 101, 76, 0, 7, 99, 111, 117, 110, 116, 114, 121, 113,
+      0, 126, 0, 33, 76, 0, 10, 101, 120, 116, 101, 110, 115, 105, 111, 110, 115, 113, 0, 126, 0, 33,
+      76, 0, 8, 108, 97, 110, 103, 117, 97, 103, 101, 113, 0, 126, 0, 33, 76, 0, 6, 115, 99, 114, 105,
+      112, 116, 113, 0, 126, 0, 33, 76, 0, 7, 118, 97, 114, 105, 97, 110, 116, 113, 0, 126, 0, 33,
+      120, 112, -1, -1, -1, -1, 116, 0, 2, 85, 83, 116, 0, 0, 116, 0, 2, 101, 110, 113, 0, 126, 0, 75,
+      113, 0, 126, 0, 75, 120, 115, 113, 0, 126, 0, 39, 113, 0, 126, 0, 41, 113, 0, 126, 0, 40, 120
+    };
+
+    final static byte[][] content = {
+            generalInstance,
+            currencyInstance,
+            scientificInstance,
+            icu58Latest,
+            newFromPattern
+    };
 }
index 897846b2be8a8f77e6846c52674fa2badf8a2147..9c940fafc7fa924e45489e4579e07a942b2bf061 100644 (file)
 
 package com.ibm.icu.dev.test.format;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
 import java.math.BigInteger;
+import java.math.RoundingMode;
 import java.text.AttributedCharacterIterator;
 import java.text.FieldPosition;
 import java.text.Format;
@@ -36,6 +41,7 @@ import com.ibm.icu.impl.ICUConfig;
 import com.ibm.icu.impl.LocaleUtility;
 import com.ibm.icu.impl.data.ResourceReader;
 import com.ibm.icu.impl.data.TokenIterator;
+import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder.SignificantDigitsMode;
 import com.ibm.icu.math.BigDecimal;
 import com.ibm.icu.math.MathContext;
 import com.ibm.icu.text.CompactDecimalFormat;
@@ -49,408 +55,12 @@ import com.ibm.icu.text.NumberFormat.SimpleNumberFormatFactory;
 import com.ibm.icu.text.NumberingSystem;
 import com.ibm.icu.text.RuleBasedNumberFormat;
 import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyUsage;
 import com.ibm.icu.util.CurrencyAmount;
 import com.ibm.icu.util.ULocale;
 
 public class NumberFormatTest extends TestFmwk {
 
-    private static ULocale EN = new ULocale("en");
-
-    private static Number toNumber(String s) {
-        if (s.equals("NaN")) {
-            return Double.NaN;
-        } else if (s.equals("-Inf")) {
-            return Double.NEGATIVE_INFINITY;
-        } else if (s.equals("Inf")) {
-            return Double.POSITIVE_INFINITY;
-        }
-        return new BigDecimal(s);
-    }
-
-
-    private DataDrivenNumberFormatTestUtility.CodeUnderTest ICU =
-            new DataDrivenNumberFormatTestUtility.CodeUnderTest() {
-                @Override
-                public Character Id() { return 'J'; }
-
-                @Override
-                public String format(NumberFormatTestData tuple) {
-                    DecimalFormat fmt = newDecimalFormat(tuple);
-                    String actual = fmt.format(toNumber(tuple.format));
-                    String expected = tuple.output;
-                    if (!expected.equals(actual)) {
-                        return "Expected " + expected + ", got " + actual;
-                    }
-                    return null;
-                }
-
-                @Override
-                public String toPattern(NumberFormatTestData tuple) {
-                    DecimalFormat fmt = newDecimalFormat(tuple);
-                    StringBuilder result = new StringBuilder();
-                    if (tuple.toPattern != null) {
-                        String expected = tuple.toPattern;
-                        String actual = fmt.toPattern();
-                        if (!expected.equals(actual)) {
-                            result.append("Expected toPattern=" + expected + ", got " + actual);
-                        }
-                    }
-                    if (tuple.toLocalizedPattern != null) {
-                        String expected = tuple.toLocalizedPattern;
-                        String actual = fmt.toLocalizedPattern();
-                        if (!expected.equals(actual)) {
-                            result.append("Expected toLocalizedPattern=" + expected + ", got " + actual);
-                        }
-                    }
-                    return result.length() == 0 ? null : result.toString();
-                }
-
-                @Override
-                public String parse(NumberFormatTestData tuple) {
-                    DecimalFormat fmt = newDecimalFormat(tuple);
-                    ParsePosition ppos = new ParsePosition(0);
-                    Number actual = fmt.parse(tuple.parse, ppos);
-                    if (ppos.getIndex() == 0) {
-                        if (!tuple.output.equals("fail")) {
-                            return "Parse error expected.";
-                        }
-                        return null;
-                    }
-                    if (tuple.output.equals("fail")) {
-                        return "Parse succeeded: "+actual+", but was expected to fail.";
-                    }
-                    Number expected = toNumber(tuple.output);
-                    // number types cannot be compared, this is the best we can do.
-                    if (expected.doubleValue() != (actual.doubleValue())) {
-                        return "Expected: " + expected + ", got: " + actual;
-                    }
-                    return null;
-                }
-
-                @Override
-                public String parseCurrency(NumberFormatTestData tuple) {
-                    DecimalFormat fmt = newDecimalFormat(tuple);
-                    ParsePosition ppos = new ParsePosition(0);
-                    CurrencyAmount currAmt = fmt.parseCurrency(tuple.parse, ppos);
-                    if (ppos.getIndex() == 0) {
-                        if (!tuple.output.equals("fail")) {
-                            return "Parse error expected.";
-                        }
-                        return null;
-                    }
-                    if (tuple.output.equals("fail")) {
-                        return "Parse succeeded: "+currAmt+", but was expected to fail.";
-                    }
-                    Number expected = toNumber(tuple.output);
-                    Number actual = currAmt.getNumber();
-                    // number types cannot be compared, this is the best we can do.
-                    if (expected.doubleValue() != (actual.doubleValue())) {
-                        return "Expected: " + expected + ", got: " + actual;
-                    }
-
-                    if (!tuple.outputCurrency.equals(currAmt.getCurrency().toString())) {
-                        return "Expected currency: " + tuple.outputCurrency + ", got: " + currAmt.getCurrency();
-                    }
-                    return null;
-                }
-
-                /**
-                 * @param tuple
-                 * @return
-                 */
-                private DecimalFormat newDecimalFormat(NumberFormatTestData tuple) {
-
-                    DecimalFormat fmt = new DecimalFormat(
-                            tuple.pattern == null ? "0" : tuple.pattern,
-                            new DecimalFormatSymbols(tuple.locale == null ? EN : tuple.locale));
-                    adjustDecimalFormat(tuple, fmt);
-                    return fmt;
-                }
-                /**
-                 * @param tuple
-                 * @param fmt
-                 */
-                private void adjustDecimalFormat(NumberFormatTestData tuple, DecimalFormat fmt) {
-                    if (tuple.minIntegerDigits != null) {
-                        fmt.setMinimumIntegerDigits(tuple.minIntegerDigits);
-                    }
-                    if (tuple.maxIntegerDigits != null) {
-                        fmt.setMaximumIntegerDigits(tuple.maxIntegerDigits);
-                    }
-                    if (tuple.minFractionDigits != null) {
-                        fmt.setMinimumFractionDigits(tuple.minFractionDigits);
-                    }
-                    if (tuple.maxFractionDigits != null) {
-                        fmt.setMaximumFractionDigits(tuple.maxFractionDigits);
-                    }
-                    if (tuple.currency != null) {
-                        fmt.setCurrency(tuple.currency);
-                    }
-                    if (tuple.minGroupingDigits != null) {
-                        // Oops we don't support this.
-                    }
-                    if (tuple.useSigDigits != null) {
-                        fmt.setSignificantDigitsUsed(
-                                tuple.useSigDigits != 0);
-                    }
-                    if (tuple.minSigDigits != null) {
-                        fmt.setMinimumSignificantDigits(tuple.minSigDigits);
-                    }
-                    if (tuple.maxSigDigits != null) {
-                        fmt.setMaximumSignificantDigits(tuple.maxSigDigits);
-                    }
-                    if (tuple.useGrouping != null) {
-                        fmt.setGroupingUsed(tuple.useGrouping != 0);
-                    }
-                    if (tuple.multiplier != null) {
-                        fmt.setMultiplier(tuple.multiplier);
-                    }
-                    if (tuple.roundingIncrement != null) {
-                        fmt.setRoundingIncrement(tuple.roundingIncrement.doubleValue());
-                    }
-                    if (tuple.formatWidth != null) {
-                        fmt.setFormatWidth(tuple.formatWidth);
-                    }
-                    if (tuple.padCharacter != null && tuple.padCharacter.length() > 0) {
-                        fmt.setPadCharacter(tuple.padCharacter.charAt(0));
-                    }
-                    if (tuple.useScientific != null) {
-                        fmt.setScientificNotation(tuple.useScientific != 0);
-                    }
-                    if (tuple.grouping != null) {
-                        fmt.setGroupingSize(tuple.grouping);
-                    }
-                    if (tuple.grouping2 != null) {
-                        fmt.setSecondaryGroupingSize(tuple.grouping2);
-                    }
-                    if (tuple.roundingMode != null) {
-                        fmt.setRoundingMode(tuple.roundingMode);
-                    }
-                    if (tuple.currencyUsage != null) {
-                        fmt.setCurrencyUsage(tuple.currencyUsage);
-                    }                    if (tuple.minimumExponentDigits != null) {
-                        fmt.setMinimumExponentDigits(
-                                tuple.minimumExponentDigits.byteValue());
-                    }
-                    if (tuple.exponentSignAlwaysShown != null) {
-                        fmt.setExponentSignAlwaysShown(
-                                tuple.exponentSignAlwaysShown != 0);
-                    }
-                    if (tuple.decimalSeparatorAlwaysShown != null) {
-                        fmt.setDecimalSeparatorAlwaysShown(
-                                tuple.decimalSeparatorAlwaysShown != 0);
-                    }
-                    if (tuple.padPosition != null) {
-                        fmt.setPadPosition(tuple.padPosition);
-                    }
-                    if (tuple.positivePrefix != null) {
-                        fmt.setPositivePrefix(tuple.positivePrefix);
-                    }
-                    if (tuple.positiveSuffix != null) {
-                        fmt.setPositiveSuffix(tuple.positiveSuffix);
-                    }
-                    if (tuple.negativePrefix != null) {
-                        fmt.setNegativePrefix(tuple.negativePrefix);
-                    }
-                    if (tuple.negativeSuffix != null) {
-                        fmt.setNegativeSuffix(tuple.negativeSuffix);
-                    }
-                    if (tuple.localizedPattern != null) {
-                        fmt.applyLocalizedPattern(tuple.localizedPattern);
-                    }
-                    int lenient = tuple.lenient == null ? 1 : tuple.lenient.intValue();
-                    fmt.setParseStrict(lenient == 0);
-                    if (tuple.parseIntegerOnly != null) {
-                        fmt.setParseIntegerOnly(tuple.parseIntegerOnly != 0);
-                    }
-                    if (tuple.decimalPatternMatchRequired != null) {
-                        fmt.setDecimalPatternMatchRequired(tuple.decimalPatternMatchRequired != 0);
-                    }
-                    if (tuple.parseNoExponent != null) {
-                        // Oops, not supported for now
-                    }
-                }
-    };
-
-
-    private DataDrivenNumberFormatTestUtility.CodeUnderTest JDK =
-            new DataDrivenNumberFormatTestUtility.CodeUnderTest() {
-                @Override
-                public Character Id() { return 'K'; }
-
-                @Override
-                public String format(NumberFormatTestData tuple) {
-                    java.text.DecimalFormat fmt = newDecimalFormat(tuple);
-                    String actual = fmt.format(toNumber(tuple.format));
-                    String expected = tuple.output;
-                    if (!expected.equals(actual)) {
-                        return "Expected " + expected + ", got " + actual;
-                    }
-                    return null;
-                }
-
-                @Override
-                public String toPattern(NumberFormatTestData tuple) {
-                    java.text.DecimalFormat fmt = newDecimalFormat(tuple);
-                    StringBuilder result = new StringBuilder();
-                    if (tuple.toPattern != null) {
-                        String expected = tuple.toPattern;
-                        String actual = fmt.toPattern();
-                        if (!expected.equals(actual)) {
-                            result.append("Expected toPattern=" + expected + ", got " + actual);
-                        }
-                    }
-                    if (tuple.toLocalizedPattern != null) {
-                        String expected = tuple.toLocalizedPattern;
-                        String actual = fmt.toLocalizedPattern();
-                        if (!expected.equals(actual)) {
-                            result.append("Expected toLocalizedPattern=" + expected + ", got " + actual);
-                        }
-                    }
-                    return result.length() == 0 ? null : result.toString();
-                }
-
-                @Override
-                public String parse(NumberFormatTestData tuple) {
-                    java.text.DecimalFormat fmt = newDecimalFormat(tuple);
-                    ParsePosition ppos = new ParsePosition(0);
-                    Number actual = fmt.parse(tuple.parse, ppos);
-                    if (ppos.getIndex() == 0) {
-                        if (!tuple.output.equals("fail")) {
-                            return "Parse error expected.";
-                        }
-                        return null;
-                    }
-                    if (tuple.output.equals("fail")) {
-                        return "Parse succeeded: "+actual+", but was expected to fail.";
-                    }
-                    Number expected = toNumber(tuple.output);
-                    // number types cannot be compared, this is the best we can do.
-                    if (expected.doubleValue() != actual.doubleValue()) {
-                        return "Expected: " + expected + ", got: " + actual;
-                    }
-                    return null;
-                }
-
-
-
-                /**
-                 * @param tuple
-                 * @return
-                 */
-                private java.text.DecimalFormat newDecimalFormat(NumberFormatTestData tuple) {
-                    java.text.DecimalFormat fmt = new java.text.DecimalFormat(
-                            tuple.pattern == null ? "0" : tuple.pattern,
-                            new java.text.DecimalFormatSymbols(
-                                    (tuple.locale == null ? EN : tuple.locale).toLocale()));
-                    adjustDecimalFormat(tuple, fmt);
-                    return fmt;
-                }
-
-                /**
-                 * @param tuple
-                 * @param fmt
-                 */
-                private void adjustDecimalFormat(NumberFormatTestData tuple, java.text.DecimalFormat fmt) {
-                    if (tuple.minIntegerDigits != null) {
-                        fmt.setMinimumIntegerDigits(tuple.minIntegerDigits);
-                    }
-                    if (tuple.maxIntegerDigits != null) {
-                        fmt.setMaximumIntegerDigits(tuple.maxIntegerDigits);
-                    }
-                    if (tuple.minFractionDigits != null) {
-                        fmt.setMinimumFractionDigits(tuple.minFractionDigits);
-                    }
-                    if (tuple.maxFractionDigits != null) {
-                        fmt.setMaximumFractionDigits(tuple.maxFractionDigits);
-                    }
-                    if (tuple.currency != null) {
-                        fmt.setCurrency(java.util.Currency.getInstance(tuple.currency.toString()));
-                    }
-                    if (tuple.minGroupingDigits != null) {
-                        // Oops we don't support this.
-                    }
-                    if (tuple.useSigDigits != null) {
-                        // Oops we don't support this
-                    }
-                    if (tuple.minSigDigits != null) {
-                        // Oops we don't support this
-                    }
-                    if (tuple.maxSigDigits != null) {
-                        // Oops we don't support this
-                    }
-                    if (tuple.useGrouping != null) {
-                        fmt.setGroupingUsed(tuple.useGrouping != 0);
-                    }
-                    if (tuple.multiplier != null) {
-                        fmt.setMultiplier(tuple.multiplier);
-                    }
-                    if (tuple.roundingIncrement != null) {
-                        // Not supported
-                    }
-                    if (tuple.formatWidth != null) {
-                        // Not supported
-                    }
-                    if (tuple.padCharacter != null && tuple.padCharacter.length() > 0) {
-                        // Not supported
-                    }
-                    if (tuple.useScientific != null) {
-                        // Not supported
-                    }
-                    if (tuple.grouping != null) {
-                        fmt.setGroupingSize(tuple.grouping);
-                    }
-                    if (tuple.grouping2 != null) {
-                        // Not supported
-                    }
-                    if (tuple.roundingMode != null) {
-                        // Not supported
-                    }
-                    if (tuple.currencyUsage != null) {
-                        // Not supported
-                    }
-                    if (tuple.minimumExponentDigits != null) {
-                        // Not supported
-                    }
-                    if (tuple.exponentSignAlwaysShown != null) {
-                        // Not supported
-                    }
-                    if (tuple.decimalSeparatorAlwaysShown != null) {
-                        fmt.setDecimalSeparatorAlwaysShown(
-                                tuple.decimalSeparatorAlwaysShown != 0);
-                    }
-                    if (tuple.padPosition != null) {
-                        // Not supported
-                    }
-                    if (tuple.positivePrefix != null) {
-                        fmt.setPositivePrefix(tuple.positivePrefix);
-                    }
-                    if (tuple.positiveSuffix != null) {
-                        fmt.setPositiveSuffix(tuple.positiveSuffix);
-                    }
-                    if (tuple.negativePrefix != null) {
-                        fmt.setNegativePrefix(tuple.negativePrefix);
-                    }
-                    if (tuple.negativeSuffix != null) {
-                        fmt.setNegativeSuffix(tuple.negativeSuffix);
-                    }
-                    if (tuple.localizedPattern != null) {
-                        fmt.applyLocalizedPattern(tuple.localizedPattern);
-                    }
-
-                    // lenient parsing not supported by JDK
-                    if (tuple.parseIntegerOnly != null) {
-                        fmt.setParseIntegerOnly(tuple.parseIntegerOnly != 0);
-                    }
-                    if (tuple.decimalPatternMatchRequired != null) {
-                       // Oops, not supported
-                    }
-                    if (tuple.parseNoExponent != null) {
-                        // Oops, not supported for now
-                    }
-                }
-    };
-
     @Test
     public void TestRoundingScientific10542() {
         DecimalFormat format =
@@ -610,7 +220,7 @@ public class NumberFormatTest extends TestFmwk {
         DecimalFormatSymbols sym = new DecimalFormatSymbols(Locale.US);
         final String pat[]    = { "#.#", "#.", ".#", "#" };
         int pat_length = pat.length;
-        final String newpat[] = { "#0.#", "#0.", "#.0", "#" };
+        final String newpat[] = { "0.#", "0.", "#.0", "0" };
         final String num[]    = { "0",   "0.", ".0", "0" };
         for (int i=0; i<pat_length; ++i)
         {
@@ -812,12 +422,14 @@ public class NumberFormatTest extends TestFmwk {
                 {"$124", "4", "-1"},
                 {"$124 $124", "4", "-1"},
                 {"$124 ", "4", "-1"},
+                {"$124  ", "4", "-1"},
                 {"$ 124 ", "5", "-1"},
                 {"$\u00A0124 ", "5", "-1"},
-                {" $ 124 ", "0", "0"}, // TODO: need to handle space correctly
-                {"124$", "0", "3"}, // TODO: need to handle space correctly
-                // {"124 $", "5", "-1"}, TODO: OK or NOT?
-                {"124 $", "0", "3"},
+                {" $ 124 ", "6", "-1"},
+                {"124$", "3", "-1"},
+                {"124 $", "3", "-1"},
+                {"$124\u200D", "4", "-1"},
+                {"$\u200D124", "5", "-1"},
         };
         NumberFormat foo = NumberFormat.getCurrencyInstance();
         for (int i = 0; i < DATA.length; ++i) {
@@ -841,6 +453,34 @@ public class NumberFormatTest extends TestFmwk {
         }
     }
 
+    @Test
+    public void TestSpaceParsingStrict() {
+        // All trailing grouping separators should be ignored in strict mode, not just the first.
+        Object[][] cases = {
+                {"123 ", 3, -1},
+                {"123  ", 3, -1},
+                {"123  ,", 3, -1},
+                {"123,", 3, -1},
+                {"123, ", 3, -1},
+                {"123,,", 3, -1},
+                {"123,, ", 3, -1},
+                {"123 ,", 3, -1},
+                {"123, ", 3, -1},
+                {"123, 456", 3, -1},
+                {"123  456", 0, 8} // TODO: Does this behavior make sense?
+        };
+        DecimalFormat df = new DecimalFormat("#,###");
+        df.setParseStrict(true);
+        for (Object[] cas : cases) {
+            String input = (String) cas[0];
+            int expectedIndex = (Integer) cas[1];
+            int expectedErrorIndex = (Integer) cas[2];
+            ParsePosition ppos = new ParsePosition(0);
+            df.parse(input, ppos);
+            assertEquals("Failed on index: '" + input + "'", expectedIndex, ppos.getIndex());
+            assertEquals("Failed on error: '" + input + "'", expectedErrorIndex, ppos.getErrorIndex());
+        }
+    }
 
     @Test
     public void TestMultiCurrencySign() {
@@ -893,13 +533,14 @@ public class NumberFormatTest extends TestFmwk {
                 }
                 try {
                     // mix style parsing
-                    for (int k=3; k<=5; ++k) {
+                    for (int k=3; k<=4; ++k) {
                         // DATA[i][3] is the currency format result using a
                         // single currency sign.
                         // DATA[i][4] is the currency format result using
                         // double currency sign.
                         // DATA[i][5] is the currency format result using
                         // triple currency sign.
+                        // ICU 59: long name parsing requires currency mode.
                         String oneCurrencyFormat = DATA[i][k];
                         if (fmt.parse(oneCurrencyFormat).doubleValue() !=
                                 numberToBeFormat.doubleValue()) {
@@ -1093,25 +734,20 @@ public class NumberFormatTest extends TestFmwk {
                 if (!strBuf.equals(formatResult)) {
                     errln("FAIL: localeID: " + localeString + ", expected(" + formatResult.length() + "): \"" + formatResult + "\", actual(" + strBuf.length() + "): \"" + strBuf + "\"");
                 }
-                try {
-                    // test parsing, and test parsing for all currency formats.
-                    for (int j = 3; j < 6; ++j) {
-                        // DATA[i][3] is the currency format result using
-                        // CURRENCYSTYLE formatter.
-                        // DATA[i][4] is the currency format result using
-                        // ISOCURRENCYSTYLE formatter.
-                        // DATA[i][5] is the currency format result using
-                        // PLURALCURRENCYSTYLE formatter.
-                        String oneCurrencyFormatResult = DATA[i][j];
-                        Number val = numFmt.parse(oneCurrencyFormatResult);
-                        if (val.doubleValue() != numberToBeFormat.doubleValue()) {
-                            errln("FAIL: getCurrencyFormat of locale " + localeString + " failed roundtripping the number. val=" + val + "; expected: " + numberToBeFormat);
-                        }
+                // test parsing, and test parsing for all currency formats.
+                for (int j = 3; j < 6; ++j) {
+                    // DATA[i][3] is the currency format result using
+                    // CURRENCYSTYLE formatter.
+                    // DATA[i][4] is the currency format result using
+                    // ISOCURRENCYSTYLE formatter.
+                    // DATA[i][5] is the currency format result using
+                    // PLURALCURRENCYSTYLE formatter.
+                    String oneCurrencyFormatResult = DATA[i][j];
+                    CurrencyAmount val = numFmt.parseCurrency(oneCurrencyFormatResult, null);
+                    if (val.getNumber().doubleValue() != numberToBeFormat.doubleValue()) {
+                        errln("FAIL: getCurrencyFormat of locale " + localeString + " failed roundtripping the number. val=" + val + "; expected: " + numberToBeFormat);
                     }
                 }
-                catch (ParseException e) {
-                    errln("FAIL: " + e.getMessage());
-                }
             }
         }
     }
@@ -1121,29 +757,43 @@ public class NumberFormatTest extends TestFmwk {
     public void TestMiscCurrencyParsing() {
         String[][] DATA = {
                 // each has: string to be parsed, parsed position, error position
-                {"1.00 ", "0", "4"},
-                {"1.00 UAE dirha", "0", "4"},
-                {"1.00 us dollar", "14", "-1"},
-                {"1.00 US DOLLAR", "14", "-1"},
-                {"1.00 usd", "0", "4"},
+                {"1.00 ", "4", "-1", "0", "5"},
+                {"1.00 UAE dirha", "4", "-1", "0", "14"},
+                {"1.00 us dollar", "4", "-1", "14", "-1"},
+                {"1.00 US DOLLAR", "4", "-1", "14", "-1"},
+                {"1.00 usd", "4", "-1", "8", "-1"},
+                {"1.00 USD", "4", "-1", "8", "-1"},
         };
         ULocale locale = new ULocale("en_US");
         for (int i=0; i<DATA.length; ++i) {
             String stringToBeParsed = DATA[i][0];
             int parsedPosition = Integer.parseInt(DATA[i][1]);
             int errorIndex = Integer.parseInt(DATA[i][2]);
+            int currParsedPosition = Integer.parseInt(DATA[i][3]);
+            int currErrorIndex = Integer.parseInt(DATA[i][4]);
             NumberFormat numFmt = NumberFormat.getInstance(locale, NumberFormat.CURRENCYSTYLE);
             ParsePosition parsePosition = new ParsePosition(0);
             Number val = numFmt.parse(stringToBeParsed, parsePosition);
             if (parsePosition.getIndex() != parsedPosition ||
                     parsePosition.getErrorIndex() != errorIndex) {
-                errln("FAIL: parse failed. expected error position: " + errorIndex + "; actual: " + parsePosition.getErrorIndex());
-                errln("FAIL: parse failed. expected position: " + parsedPosition +"; actual: " + parsePosition.getIndex());
+                errln("FAIL: parse failed on case "+i+". expected position: " + parsedPosition +"; actual: " + parsePosition.getIndex());
+                errln("FAIL: parse failed on case "+i+". expected error position: " + errorIndex + "; actual: " + parsePosition.getErrorIndex());
             }
             if (parsePosition.getErrorIndex() == -1 &&
                     val.doubleValue() != 1.00) {
                 errln("FAIL: parse failed. expected 1.00, actual:" + val);
             }
+            parsePosition = new ParsePosition(0);
+            CurrencyAmount amt = numFmt.parseCurrency(stringToBeParsed, parsePosition);
+            if (parsePosition.getIndex() != currParsedPosition ||
+                    parsePosition.getErrorIndex() != currErrorIndex) {
+                errln("FAIL: parseCurrency failed on case "+i+". expected error position: " + currErrorIndex + "; actual: " + parsePosition.getErrorIndex());
+                errln("FAIL: parseCurrency failed on case "+i+". expected position: " + currParsedPosition +"; actual: " + parsePosition.getIndex());
+            }
+            if (parsePosition.getErrorIndex() == -1 &&
+                    amt.getNumber().doubleValue() != 1.00) {
+                errln("FAIL: parseCurrency failed. expected 1.00, actual:" + val);
+            }
         }
     }
 
@@ -1178,26 +828,28 @@ public class NumberFormatTest extends TestFmwk {
             public int    getCurExpectVal()  { return curExpectVal; }
             public String getCurExpectCurr() { return curExpectCurr; }
         }
+        // Note: In cases where the number occurs before the currency sign, non-currency mode will parse the number
+        // and stop when it reaches the currency symbol.
         final ParseCurrencyItem[] parseCurrencyItems = {
                 new ParseCurrencyItem( "en_US", "dollars2", "$2.00",            5,  2,  5,  2,  "USD" ),
                 new ParseCurrencyItem( "en_US", "dollars4", "$4",               2,  4,  2,  4,  "USD" ),
-                new ParseCurrencyItem( "en_US", "dollars9", "9\u00A0$",         0,  0,  0,  0,  ""    ),
+                new ParseCurrencyItem( "en_US", "dollars9", "9\u00A0$",         1,  9,  3,  9,  "USD" ),
                 new ParseCurrencyItem( "en_US", "pounds3",  "\u00A33.00",       0,  0,  5,  3,  "GBP" ),
                 new ParseCurrencyItem( "en_US", "pounds5",  "\u00A35",          0,  0,  2,  5,  "GBP" ),
-                new ParseCurrencyItem( "en_US", "pounds7",  "7\u00A0\u00A3",    0,  0,  0,  0,  ""    ),
+                new ParseCurrencyItem( "en_US", "pounds7",  "7\u00A0\u00A3",    1,  7,  3,  7,  "GBP" ),
                 new ParseCurrencyItem( "en_US", "euros8",   "\u20AC8",          0,  0,  2,  8,  "EUR" ),
 
                 new ParseCurrencyItem( "en_GB", "pounds3",  "\u00A33.00",       5,  3,  5,  3,  "GBP" ),
                 new ParseCurrencyItem( "en_GB", "pounds5",  "\u00A35",          2,  5,  2,  5,  "GBP" ),
-                new ParseCurrencyItem( "en_GB", "pounds7",  "7\u00A0\u00A3",    0,  0,  0,  0,  ""    ),
-                new ParseCurrencyItem( "en_GB", "euros4",   "4,00\u00A0\u20AC", 0,  0,  0,  0,  ""    ),
-                new ParseCurrencyItem( "en_GB", "euros6",   "6\u00A0\u20AC",    0,  0,  0,  0,  ""    ),
+                new ParseCurrencyItem( "en_GB", "pounds7",  "7\u00A0\u00A3",    1,  7,  3,  7,  "GBP" ),
+                new ParseCurrencyItem( "en_GB", "euros4",   "4,00\u00A0\u20AC", 4,400,  6,400,  "EUR" ),
+                new ParseCurrencyItem( "en_GB", "euros6",   "6\u00A0\u20AC",    1,  6,  3,  6,  "EUR" ),
                 new ParseCurrencyItem( "en_GB", "euros8",   "\u20AC8",          0,  0,  2,  8,  "EUR" ),
                 new ParseCurrencyItem( "en_GB", "dollars4", "US$4",             0,  0,  4,  4,  "USD" ),
 
                 new ParseCurrencyItem( "fr_FR", "euros4",   "4,00\u00A0\u20AC", 6,  4,  6,  4,  "EUR" ),
                 new ParseCurrencyItem( "fr_FR", "euros6",   "6\u00A0\u20AC",    3,  6,  3,  6,  "EUR" ),
-                new ParseCurrencyItem( "fr_FR", "euros8",   "\u20AC8",          0,  0,  0,  0,  ""    ),
+                new ParseCurrencyItem( "fr_FR", "euros8",   "\u20AC8",          0,  0,  2,  8,  "EUR" ),
                 new ParseCurrencyItem( "fr_FR", "dollars2", "$2.00",            0,  0,  0,  0,  ""    ),
                 new ParseCurrencyItem( "fr_FR", "dollars4", "$4",               0,  0,  0,  0,  ""    ),
         };
@@ -1244,6 +896,14 @@ public class NumberFormatTest extends TestFmwk {
         }
     }
 
+    @Test
+    public void TestParseCurrencyWithWhitespace() {
+        DecimalFormat df = new DecimalFormat("#,##0.00 ¤¤");
+        ParsePosition ppos = new ParsePosition(0);
+        df.parseCurrency("1.00 us denmark", ppos);
+        assertEquals("Expected to fail on 'us denmark' string", 9, ppos.getErrorIndex());
+    }
+
     @Test
     public void TestParseCurrPatternWithDecStyle() {
         String currpat = "¤#,##0.00";
@@ -1382,12 +1042,12 @@ public class NumberFormatTest extends TestFmwk {
         DecimalFormat f = new DecimalFormat("#,##,###", US);
 
         expect(f, 123456789L, "12,34,56,789");
-        expectPat(f, "#,##,###");
+        expectPat(f, "#,##,##0");
         f.applyPattern("#,###");
 
         f.setSecondaryGroupingSize(4);
         expect(f, 123456789L, "12,3456,789");
-        expectPat(f, "#,####,###");
+        expectPat(f, "#,####,##0");
         NumberFormat g = NumberFormat.getInstance(new Locale("hi", "IN"));
 
         String out = "";
@@ -1482,6 +1142,7 @@ public class NumberFormatTest extends TestFmwk {
                 errln("FAIL Pattern rt \"" + pat + "\" . \"" + pat2 + "\"");
             }
             // Make sure digit counts match what we expect
+            if (i == 0) continue; // outputs to 1,1,0,0 since at least one min digit is required.
             if (df.getMinimumIntegerDigits() != DIGITS[4 * i]
                     || df.getMaximumIntegerDigits() != DIGITS[4 * i + 1]
                             || df.getMinimumFractionDigits() != DIGITS[4 * i + 2]
@@ -1669,6 +1330,9 @@ public class NumberFormatTest extends TestFmwk {
           UnicodeString pattern(patternChars);
           expectPad(fmt, pattern , DecimalFormat.kPadBeforePrefix, 4, padString);
          */
+
+        // Test multi-char padding sequence specified via pattern
+        expect2(new DecimalFormat("*'😃'####.00", US), 1.1, "😃😃😃1.10");
     }
 
     /**
@@ -1723,9 +1387,12 @@ public class NumberFormatTest extends TestFmwk {
         //              12  3456789012345
         expectPat(fmt, "AA*^####,##0.00ZZ"); // This is the interesting case
 
+        // The new implementation produces "AA*^#####,##0.00ZZ", which is functionally equivalent
+        // to what the old implementation produced, "AA*^#,###,##0.00ZZ"
         fmt.setFormatWidth(16);
         //              12  34567890123456
-        expectPat(fmt, "AA*^#,###,##0.00ZZ");
+        //expectPat(fmt, "AA*^#,###,##0.00ZZ");
+        expectPat(fmt, "AA*^#####,##0.00ZZ");
     }
 
     @Test
@@ -1872,7 +1539,8 @@ public class NumberFormatTest extends TestFmwk {
             errln("FAIL: isScientificNotation = true, expect false");
         }
 
-        df.applyPattern("0.00000");
+        // Create a new instance to flush out currency info
+        df = new DecimalFormat("0.00000", US);
         df.setScientificNotation(true);
         if (!df.isScientificNotation()) {
             errln("FAIL: isScientificNotation = false, expect true");
@@ -1924,6 +1592,23 @@ public class NumberFormatTest extends TestFmwk {
         }
     }
 
+    @Test
+    public void TestParseNull() throws ParseException {
+        DecimalFormat df = new DecimalFormat();
+        try {
+            df.parse(null);
+            fail("df.parse(null) didn't throw an exception");
+        } catch (IllegalArgumentException e){}
+        try {
+            df.parse(null, null);
+            fail("df.parse(null) didn't throw an exception");
+        } catch (IllegalArgumentException e){}
+        try {
+            df.parseCurrency(null, null);
+            fail("df.parse(null) didn't throw an exception");
+        } catch (IllegalArgumentException e){}
+    }
+
     @Test
     public void TestWhiteSpaceParsing() {
         DecimalFormatSymbols US = new DecimalFormatSymbols(Locale.US);
@@ -1931,6 +1616,13 @@ public class NumberFormatTest extends TestFmwk {
         int n = 1234;
         expect(fmt, "a b1234c ", n);
         expect(fmt, "a   b1234c   ", n);
+        expect(fmt, "ab1234", n);
+
+        fmt.applyPattern("a b #");
+        expect(fmt, "ab1234", n);
+        expect(fmt, "ab  1234", n);
+        expect(fmt, "a b1234", n);
+        expect(fmt, "a   b1234", n);
     }
 
     /**
@@ -2734,38 +2426,38 @@ public class NumberFormatTest extends TestFmwk {
     // Support methods
     //------------------------------------------------------------------
 
-    // Format-Parse test
+    /** Format-Parse test */
     public void expect2(NumberFormat fmt, Number n, String exp) {
         // Don't round-trip format test, since we explicitly do it
         expect(fmt, n, exp, false);
         expect(fmt, exp, n);
     }
-    // Format-Parse test
+    /** Format-Parse test */
     public void expect3(NumberFormat fmt, Number n, String exp) {
         // Don't round-trip format test, since we explicitly do it
         expect_rbnf(fmt, n, exp, false);
         expect_rbnf(fmt, exp, n);
     }
 
-    // Format-Parse test (convenience)
+    /** Format-Parse test (convenience) */
     public void expect2(NumberFormat fmt, double n, String exp) {
         expect2(fmt, new Double(n), exp);
     }
-    // Format-Parse test (convenience)
+    /** RBNF Format-Parse test (convenience) */
     public void expect3(NumberFormat fmt, double n, String exp) {
         expect3(fmt, new Double(n), exp);
     }
 
-    // Format-Parse test (convenience)
+    /** Format-Parse test (convenience) */
     public void expect2(NumberFormat fmt, long n, String exp) {
         expect2(fmt, new Long(n), exp);
     }
-    // Format-Parse test (convenience)
+    /** RBNF Format-Parse test (convenience) */
     public void expect3(NumberFormat fmt, long n, String exp) {
         expect3(fmt, new Long(n), exp);
     }
 
-    // Format test
+    /** Format test */
     public void expect(NumberFormat fmt, Number n, String exp, boolean rt) {
         StringBuffer saw = new StringBuffer();
         FieldPosition pos = new FieldPosition(0);
@@ -2798,7 +2490,7 @@ public class NumberFormatTest extends TestFmwk {
                     ", FAIL " + n + " x " + pat + " = \"" + saw + "\", expected \"" + exp + "\"");
         }
     }
-    // Format test
+    /** RBNF format test */
     public void expect_rbnf(NumberFormat fmt, Number n, String exp, boolean rt) {
         StringBuffer saw = new StringBuffer();
         FieldPosition pos = new FieldPosition(0);
@@ -2830,22 +2522,22 @@ public class NumberFormatTest extends TestFmwk {
         }
     }
 
-    // Format test (convenience)
+    /** Format test (convenience) */
     public void expect(NumberFormat fmt, Number n, String exp) {
         expect(fmt, n, exp, true);
     }
 
-    // Format test (convenience)
+    /** Format test (convenience) */
     public void expect(NumberFormat fmt, double n, String exp) {
         expect(fmt, new Double(n), exp);
     }
 
-    // Format test (convenience)
+    /** Format test (convenience) */
     public void expect(NumberFormat fmt, long n, String exp) {
         expect(fmt, new Long(n), exp);
     }
 
-    // Parse test
+    /** Parse test */
     public void expect(NumberFormat fmt, String str, Number n) {
         Number num = null;
         try {
@@ -2867,7 +2559,7 @@ public class NumberFormatTest extends TestFmwk {
         }
     }
 
-    // Parse test
+    /** RBNF Parse test */
     public void expect_rbnf(NumberFormat fmt, String str, Number n) {
         Number num = null;
         try {
@@ -2887,12 +2579,12 @@ public class NumberFormatTest extends TestFmwk {
         }
     }
 
-    // Parse test (convenience)
+    /** Parse test (convenience) */
     public void expect(NumberFormat fmt, String str, double n) {
         expect(fmt, str, new Double(n));
     }
 
-    // Parse test (convenience)
+    /** Parse test (convenience) */
     public void expect(NumberFormat fmt, String str, long n) {
         expect(fmt, str, new Long(n));
     }
@@ -2973,6 +2665,14 @@ public class NumberFormatTest extends TestFmwk {
         }
     }
 
+    @Test
+    public void TestScientificWithGrouping() {
+        DecimalFormat df = new DecimalFormat("#,##0.000E0");
+        expect2(df, 123, "123.0E0");
+        expect2(df, 1234, "1,234E0");
+        expect2(df, 12340, "1.234E4");
+    }
+
     @Test
     public void TestStrictParse() {
         String[] pass = {
@@ -2992,6 +2692,10 @@ public class NumberFormatTest extends TestFmwk {
                 "00",          // leading zero before zero - used to be error - see ticket #7913
                 "012",         // leading zero before digit - used to be error - see ticket #7913
                 "0,456",       // leading zero before group separator - used to be error - see ticket #7913
+                "999,999",     // see ticket #6863
+                "-99,999",     // see ticket #6863
+                "-999,999",    // see ticket #6863
+                "-9,999,999",  // see ticket #6863
         };
         String[] fail = {
                 "1,2",       // wrong number of digits after group separator
@@ -3018,7 +2722,6 @@ public class NumberFormatTest extends TestFmwk {
                 "00E2",     // leading zeroes now allowed in strict mode - see ticket #
         };
         String[] scientificFail = {
-                "1,234E2",  // group separators with exponent fail
         };
 
         nf = (DecimalFormat) NumberFormat.getInstance(Locale.ENGLISH);
@@ -3589,17 +3292,17 @@ public class NumberFormatTest extends TestFmwk {
         // For valid array, it is displayed as {min value, max value}
         // Tests when "if (minimumIntegerDigits > maximumIntegerDigits)" is true
         int[][] cases = { { -1, 0 }, { 0, 1 }, { 1, 0 }, { 2, 0 }, { 2, 1 }, { 10, 0 } };
-        int[] expectedMax = { 0, 1, 1, 2, 2, 10 };
+        int[] expectedMax = { 1, 1, 1, 2, 2, 10 };
         if (cases.length != expectedMax.length) {
             errln("Can't continue test case method TestSetMinimumIntegerDigits "
                     + "since the test case arrays are unequal.");
         } else {
             for (int i = 0; i < cases.length; i++) {
-                nf.setMaximumIntegerDigits(cases[i][1]);
                 nf.setMinimumIntegerDigits(cases[i][0]);
+                nf.setMaximumIntegerDigits(cases[i][1]);
                 if (nf.getMaximumIntegerDigits() != expectedMax[i]) {
                     errln("NumberFormat.setMinimumIntegerDigits(int newValue "
-                            + "did not return an expected result for parameter " + cases[i][1] + " and " + cases[i][0]
+                            + "did not return an expected result for parameter " + cases[i][0] + " and " + cases[i][1]
                                     + " and expected " + expectedMax[i] + " but got " + nf.getMaximumIntegerDigits());
                 }
             }
@@ -3875,6 +3578,10 @@ public class NumberFormatTest extends TestFmwk {
         }
     }
 
+    /*
+     * This feature had to do with a limitation in DigitList.java that no longer exists in the
+     * new implementation.
+     *
     @Test
     public void TestParseMaxDigits() {
         DecimalFormat fmt = new DecimalFormat();
@@ -3883,7 +3590,7 @@ public class NumberFormatTest extends TestFmwk {
 
         fmt.setParseMaxDigits(-1);
 
-        /* Default value is 1000 */
+        // Default value is 1000
         if (fmt.getParseMaxDigits() != 1000) {
             errln("Fail valid value checking in setParseMaxDigits.");
         }
@@ -3902,6 +3609,7 @@ public class NumberFormatTest extends TestFmwk {
 
         }
     }
+    */
 
     private static class FormatCharItrTestThread implements Runnable {
         private final NumberFormat fmt;
@@ -4392,13 +4100,27 @@ public class NumberFormatTest extends TestFmwk {
         }
     }
 
+    @Test
+    public void TestCurrencyWithMinMaxFractionDigits() {
+        DecimalFormat df = new DecimalFormat();
+        df.applyPattern("¤#,##0.00");
+        df.setCurrency(Currency.getInstance("USD"));
+        assertEquals("Basic currency format fails", "$1.23", df.format(1.234));
+        df.setMaximumFractionDigits(4);
+        assertEquals("Currency with max fraction == 4", "$1.234", df.format(1.234));
+        df.setMinimumFractionDigits(4);
+        assertEquals("Currency with min fraction == 4", "$1.2340", df.format(1.234));
+    }
+
     @Test
     public void TestParseRequiredDecimalPoint() {
 
         String[] testPattern = { "00.####", "00.0", "00" };
 
         String value2Parse = "99";
+        String value2ParseWithDecimal = "99.9";
         double parseValue  =  99;
+        double parseValueWithDecimal = 99.9;
         DecimalFormat parser = new DecimalFormat();
         double result;
         boolean hasDecimalPoint;
@@ -4414,6 +4136,13 @@ public class NumberFormatTest extends TestFmwk {
                 TestFmwk.errln("Parsing " + value2Parse + " should have succeeded with " + testPattern[i] +
                             " and isDecimalPointMatchRequired set to: " + parser.isDecimalPatternMatchRequired());
             }
+            try {
+                result = parser.parse(value2ParseWithDecimal).doubleValue();
+                assertEquals("wrong parsed value", parseValueWithDecimal, result);
+            } catch (ParseException e) {
+                TestFmwk.errln("Parsing " + value2ParseWithDecimal + " should have succeeded with " + testPattern[i] +
+                            " and isDecimalPointMatchRequired set to: " + parser.isDecimalPatternMatchRequired());
+            }
 
             parser.setDecimalPatternMatchRequired(true);
             try {
@@ -4425,31 +4154,23 @@ public class NumberFormatTest extends TestFmwk {
             } catch (ParseException e) {
                     // OK, should fail
             }
+            try {
+                result = parser.parse(value2ParseWithDecimal).doubleValue();
+                if(!hasDecimalPoint){
+                    TestFmwk.errln("Parsing " + value2ParseWithDecimal + " should NOT have succeeded with " + testPattern[i] +
+                            " and isDecimalPointMatchRequired set to: " + parser.isDecimalPatternMatchRequired());
+                }
+            } catch (ParseException e) {
+                    // OK, should fail
+            }
         }
-
-    }
-
-
-    //TODO(junit): investigate
-    @Test
-    public void TestDataDrivenICU() {
-        DataDrivenNumberFormatTestUtility.runSuite(
-                "numberformattestspecification.txt", ICU);
     }
 
-    //TODO(junit): investigate
-    @Test
-    public void TestDataDrivenJDK() {
-        DataDrivenNumberFormatTestUtility.runSuite(
-                "numberformattestspecification.txt", JDK);
-    }
-
-
     @Test
     public void TestCurrFmtNegSameAsPositive() {
         DecimalFormatSymbols decfmtsym = DecimalFormatSymbols.getInstance(Locale.US);
         decfmtsym.setMinusSign('\u200B'); // ZERO WIDTH SPACE, in ICU4J cannot set to empty string
-        DecimalFormat decfmt = new DecimalFormat("\u00A4#,##0.00;\u00A4#,##0.00", decfmtsym);
+        DecimalFormat decfmt = new DecimalFormat("\u00A4#,##0.00;-\u00A4#,##0.00", decfmtsym);
         String currFmtResult = decfmt.format(-100.0);
         if (!currFmtResult.equals("\u200B$100.00")) {
             errln("decfmt.toPattern results wrong, expected \u200B$100.00, got " + currFmtResult);
@@ -4458,7 +4179,7 @@ public class NumberFormatTest extends TestFmwk {
 
     @Test
     public void TestNumberFormatTestDataToString() {
-        new NumberFormatTestData().toString();
+        new DataDrivenNumberFormatTestData().toString();
     }
 
    // Testing for Issue 11805.
@@ -4603,9 +4324,7 @@ public class NumberFormatTest extends TestFmwk {
                 result.get(i).value);
           }
         }
-        // TODO: restore when #11914 is fixed.
-        // assertTrue("Comparing vector results for " + formattedOutput,
-        //    expected.containsAll(result));
+        assertTrue("Comparing vector results for " + formattedOutput, expected.containsAll(result));
     }
 
     // Testing for Issue 11914, missing FieldPositions for some field types.
@@ -4905,30 +4624,504 @@ public class NumberFormatTest extends TestFmwk {
     public void TestStringSymbols() {
         DecimalFormatSymbols symbols = new DecimalFormatSymbols(ULocale.US);
 
+        // Attempt digits with multiple code points.
         String[] customDigits = {"(0)", "(1)", "(2)", "(3)", "(4)", "(5)", "(6)", "(7)", "(8)", "(9)"};
         symbols.setDigitStrings(customDigits);
-        symbols.setDecimalSeparatorString("~~");
-        symbols.setGroupingSeparatorString("^^");
-
         DecimalFormat fmt = new DecimalFormat("#,##0.0#", symbols);
+        expect2(fmt, 1234567.89, "(1),(2)(3)(4),(5)(6)(7).(8)(9)");
+
+        // Scientific notation should work.
+        fmt.applyPattern("@@@E0");
+        expect2(fmt, 1230000, "(1).(2)(3)E(6)");
 
-        expect2(fmt, 1234567.89, "(1)^^(2)(3)(4)^^(5)(6)(7)~~(8)(9)");
+        // Grouping and decimal with multiple code points are not supported during parsing.
+        symbols.setDecimalSeparatorString("~~");
+        symbols.setGroupingSeparatorString("^^");
+        fmt.setDecimalFormatSymbols(symbols);
+        fmt.applyPattern("#,##0.0#");
+        assertEquals("Custom decimal and grouping separator string with multiple characters",
+                fmt.format(1234567.89), "(1)^^(2)(3)(4)^^(5)(6)(7)~~(8)(9)");
+
+        // Digits starting at U+1D7CE MATHEMATICAL BOLD DIGIT ZERO
+        // These are all single code points, so parsing will work.
+        for (int i=0; i<10; i++) customDigits[i] = new String(Character.toChars(0x1D7CE+i));
+        symbols.setDigitStrings(customDigits);
+        symbols.setDecimalSeparatorString("😁");
+        symbols.setGroupingSeparatorString("😎");
+        fmt.setDecimalFormatSymbols(symbols);
+        expect2(fmt, 1234.56, "𝟏😎𝟐𝟑𝟒😁𝟓𝟔");
     }
 
     @Test
     public void TestArabicCurrencyPatternInfo() {
         ULocale arLocale = new ULocale("ar");
+
         DecimalFormatSymbols symbols = new DecimalFormatSymbols(arLocale);
         String currSpacingPatn = symbols.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_CURRENCY_MATCH, true);
         if (currSpacingPatn==null || currSpacingPatn.length() == 0) {
             errln("locale ar, getPatternForCurrencySpacing returns null or 0-length string");
         }
-        
+
         DecimalFormat currAcctFormat = (DecimalFormat)NumberFormat.getInstance(arLocale, NumberFormat.ACCOUNTINGCURRENCYSTYLE);
         String currAcctPatn = currAcctFormat.toPattern();
         if (currAcctPatn==null || currAcctPatn.length() == 0) {
             errln("locale ar, toPattern for ACCOUNTINGCURRENCYSTYLE returns null or 0-length string");
         }
     }
+
+    @Test
+    public void Test10436() {
+        DecimalFormat df = (DecimalFormat) NumberFormat.getInstance(Locale.ENGLISH);
+        df.setRoundingMode(MathContext.ROUND_CEILING);
+        df.setMinimumFractionDigits(0);
+        df.setMaximumFractionDigits(0);
+        assertEquals("-.99 should round toward infinity", "-0", df.format(-0.99));
+    }
+
+    @Test
+    public void Test10765() {
+        NumberFormat fmt = NumberFormat.getInstance(new ULocale("en"));
+        fmt.setMinimumIntegerDigits(10);
+        FieldPosition pos = new FieldPosition(NumberFormat.Field.GROUPING_SEPARATOR);
+        fmt.format(1234, new StringBuffer(), pos);
+        assertEquals("FieldPosition should report the first occurence", 1, pos.getBeginIndex());
+        assertEquals("FieldPosition should report the first occurence", 2, pos.getEndIndex());
+    }
+
+    @Test
+    public void Test10997() {
+        NumberFormat fmt = NumberFormat.getCurrencyInstance(new ULocale("en-US"));
+        fmt.setMinimumFractionDigits(4);
+        fmt.setMaximumFractionDigits(4);
+        String str1 = fmt.format(new CurrencyAmount(123.45, Currency.getInstance("USD")));
+        String str2 = fmt.format(new CurrencyAmount(123.45, Currency.getInstance("EUR")));
+        assertEquals("minFrac 4 should be respected in default currency", "$123.4500", str1);
+        assertEquals("minFrac 4 should be respected in different currency", "€123.4500", str2);
+    }
+
+    @Test
+    public void Test11020() {
+        DecimalFormatSymbols sym = new DecimalFormatSymbols(ULocale.FRANCE);
+        DecimalFormat fmt = new DecimalFormat("0.05E0", sym);
+        String result = fmt.format(12301.2).replace('\u00a0', ' ');
+        assertEquals("Rounding increment should be applied after magnitude scaling", "1,25E4", result);
+    }
+
+    @Test
+    public void Test11025() {
+        String pattern = "¤¤ **####0.00";
+        DecimalFormatSymbols sym = new DecimalFormatSymbols(ULocale.FRANCE);
+        DecimalFormat fmt = new DecimalFormat(pattern, sym);
+        String result = fmt.format(433.0);
+        assertEquals("Number should be padded to 11 characters", "EUR *433,00", result);
+    }
+
+    @Test
+    public void Test11640() {
+        DecimalFormat df = (DecimalFormat) NumberFormat.getInstance();
+        df.applyPattern("¤¤¤ 0");
+        String result = df.getPositivePrefix();
+        assertEquals("Triple-currency should give long name on getPositivePrefix", "US dollars ", result);
+    }
+
+    @Test
+    public void Test11645() {
+        String pattern = "#,##0.0#";
+        DecimalFormat fmt = (DecimalFormat) NumberFormat.getInstance();
+        fmt.applyPattern(pattern);
+        DecimalFormat fmtCopy;
+
+        final int newMultiplier = 37;
+        fmtCopy = (DecimalFormat) fmt.clone();
+        assertNotEquals("Value before setter", fmtCopy.getMultiplier(), newMultiplier);
+        fmtCopy.setMultiplier(newMultiplier);
+        assertEquals("Value after setter", fmtCopy.getMultiplier(), newMultiplier);
+        fmtCopy.applyPattern(pattern);
+        assertEquals("Value after applyPattern", fmtCopy.getMultiplier(), newMultiplier);
+        assertFalse("multiplier", fmt.equals(fmtCopy));
+
+        final int newRoundingMode = RoundingMode.CEILING.ordinal();
+        fmtCopy = (DecimalFormat) fmt.clone();
+        assertNotEquals("Value before setter", fmtCopy.getRoundingMode(), newRoundingMode);
+        fmtCopy.setRoundingMode(newRoundingMode);
+        assertEquals("Value after setter", fmtCopy.getRoundingMode(), newRoundingMode);
+        fmtCopy.applyPattern(pattern);
+        assertEquals("Value after applyPattern", fmtCopy.getRoundingMode(), newRoundingMode);
+        assertFalse("roundingMode", fmt.equals(fmtCopy));
+
+        final Currency newCurrency = Currency.getInstance("EAT");
+        fmtCopy = (DecimalFormat) fmt.clone();
+        assertNotEquals("Value before setter", fmtCopy.getCurrency(), newCurrency);
+        fmtCopy.setCurrency(newCurrency);
+        assertEquals("Value after setter", fmtCopy.getCurrency(), newCurrency);
+        fmtCopy.applyPattern(pattern);
+        assertEquals("Value after applyPattern", fmtCopy.getCurrency(), newCurrency);
+        assertFalse("currency", fmt.equals(fmtCopy));
+
+        final CurrencyUsage newCurrencyUsage = CurrencyUsage.CASH;
+        fmtCopy = (DecimalFormat) fmt.clone();
+        assertNotEquals("Value before setter", fmtCopy.getCurrencyUsage(), newCurrencyUsage);
+        fmtCopy.setCurrencyUsage(CurrencyUsage.CASH);
+        assertEquals("Value after setter", fmtCopy.getCurrencyUsage(), newCurrencyUsage);
+        fmtCopy.applyPattern(pattern);
+        assertEquals("Value after applyPattern", fmtCopy.getCurrencyUsage(), newCurrencyUsage);
+        assertFalse("currencyUsage", fmt.equals(fmtCopy));
+    }
+
+    @Test
+    public void Test11646() {
+        DecimalFormatSymbols symbols = new DecimalFormatSymbols(new ULocale("en_US"));
+        String pattern = "\u00a4\u00a4\u00a4 0.00 %\u00a4\u00a4";
+        DecimalFormat fmt = new DecimalFormat(pattern, symbols);
+
+        // Test equality with affixes. set affix methods can't capture special
+        // characters which is why equality should fail.
+        {
+          DecimalFormat fmtCopy = (DecimalFormat) fmt.clone();
+          assertEquals("", fmt, fmtCopy);
+          fmtCopy.setPositivePrefix(fmtCopy.getPositivePrefix());
+          assertNotEquals("", fmt, fmtCopy);
+        }
+        {
+          DecimalFormat fmtCopy = (DecimalFormat) fmt.clone();
+          assertEquals("", fmt, fmtCopy);
+          fmtCopy.setPositiveSuffix(fmtCopy.getPositiveSuffix());
+          assertNotEquals("", fmt, fmtCopy);
+        }
+        {
+          DecimalFormat fmtCopy = (DecimalFormat) fmt.clone();
+          assertEquals("", fmt, fmtCopy);
+          fmtCopy.setNegativePrefix(fmtCopy.getNegativePrefix());
+          assertNotEquals("", fmt, fmtCopy);
+        }
+        {
+          DecimalFormat fmtCopy = (DecimalFormat) fmt.clone();
+          assertEquals("", fmt, fmtCopy);
+          fmtCopy.setNegativeSuffix(fmtCopy.getNegativeSuffix());
+          assertNotEquals("", fmt, fmtCopy);
+        }
+    }
+
+    @Test
+    public void Test11648() {
+        DecimalFormat df = new DecimalFormat("0.00");
+        df.setScientificNotation(true);
+        String pat = df.toPattern();
+        assertEquals("A valid scientific notation pattern should be produced", "0.00E0", pat);
+    }
+
+    @Test
+    public void Test11649() {
+        String pattern = "\u00a4\u00a4\u00a4 0.00";
+        DecimalFormat fmt = new DecimalFormat(pattern);
+        fmt.setCurrency(Currency.getInstance("USD"));
+        assertEquals("Triple currency sign should format long name", "US dollars 12.34", fmt.format(12.34));
+
+        String newPattern = fmt.toPattern();
+        assertEquals("Should produce a valid pattern", pattern, newPattern);
+
+        DecimalFormat fmt2 = new DecimalFormat(newPattern);
+        fmt2.setCurrency(Currency.getInstance("USD"));
+        assertEquals("Triple currency sign pattern should round-trip", "US dollars 12.34", fmt2.format(12.34));
+    }
+
+    @Test
+    public void Test11686() {
+        DecimalFormat df = new DecimalFormat();
+        df.setPositiveSuffix("0K");
+        df.setNegativeSuffix("0N");
+        expect2(df, 123, "1230K");
+        expect2(df, -123, "1230N");
+    }
+
+    @Test
+    public void Test11839() {
+        DecimalFormatSymbols dfs = new DecimalFormatSymbols(ULocale.ENGLISH);
+        dfs.setMinusSign('∸');
+        dfs.setPlusSign('∔'); //  ∔  U+2214 DOT PLUS
+        DecimalFormat df = new DecimalFormat("0.00+;0.00-", dfs);
+        String result = df.format(-1.234);
+        assertEquals("Locale-specific minus sign should be used", "1.23∸", result);
+        result = df.format(1.234);
+        assertEquals("Locale-specific plus sign should be used", "1.23∔", result);
+    }
+
+    @Test
+    public void Test12753() {
+        ULocale locale = new ULocale("en-US");
+        DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale);
+        symbols.setDecimalSeparator('*');
+        DecimalFormat df = new DecimalFormat("0.00", symbols);
+        df.setDecimalPatternMatchRequired(true);
+        try {
+            df.parse("123");
+            fail("Parsing integer succeeded even though setDecimalPatternMatchRequired was set");
+        } catch (ParseException e) {
+            // Parse failed (expected)
+        }
+    }
+
+    @Test
+    public void Test12962() {
+        String pat = "**0.00";
+        DecimalFormat df = new DecimalFormat(pat);
+        String newPat = df.toPattern();
+        assertEquals("Format width changed upon calling applyPattern", pat.length(), newPat.length());
+    }
+
+    @Test
+    public void Test10354() {
+        DecimalFormatSymbols dfs = new DecimalFormatSymbols();
+        dfs.setNaN("");
+        DecimalFormat df = new DecimalFormat();
+        df.setDecimalFormatSymbols(dfs);
+        try {
+            df.formatToCharacterIterator(Double.NaN);
+            // pass
+        } catch (IllegalArgumentException e) {
+            throw new AssertionError(e);
+        }
+    }
+
+    @Test
+    public void Test11913() {
+        NumberFormat df = DecimalFormat.getInstance();
+        String result = df.format(new BigDecimal("1.23456789E400"));
+        assertEquals("Should format more than 309 digits", "12,345,678", result.substring(0, 10));
+        assertEquals("Should format more than 309 digits", 534, result.length());
+    }
+
+    @Test
+    public void Test12045() {
+        if (logKnownIssue("12045", "XSU is missing from fr")) { return; }
+
+        NumberFormat nf = NumberFormat.getInstance(new ULocale("fr"), NumberFormat.PLURALCURRENCYSTYLE);
+        ParsePosition ppos = new ParsePosition(0);
+        try {
+            CurrencyAmount result = nf.parseCurrency("2,34 XSU", ppos);
+            assertEquals("Parsing should succeed on XSU",
+                         new CurrencyAmount(2.34, Currency.getInstance("XSU")), result);
+            // pass
+        } catch (Exception e) {
+            throw new AssertionError("Should have been able to parse XSU", e);
+        }
+    }
+
+    @Test
+    public void Test11739() {
+        NumberFormat nf = NumberFormat.getCurrencyInstance(new ULocale("sr_BA"));
+        ((DecimalFormat) nf).applyPattern("0.0 ¤¤¤");
+        ParsePosition ppos = new ParsePosition(0);
+        CurrencyAmount result = nf.parseCurrency("1.500 амерички долар", ppos);
+        assertEquals("Should parse to 1500 USD", new CurrencyAmount(1500, Currency.getInstance("USD")), result);
+    }
+
+    @Test
+    public void Test11647() {
+        DecimalFormat df = new DecimalFormat();
+        df.applyPattern("¤¤¤¤#");
+        String actual = df.format(123);
+        assertEquals("Should replace 4 currency signs with U+FFFD", "\uFFFD123", actual);
+    }
+
+    @Test
+    public void Test12567() {
+        DecimalFormat df1 = (DecimalFormat) NumberFormat.getInstance(NumberFormat.PLURALCURRENCYSTYLE);
+        DecimalFormat df2 = (DecimalFormat) NumberFormat.getInstance(NumberFormat.NUMBERSTYLE);
+        df2.setCurrency(df1.getCurrency());
+        df2.setCurrencyPluralInfo(df1.getCurrencyPluralInfo());
+        df1.applyPattern("0.00");
+        df2.applyPattern("0.00");
+        assertEquals("df1 == df2", df1, df2);
+        assertEquals("df2 == df1", df2, df1);
+        df2.setPositivePrefix("abc");
+        assertNotEquals("df1 != df2", df1, df2);
+        assertNotEquals("df2 != df1", df2, df1);
+    }
+
+    @Test
+    public void testPercentZero() {
+        DecimalFormat df = (DecimalFormat) NumberFormat.getPercentInstance();
+        String actual = df.format(0);
+        assertEquals("Should have one zero digit", "0%", actual);
+    }
+
+    @Test
+    public void testCurrencyZeroRounding() {
+        DecimalFormat df = (DecimalFormat) NumberFormat.getCurrencyInstance();
+        df.setMaximumFractionDigits(0);
+        String actual = df.format(0);
+        assertEquals("Should have zero fraction digits", "$0", actual);
+    }
+
+    @Test
+    public void testCustomCurrencySymbol() {
+        DecimalFormat df = (DecimalFormat) NumberFormat.getCurrencyInstance();
+        df.setCurrency(Currency.getInstance("USD"));
+        DecimalFormatSymbols symbols = df.getDecimalFormatSymbols();
+        symbols.setCurrencySymbol("#");
+        df.setDecimalFormatSymbols(symbols);
+        String actual = df.format(123);
+        assertEquals("Should use '#' instad of '$'", "#123.00", actual);
+    }
+
+    @Test
+    public void TestBasicSerializationRoundTrip() throws IOException, ClassNotFoundException {
+        DecimalFormat df0 = new DecimalFormat("A-**#####,#00.00b¤");
+
+        // Write to byte stream
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        ObjectOutputStream oos = new ObjectOutputStream(baos);
+        oos.writeObject(df0);
+        oos.flush();
+        baos.close();
+        byte[] bytes = baos.toByteArray();
+
+        // Read from byte stream
+        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
+        Object obj = ois.readObject();
+        ois.close();
+        DecimalFormat df1 = (DecimalFormat) obj;
+
+        // Test equality
+        assertEquals("Did not round-trip through serialization", df0, df1);
+
+        // Test basic functionality
+        String str0 = df0.format(12345.67);
+        String str1 = df1.format(12345.67);
+        assertEquals("Serialized formatter does not produce same output", str0, str1);
+    }
+
+    @Test
+    public void testGetSetCurrency() {
+        DecimalFormat df = new DecimalFormat("¤#");
+        assertEquals("Currency should start out null", null, df.getCurrency());
+        Currency curr = Currency.getInstance("EUR");
+        df.setCurrency(curr);
+        assertEquals("Currency should equal EUR after set", curr, df.getCurrency());
+        String result = df.format(123);
+        assertEquals("Currency should format as expected in EUR", "€123.00", result);
+    }
+
+    @Test
+    public void testRoundingModeSetters() {
+        DecimalFormat df1 = new DecimalFormat();
+        DecimalFormat df2 = new DecimalFormat();
+
+        df1.setRoundingMode(java.math.BigDecimal.ROUND_CEILING);
+        assertNotEquals("Rounding mode was set to a non-default", df1, df2);
+        df2.setRoundingMode(com.ibm.icu.math.BigDecimal.ROUND_CEILING);
+        assertEquals("Rounding mode from icu.math and java.math should be the same", df1, df2);
+        df2.setRoundingMode(java.math.RoundingMode.CEILING.ordinal());
+        assertEquals("Rounding mode ordinal from java.math.RoundingMode should be the same", df1, df2);
+    }
+
+    @Test
+    public void testSignificantDigitsMode() {
+        String[][] allExpected = {
+              {"12340.0", "12340.0", "12340.0"},
+              {"1234.0", "1234.0", "1234.0"},
+              {"123.4", "123.4", "123.4"},
+              {"12.34", "12.34", "12.34"},
+              {"1.234", "1.23", "1.23"},
+              {"0.1234", "0.12", "0.123"},
+              {"0.01234", "0.01", "0.0123"},
+              {"0.001234", "0.00", "0.00123"}
+        };
+
+        DecimalFormat df = new DecimalFormat();
+        df.setMinimumFractionDigits(1);
+        df.setMaximumFractionDigits(2);
+        df.setMinimumSignificantDigits(3);
+        df.setMaximumSignificantDigits(4);
+        df.setGroupingUsed(false);
+
+        SignificantDigitsMode[] modes = new SignificantDigitsMode[] {
+                SignificantDigitsMode.OVERRIDE_MAXIMUM_FRACTION,
+                SignificantDigitsMode.RESPECT_MAXIMUM_FRACTION,
+                SignificantDigitsMode.ENSURE_MINIMUM_SIGNIFICANT
+        };
+
+        for (double d = 12340.0, i=0; d > 0.001; d /= 10, i++) {
+            for (int j=0; j<modes.length; j++) {
+                SignificantDigitsMode mode = modes[j];
+                df.setSignificantDigitsMode(mode);
+                String expected = allExpected[(int)i][j];
+                String actual = df.format(d);
+                assertEquals("Significant digits mode getter is broken",
+                        mode, df.getSignificantDigitsMode());
+                assertEquals("Significant digits output differs for "+i+", "+j,
+                        expected, actual);
+            }
+        }
+    }
+
+    @Test
+    public void testParseNoExponent() throws ParseException {
+        DecimalFormat df = new DecimalFormat();
+        assertEquals("Parse no exponent has wrong default", false, df.getParseNoExponent());
+        Number result1 = df.parse("123E4");
+        df.setParseNoExponent(true);
+        assertEquals("Parse no exponent getter is broken", true, df.getParseNoExponent());
+        Number result2 = df.parse("123E4");
+        assertEquals("Exponent did not parse before setParseNoExponent", result1, new Long(1230000));
+        assertEquals("Exponent parsed after setParseNoExponent", result2, new Long(123));
+    }
+
+    @Test
+    public void testMinimumGroupingDigits() {
+        String[][] allExpected = {
+                {"123", "123"},
+                {"1,230", "1230"},
+                {"12,300", "12,300"},
+                {"1,23,000", "1,23,000"}
+        };
+
+        DecimalFormat df = new DecimalFormat("#,##,##0");
+        assertEquals("Minimum grouping digits has wrong default", 1, df.getMinimumGroupingDigits());
+
+        for (int l = 123, i=0; l <= 123000; l *= 10, i++) {
+            df.setMinimumGroupingDigits(1);
+            assertEquals("Minimum grouping digits getter is broken", 1, df.getMinimumGroupingDigits());
+            String actual = df.format(l);
+            assertEquals("Output is wrong for 1, "+i, allExpected[i][0], actual);
+            df.setMinimumGroupingDigits(2);
+            assertEquals("Minimum grouping digits getter is broken", 2, df.getMinimumGroupingDigits());
+            actual = df.format(l);
+            assertEquals("Output is wrong for 2, "+i, allExpected[i][1], actual);
+        }
+    }
+
+    @Test
+    public void testParseCaseSensitive() {
+        String[] patterns = {"a#b", "A#B"};
+        String[] inputs = {"a500b", "A500b", "a500B", "a500e10b", "a500E10b"};
+        int[][] expectedParsePositions = {
+                {5, 5, 5, 8, 8}, // case insensitive, pattern 0
+                {5, 0, 4, 4, 8}, // case sensitive, pattern 0
+                {5, 5, 5, 8, 8}, // case insensitive, pattern 1
+                {0, 4, 0, 0, 0}, // case sensitive, pattern 1
+        };
+
+        for (int p = 0; p < patterns.length; p++) {
+            String pat = patterns[p];
+            DecimalFormat df = new DecimalFormat(pat);
+            assertEquals("parseCaseSensitive default is wrong", false, df.getParseCaseSensitive());
+            for (int i = 0; i < inputs.length; i++) {
+                String inp = inputs[i];
+                df.setParseCaseSensitive(false);
+                assertEquals("parseCaseSensitive getter is broken", false, df.getParseCaseSensitive());
+                ParsePosition actualInsensitive = new ParsePosition(0);
+                df.parse(inp, actualInsensitive);
+                assertEquals("Insensitive, pattern "+p+", input "+i,
+                        expectedParsePositions[p*2][i], actualInsensitive.getIndex());
+                df.setParseCaseSensitive(true);
+                assertEquals("parseCaseSensitive getter is broken", true, df.getParseCaseSensitive());
+                ParsePosition actualSensitive = new ParsePosition(0);
+                df.parse(inp, actualSensitive);
+                assertEquals("Sensitive, pattern "+p+", input "+i,
+                        expectedParsePositions[p*2+1][i], actualSensitive.getIndex());
+            }
+        }
+    }
 }
index b3b9fb7d28e8c09e6b98940f6786f1618e35dbe3..e1ba90826bc42b607c61053fcbe16d5dd32950fa 100644 (file)
@@ -18,12 +18,12 @@ rt:  "0.###"  1.0         "1"
 # Basics
 fp:  "0.####" 0.10005     "0.1"        0.1
 fp:  -        0.10006     "0.1001"     0.1001
-pat: -        "#0.####"
+pat: -        "0.####"
 fp:  "#.####" 0.10005     "0.1"        0.1
-pat: -        "#0.####"
+pat: -        "0.####"
 
 rt:  "0"      1234        "1234"
-pat: -        "#0"
+pat: -        "0"
 
 # Significant digits                                                  
 fp:  "@@@"    1.234567    "1.23"       1.23
@@ -68,7 +68,8 @@ pat: "##,@@##" "#,@@##"
 pat: "##@@##"  "@@##"
 
 pat: "@@.@@"  err  # decimal sep. disallowed in sig. digits
-pat: "@#@"    err  # only one cluster of sig. digits
+# The new pattern parser treats this the same as "@@#"
+#pat: "@#@"    err  # only one cluster of sig. digits
 pat: "@@0"    err  # either @ or 0, not both
 
 # NumberRegression/Test4140009
index 0c4c73d6156b73cce7d84548a60848785fc2673f..5a2e63e967fb87380d575207ff2330f2286b68b9 100644 (file)
@@ -6,11 +6,11 @@
  * Corporation and others.  All Rights Reserved.
  **/
 
-/** 
+/**
  * Port From:   JDK 1.4b1 : java.text.Format.NumberRegression
  * Source File: java/text/format/NumberRegression.java
  **/
+
 /**
  * @test 1.49 01/05/21
  * @bug 4052223 4059870 4061302 4062486 4066646 4068693 4070798 4071005 4071014
@@ -28,7 +28,6 @@ package com.ibm.icu.dev.test.format;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.InvalidObjectException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
@@ -424,24 +423,24 @@ public class NumberRegressionTests extends TestFmwk {
 
         try {
             dfFoo.applyPattern("0000;-000");
-            if (!dfFoo.toPattern().equals("#0000"))
+            if (!dfFoo.toPattern().equals("0000"))
                 errln("dfFoo.toPattern : " + dfFoo.toPattern());
             logln(dfFoo.format(42));
             logln(dfFoo.format(-42));
             dfFoo.applyPattern("000;-000");
-            if (!dfFoo.toPattern().equals("#000"))
+            if (!dfFoo.toPattern().equals("000"))
                 errln("dfFoo.toPattern : " + dfFoo.toPattern());
             logln(dfFoo.format(42));
             logln(dfFoo.format(-42));
 
             dfFoo.applyPattern("000;-0000");
-            if (!dfFoo.toPattern().equals("#000"))
+            if (!dfFoo.toPattern().equals("000"))
                 errln("dfFoo.toPattern : " + dfFoo.toPattern());
             logln(dfFoo.format(42));
             logln(dfFoo.format(-42));
 
             dfFoo.applyPattern("0000;-000");
-            if (!dfFoo.toPattern().equals("#0000"))
+            if (!dfFoo.toPattern().equals("0000"))
                 errln("dfFoo.toPattern : " + dfFoo.toPattern());
             logln(dfFoo.format(42));
             logln(dfFoo.format(-42));
@@ -987,7 +986,7 @@ public class NumberRegressionTests extends TestFmwk {
             errln("getMaximumIntegerDigits() returns " +
                 nf.getMaximumIntegerDigits());
     }
-    
+
     /**
      * Locale data should use generic currency symbol
      *
@@ -999,7 +998,7 @@ public class NumberRegressionTests extends TestFmwk {
     public void Test4122840()
     {
         Locale[] locales = NumberFormat.getAvailableLocales();
-        
+
         for (int i = 0; i < locales.length; i++) {
             ICUResourceBundle rb = (ICUResourceBundle)ICUResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME,locales[i]);
 
@@ -1014,13 +1013,13 @@ public class NumberRegressionTests extends TestFmwk {
                         " does not contain generic currency symbol:" +
                         pattern );
             }
-            
+
             // Create a DecimalFormat using the pattern we got and format a number
             DecimalFormatSymbols symbols = new DecimalFormatSymbols(locales[i]);
             DecimalFormat fmt1 = new DecimalFormat(pattern, symbols);
-            
+
             String result1 = fmt1.format(1.111);
-            
+
             //
             // Now substitute in the locale's currency symbol and create another
             // pattern.  Replace the decimal separator with the monetary separator.
@@ -1031,7 +1030,7 @@ public class NumberRegressionTests extends TestFmwk {
             for (int j = 0; j < buf.length(); j++) {
                 if (buf.charAt(j) == '\u00a4') {
                     String cur = "'" + symbols.getCurrencySymbol() + "'";
-                    buf.replace(j, j+1, cur); 
+                    buf.replace(j, j+1, cur);
                     j += cur.length() - 1;
                 }
             }
@@ -1039,16 +1038,22 @@ public class NumberRegressionTests extends TestFmwk {
             DecimalFormat fmt2 = new DecimalFormat(buf.toString(), symbols);
 
             // Actual width of decimal fractions and rounding option are inherited
-            // from the currency, not the pattern itself.  So we need to force 
+            // from the currency, not the pattern itself.  So we need to force
             // maximum/minimumFractionDigits and rounding option for the second
             // DecimalForamt instance.  The fix for ticket#7282 requires this test
             // code change to make it work properly.
-            fmt2.setMaximumFractionDigits(fmt1.getMaximumFractionDigits());
-            fmt2.setMinimumFractionDigits(fmt1.getMinimumFractionDigits());
-            fmt2.setRoundingIncrement(fmt1.getRoundingIncrement());
+            if (symbols.getCurrency() != null) {
+                fmt2.setMaximumFractionDigits(symbols.getCurrency().getDefaultFractionDigits());
+                fmt2.setMinimumFractionDigits(symbols.getCurrency().getDefaultFractionDigits());
+                fmt2.setRoundingIncrement(symbols.getCurrency().getRoundingIncrement());
+            } else {
+                fmt2.setMaximumFractionDigits(fmt1.getMinimumFractionDigits());
+                fmt2.setMinimumFractionDigits(fmt1.getMaximumFractionDigits());
+                fmt2.setRoundingIncrement(fmt1.getRoundingIncrement());
+            }
 
             String result2 = fmt2.format(1.111);
-            
+
             // NOTE: en_IN is a special case (ChoiceFormat currency display name)
             if (!result1.equals(result2) &&
                 !locales[i].toString().equals("en_IN")) {
@@ -1057,7 +1062,7 @@ public class NumberRegressionTests extends TestFmwk {
             }
         }
     }
-     
+
     /**
      * DecimalFormat.format() delivers wrong string.
      */
@@ -1086,7 +1091,7 @@ public class NumberRegressionTests extends TestFmwk {
     @Test
     public void Test4134034() {
         DecimalFormat nf = new DecimalFormat("##,###,###.00");
-        
+
         String f = nf.format(9.02);
         if (f.equals("9.02")) logln(f + " ok"); else errln("9.02 -> " + f + "; want 9.02");
 
@@ -1100,17 +1105,17 @@ public class NumberRegressionTests extends TestFmwk {
      *
      * JDK 1.1.6 Bug, did NOT occur in 1.1.5
      * Possibly related to bug 4125885.
-     * 
+     *
      * This class demonstrates a regression in version 1.1.6
      * of DecimalFormat class.
-     * 
+     *
      * 1.1.6 Results
      * Value 1.2 Format #.00 Result '01.20' !!!wrong
      * Value 1.2 Format 0.00 Result '001.20' !!!wrong
      * Value 1.2 Format 00.00 Result '0001.20' !!!wrong
      * Value 1.2 Format #0.0# Result '1.2'
      * Value 1.2 Format #0.00 Result '001.20' !!!wrong
-     * 
+     *
      * 1.1.5 Results
      * Value 1.2 Format #.00 Result '1.20'
      * Value 1.2 Format 0.00 Result '1.20'
@@ -1204,12 +1209,12 @@ public class NumberRegressionTests extends TestFmwk {
                 String out = nf.format(pi);
                 String pat = nf.toPattern();
                 double val = nf.parse(out).doubleValue();
-            
+
                 nf.applyPattern(pat);
                 String out2 = nf.format(pi);
                 String pat2 = nf.toPattern();
                 double val2 = nf.parse(out2).doubleValue();
-            
+
                 if (!pat.equals(pat2))
                     errln("Fail with \"" + PATS[i] + "\": Patterns should concur, \"" +
                           pat + "\" vs. \"" + pat2 + "\"");
@@ -1246,7 +1251,9 @@ public class NumberRegressionTests extends TestFmwk {
         logln("Applying pattern \"" + pattern + "\"");
         sdf.applyPattern(pattern);
         int minIntDig = sdf.getMinimumIntegerDigits();
-        if (minIntDig != 0) {
+        // In ICU 58 and older, this case returned 0.
+        // Now it returns 1 instead, since the pattern parser enforces at least 1 min digit.
+        if (minIntDig != 1) {
             errln("Test failed");
             errln(" Minimum integer digits : " + minIntDig);
             errln(" new pattern: " + sdf.toPattern());
@@ -1295,9 +1302,7 @@ public class NumberRegressionTests extends TestFmwk {
             e.printStackTrace();
         }
         logln("The string " + s + " parsed as " + n);
-        if (n.doubleValue() != dbl) {
-            errln("Round trip failure");
-        }
+        assertEquals("Round trip failure", dbl, n.doubleValue());
     }
 
     /**
@@ -1327,7 +1332,7 @@ public class NumberRegressionTests extends TestFmwk {
     @Test
     public void Test4167494() throws Exception {
         NumberFormat fmt = NumberFormat.getInstance(Locale.US);
-        
+
         double a = Double.MAX_VALUE;
         String s = fmt.format(a);
         double b = fmt.parse(s).doubleValue();
@@ -1379,15 +1384,15 @@ public class NumberRegressionTests extends TestFmwk {
     @Test
     public void Test4176114() {
         String[] DATA = {
-            "00", "#00",
-            "000", "#000", // No grouping
-            "#000", "#000", // No grouping
+            "00", "00",
+            "000", "000", // No grouping
+            "#000", "000", // No grouping
             "#,##0", "#,##0",
             "#,000", "#,000",
-            "0,000", "#0,000",
-            "00,000", "#00,000",
-            "000,000", "#,000,000",
-            "0,000,000,000,000.0000", "#0,000,000,000,000.0000", // Reported
+            "0,000", "0,000",
+            "00,000", "00,000",
+            "000,000", "000,000",
+            "0,000,000,000,000.0000", "0,000,000,000,000.0000", // Reported
         };
         for (int i=0; i<DATA.length; i+=2) {
             DecimalFormat df = new DecimalFormat(DATA[i]);
@@ -1426,98 +1431,6 @@ public class NumberRegressionTests extends TestFmwk {
         }
     }
 
-    @Test
-    public void Test4185761() throws IOException, ClassNotFoundException {
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        ObjectOutputStream oos = new ObjectOutputStream(baos);
-        
-        NumberFormat nf = NumberFormat.getInstance(Locale.US);
-
-    // Set special values we are going to search for in the output byte stream
-    // These are all legal values.
-        nf.setMinimumIntegerDigits(0x111); // Keep under 309
-        nf.setMaximumIntegerDigits(0x112); // Keep under 309
-        nf.setMinimumFractionDigits(0x113); // Keep under 340
-        nf.setMaximumFractionDigits(0x114); // Keep under 340
-        
-        oos.writeObject(nf);
-        oos.flush();
-        baos.close();
-        
-        byte[] bytes = baos.toByteArray();
-
-    // Scan for locations of min/max int/fract values in the byte array.
-    // At the moment (ICU4J 2.1), there is only one instance of each target pair
-    // in the byte stream, so assume first match is it.  Note this is not entirely
-    // failsafe, and needs to be checked if we change the package or structure of
-    // this class.
-    // Current positions are 890, 880, 886, 876
-        int[] offsets = new int[4];
-        for (int i = 0; i < bytes.length - 1; ++i) {
-            if (bytes[i] == 0x01) { // high byte
-                for (int j = 0; j < offsets.length; ++j) {
-                    if ((offsets[j] == 0) && (bytes[i+1] == (0x11 + j))) { // low byte
-                        offsets[j] = i;
-                        break;
-                    }
-                }
-            }
-        }
-
-        {
-            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
-            Object o = ois.readObject();
-            ois.close();
-            
-            if (!nf.equals(o)) {
-                errln("Fail: NumberFormat serialization/equality bug");
-            } else {
-                logln("NumberFormat serialization/equality is OKAY.");
-            }
-        }
-
-    // Change the values in the byte stream so that min > max.
-    // Numberformat should catch this and throw an exception.
-        for (int i = 0; i < offsets.length; ++i) {
-            bytes[offsets[i]] = (byte)(4 - i);
-        }
-
-        {
-            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
-            try {
-                NumberFormat format = (NumberFormat) ois.readObject();
-                logln("format: " + format.format(1234.56)); //fix "The variable is never used"
-                errln("FAIL: Deserialized bogus NumberFormat with minXDigits > maxXDigits");
-            } catch (InvalidObjectException e) {
-                logln("Ok: " + e.getMessage());
-            }
-        }
-
-    // Set values so they are too high, but min <= max
-    // Format should pass the min <= max test, and DecimalFormat should reset to current maximum
-    // (for compatibility with versions streamed out before the maximums were imposed).
-        for (int i = 0; i < offsets.length; ++i) {
-            bytes[offsets[i]] = 4;
-        }
-
-        {
-            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
-            NumberFormat format = (NumberFormat) ois.readObject();
-            //For compatibility with previous version
-            if ((format.getMaximumIntegerDigits() != 309) 
-                || format.getMaximumFractionDigits() != 340) {
-                errln("FAIL: Deserialized bogus NumberFormat with values out of range," +
-                      " intMin: " + format.getMinimumIntegerDigits() +
-                      " intMax: " + format.getMaximumIntegerDigits() +
-                      " fracMin: " + format.getMinimumFractionDigits() +
-                      " fracMax: " + format.getMaximumFractionDigits());
-            } else {
-                logln("Ok: Digit count out of range");
-            }
-        }
-    }
-
-
     /**
      * Some DecimalFormatSymbols changes are not picked up by DecimalFormat.
      * This includes the minus sign, currency symbol, international currency
@@ -1553,7 +1466,7 @@ public class NumberRegressionTests extends TestFmwk {
                   fmt.getPositiveSuffix() + ", exp ^");
         }
         sym.setPercent('%');
-        
+
         fmt.applyPattern("#\u2030");
         sym.setPerMill('^');
         fmt.setDecimalFormatSymbols(sym);
@@ -1599,7 +1512,7 @@ public class NumberRegressionTests extends TestFmwk {
             System.out.println("\n        Test skipped for release 2.2");
             return;
         }
-        
+
         // Since the pattern logic has changed, make sure that patterns round
         // trip properly.  Test stream in/out integrity too.
         Locale[] avail = NumberFormat.getAvailableLocales();
@@ -1622,11 +1535,12 @@ public class NumberRegressionTests extends TestFmwk {
                     break;
                 }
                 DecimalFormat df = (DecimalFormat) nf;
-                
+
                 // Test toPattern/applyPattern round trip
                 String pat = df.toPattern();
                 DecimalFormatSymbols symb = new DecimalFormatSymbols(avail[i]);
                 DecimalFormat f2 = new DecimalFormat(pat, symb);
+                f2.setCurrency(df.getCurrency()); // Currency does not travel with the pattern string
                 if (!df.equals(f2)) {
                     errln("FAIL: " + avail[i] + " #" + j + " -> \"" + pat +
                           "\" -> \"" + f2.toPattern() + '"');
@@ -1636,24 +1550,32 @@ public class NumberRegressionTests extends TestFmwk {
                 pat = df.toLocalizedPattern();
                 try{
                     f2.applyLocalizedPattern(pat);
-                    
+
                     String s1 = f2.format(123456);
                     String s2 = df.format(123456);
                     if(!s1.equals(s2)){
                         errln("FAIL: " + avail[i] + " #" + j + " -> localized \"" + s2 +
                                 "\" -> \"" + s2 + '"'+ " in locale "+df.getLocale(ULocale.ACTUAL_LOCALE));
-  
-                    }
-                    if (!df.equals(f2)) {
-                        errln("FAIL: " + avail[i] + " #" + j + " -> localized \"" + pat +
-                              "\" -> \"" + f2.toLocalizedPattern() + '"'+ " in locale "+df.getLocale(ULocale.ACTUAL_LOCALE));
-                        errln("s1: "+s1+" s2: "+s2);
+
                     }
-                   
+
+                    // Equality of formatter objects is NOT guaranteed across toLocalizedPattern/applyLocalizedPattern.
+                    // However, equality of relevant properties is guaranteed.
+                    assertEquals("Localized FAIL on posPrefix", df.getPositivePrefix(), f2.getPositivePrefix());
+                    assertEquals("Localized FAIL on posSuffix", df.getPositiveSuffix(), f2.getPositiveSuffix());
+                    assertEquals("Localized FAIL on negPrefix", df.getNegativePrefix(), f2.getNegativePrefix());
+                    assertEquals("Localized FAIL on negSuffix", df.getNegativeSuffix(), f2.getNegativeSuffix());
+                    assertEquals("Localized FAIL on groupingSize", df.getGroupingSize(), f2.getGroupingSize());
+                    assertEquals("Localized FAIL on secondaryGroupingSize", df.getSecondaryGroupingSize(), f2.getSecondaryGroupingSize());
+                    assertEquals("Localized FAIL on minFrac", df.getMinimumFractionDigits(), f2.getMinimumFractionDigits());
+                    assertEquals("Localized FAIL on maxFrac", df.getMaximumFractionDigits(), f2.getMaximumFractionDigits());
+                    assertEquals("Localized FAIL on minInt", df.getMinimumIntegerDigits(), f2.getMinimumIntegerDigits());
+                    assertEquals("Localized FAIL on maxInt", df.getMaximumIntegerDigits(), f2.getMaximumIntegerDigits());
+
                 }catch(IllegalArgumentException ex){
-                    errln(ex.getMessage()+" for locale "+ df.getLocale(ULocale.ACTUAL_LOCALE));
+                    throw new AssertionError("For locale " + avail[i], ex);
                 }
-                
+
 
                 // Test writeObject/readObject round trip
                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -1682,7 +1604,7 @@ public class NumberRegressionTests extends TestFmwk {
         sym = new DecimalFormatSymbols(Locale.US);
         for (int j=0; j<SPECIALS.length; ++j) {
             char special = SPECIALS[j];
-            String pat = "'" + special + "'#0'" + special + "'";
+            String pat = "'" + special + "'0'" + special + "'";
             try {
                 fmt = new DecimalFormat(pat, sym);
                 String pat2 = fmt.toPattern();
@@ -1713,7 +1635,7 @@ public class NumberRegressionTests extends TestFmwk {
             String str = Long.toString(DATA[i]);
             for (int m = 1; m <= 100; m++) {
                 fmt.setMultiplier(m);
-                long n = ((Number) fmt.parse(str)).longValue();
+                long n = fmt.parse(str).longValue();
                 if (n > 0 != DATA[i] > 0) {
                     errln("\"" + str + "\" parse(x " + fmt.getMultiplier() +
                           ") => " + n);
@@ -1735,15 +1657,15 @@ public class NumberRegressionTests extends TestFmwk {
             new Double(1.006), "1.01",
         };
         NumberFormat fmt = NumberFormat.getInstance(Locale.US);
-        fmt.setMaximumFractionDigits(2); 
+        fmt.setMaximumFractionDigits(2);
         for (int i=0; i<DATA.length; i+=2) {
             String s = fmt.format(((Double) DATA[i]).doubleValue());
             if (!s.equals(DATA[i+1])) {
-                errln("FAIL: Got " + s + ", exp " + DATA[i+1]); 
+                errln("FAIL: Got " + s + ", exp " + DATA[i+1]);
             }
         }
     }
-    
+
     /**
      * 4243011: Formatting .5 rounds to "1" instead of "0"
      */
@@ -1751,7 +1673,7 @@ public class NumberRegressionTests extends TestFmwk {
     public void Test4243011() {
         double DATA[] = {0.5, 1.5, 2.5, 3.5, 4.5};
         String EXPECTED[] = {"0.", "2.", "2.", "4.", "4."};
-        
+
         DecimalFormat format = new DecimalFormat("0.");
         for (int i = 0; i < DATA.length; i++) {
             String result = format.format(DATA[i]);
@@ -1762,7 +1684,7 @@ public class NumberRegressionTests extends TestFmwk {
             }
         }
     }
-    
+
     /**
      * 4243108: format(0.0) gives "0.1" if preceded by parse("99.99")
      */
@@ -1785,7 +1707,7 @@ public class NumberRegressionTests extends TestFmwk {
         } catch (ParseException e) {
             errln("Caught a ParseException:");
             e.printStackTrace();
-        }            
+        }
         result = f.format(0.0);
         if (result.equals("0")) {
             logln("OK: got " + result);
@@ -1793,7 +1715,7 @@ public class NumberRegressionTests extends TestFmwk {
             errln("FAIL: got " + result);
         }
     }
-    
+
     /**
      * 4330377: DecimalFormat engineering notation gives incorrect results
      */
@@ -1837,7 +1759,7 @@ public class NumberRegressionTests extends TestFmwk {
         }
         */
     }
-    
+
     /**
      * 4233840: NumberFormat does not round correctly
      */
@@ -1847,14 +1769,14 @@ public class NumberRegressionTests extends TestFmwk {
 
         NumberFormat nf = new DecimalFormat("0.##", new DecimalFormatSymbols(Locale.US));
     nf.setMinimumFractionDigits(2);
-    
+
     String result = nf.format(f);
-    
+
     if (!result.equals("0.01")) {
         errln("FAIL: input: " + f + ", expected: 0.01, got: " + result);
     }
     }
-    
+
     /**
      * 4241880: Decimal format doesnt round a double properly when the number is less than 1
      */
@@ -1916,22 +1838,28 @@ class MyNumberFormat extends NumberFormat {
      * For serialization
      */
     private static final long serialVersionUID = 1251303884737169952L;
-    public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
+    @Override
+  public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
         return new StringBuffer("");
     }
-    public StringBuffer format(long number,StringBuffer toAppendTo, FieldPosition pos) {
+    @Override
+  public StringBuffer format(long number,StringBuffer toAppendTo, FieldPosition pos) {
         return new StringBuffer("");
     }
-    public Number parse(String text, ParsePosition parsePosition) {
+    @Override
+  public Number parse(String text, ParsePosition parsePosition) {
         return new Integer(0);
     }
-    public StringBuffer format(java.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
+    @Override
+  public StringBuffer format(java.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
         return new StringBuffer("");
     }
-    public StringBuffer format(BigInteger number, StringBuffer toAppendTo, FieldPosition pos) {
+    @Override
+  public StringBuffer format(BigInteger number, StringBuffer toAppendTo, FieldPosition pos) {
         return new StringBuffer("");
     }
-    public StringBuffer format(com.ibm.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
+    @Override
+  public StringBuffer format(com.ibm.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) {
         return new StringBuffer("");
     }
 }
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/ShanesDataDrivenTester.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/ShanesDataDrivenTester.java
new file mode 100644 (file)
index 0000000..5a8af5f
--- /dev/null
@@ -0,0 +1,343 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.dev.test.format;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.ParseException;
+import java.text.ParsePosition;
+
+import com.ibm.icu.dev.test.format.DataDrivenNumberFormatTestUtility.CodeUnderTest;
+import com.ibm.icu.impl.number.Endpoint;
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.FormatQuantity1;
+import com.ibm.icu.impl.number.FormatQuantity2;
+import com.ibm.icu.impl.number.FormatQuantity3;
+import com.ibm.icu.impl.number.FormatQuantity4;
+import com.ibm.icu.impl.number.Parse;
+import com.ibm.icu.impl.number.Parse.ParseMode;
+import com.ibm.icu.impl.number.PatternString;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
+import com.ibm.icu.text.DecimalFormat;
+import com.ibm.icu.text.DecimalFormat.PropertySetter;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.util.CurrencyAmount;
+import com.ibm.icu.util.ULocale;
+
+public class ShanesDataDrivenTester extends CodeUnderTest {
+  static final String dataPath =
+      "../../../icu4j-core-tests/src/com/ibm/icu/dev/data/numberformattestspecification.txt";
+
+  public static void run() {
+    CodeUnderTest tester = new ShanesDataDrivenTester();
+    DataDrivenNumberFormatTestUtility.runFormatSuiteIncludingKnownFailures(dataPath, tester);
+  }
+
+  @Override
+  public Character Id() {
+    return 'S';
+  }
+
+  /**
+   * Runs a single formatting test. On success, returns null. On failure, returns the error. This
+   * implementation just returns null. Subclasses should override.
+   *
+   * @param tuple contains the parameters of the format test.
+   */
+  @Override
+  public String format(DataDrivenNumberFormatTestData tuple) {
+    String pattern = (tuple.pattern == null) ? "0" : tuple.pattern;
+    ULocale locale = (tuple.locale == null) ? ULocale.ENGLISH : tuple.locale;
+    Properties properties = PatternString.parseToProperties(pattern, tuple.currency != null);
+    propertiesFromTuple(tuple, properties);
+    Format fmt = Endpoint.fromBTA(properties, locale);
+    FormatQuantity q1, q2, q3;
+    if (tuple.format.equals("NaN")) {
+      q1 = q2 = new FormatQuantity1(Double.NaN);
+      q3 = new FormatQuantity2(Double.NaN);
+    } else if (tuple.format.equals("-Inf")) {
+      q1 = q2 = new FormatQuantity1(Double.NEGATIVE_INFINITY);
+      q3 = new FormatQuantity1(Double.NEGATIVE_INFINITY);
+    } else if (tuple.format.equals("Inf")) {
+      q1 = q2 = new FormatQuantity1(Double.POSITIVE_INFINITY);
+      q3 = new FormatQuantity1(Double.POSITIVE_INFINITY);
+    } else {
+      BigDecimal d = new BigDecimal(tuple.format);
+      if (d.precision() <= 16) {
+        q1 = new FormatQuantity1(d);
+        q2 = new FormatQuantity1(Double.parseDouble(tuple.format));
+        q3 = new FormatQuantity4(d);
+      } else {
+        q1 = new FormatQuantity1(d);
+        q2 = new FormatQuantity3(d);
+        q3 = new FormatQuantity4(d); // duplicate values so no null
+      }
+    }
+    String expected = tuple.output;
+    String actual1 = fmt.format(q1);
+    if (!expected.equals(actual1)) {
+      return "Expected \"" + expected + "\", got \"" + actual1 + "\" on FormatQuantity1 BigDecimal";
+    }
+    String actual2 = fmt.format(q2);
+    if (!expected.equals(actual2)) {
+      return "Expected \"" + expected + "\", got \"" + actual2 + "\" on FormatQuantity1 double";
+    }
+    String actual3 = fmt.format(q3);
+    if (!expected.equals(actual3)) {
+      return "Expected \"" + expected + "\", got \"" + actual3 + "\" on FormatQuantity4 BigDecimal";
+    }
+    return null;
+  }
+
+  /**
+   * Runs a single toPattern test. On success, returns null. On failure, returns the error. This
+   * implementation just returns null. Subclasses should override.
+   *
+   * @param tuple contains the parameters of the format test.
+   */
+  @Override
+  public String toPattern(DataDrivenNumberFormatTestData tuple) {
+    String pattern = (tuple.pattern == null) ? "0" : tuple.pattern;
+    final Properties properties;
+    DecimalFormat df;
+    try {
+      properties = PatternString.parseToProperties(pattern, tuple.currency != null);
+      propertiesFromTuple(tuple, properties);
+      // TODO: Use PatternString.propertiesToString() directly. (How to deal with CurrencyUsage?)
+      df = new DecimalFormat();
+      df.setProperties(
+          new PropertySetter() {
+            @Override
+            public void set(Properties props) {
+              props.copyFrom(properties);
+            }
+          });
+    } catch (IllegalArgumentException e) {
+      e.printStackTrace();
+      return e.getLocalizedMessage();
+    }
+
+    if (tuple.toPattern != null) {
+      String expected = tuple.toPattern;
+      String actual = df.toPattern();
+      if (!expected.equals(actual)) {
+        return "Expected toPattern='" + expected + "'; got '" + actual + "'";
+      }
+    }
+    if (tuple.toLocalizedPattern != null) {
+      String expected = tuple.toLocalizedPattern;
+      String actual = PatternString.propertiesToString(properties);
+      if (!expected.equals(actual)) {
+        return "Expected toLocalizedPattern='" + expected + "'; got '" + actual + "'";
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Runs a single parse test. On success, returns null. On failure, returns the error. This
+   * implementation just returns null. Subclasses should override.
+   *
+   * @param tuple contains the parameters of the format test.
+   */
+  @Override
+  public String parse(DataDrivenNumberFormatTestData tuple) {
+    String pattern = (tuple.pattern == null) ? "0" : tuple.pattern;
+    Properties properties;
+    ParsePosition ppos = new ParsePosition(0);
+    Number actual;
+    try {
+      properties = PatternString.parseToProperties(pattern, tuple.currency != null);
+      propertiesFromTuple(tuple, properties);
+      actual =
+          Parse.parse(
+              tuple.parse, ppos, properties, DecimalFormatSymbols.getInstance(tuple.locale));
+    } catch (IllegalArgumentException e) {
+      return "parse exception: " + e.getMessage();
+    }
+    if (actual == null && ppos.getIndex() != 0) {
+      throw new AssertionError("Error: value is null but parse position is not zero");
+    }
+    if (ppos.getIndex() == 0) {
+      return "Parse failed; got " + actual + ", but expected " + tuple.output;
+    }
+    if (tuple.output.equals("NaN")) {
+      if (!Double.isNaN(actual.doubleValue())) {
+        return "Expected NaN, but got: " + actual;
+      }
+      return null;
+    } else if (tuple.output.equals("Inf")) {
+      if (!Double.isInfinite(actual.doubleValue())
+          || Double.compare(actual.doubleValue(), 0.0) < 0) {
+        return "Expected Inf, but got: " + actual;
+      }
+      return null;
+    } else if (tuple.output.equals("-Inf")) {
+      if (!Double.isInfinite(actual.doubleValue())
+          || Double.compare(actual.doubleValue(), 0.0) > 0) {
+        return "Expected -Inf, but got: " + actual;
+      }
+      return null;
+    } else if (tuple.output.equals("fail")) {
+      return null;
+    } else if (new BigDecimal(tuple.output).compareTo(new BigDecimal(actual.toString())) != 0) {
+      return "Expected: " + tuple.output + ", got: " + actual;
+    } else {
+      return null;
+    }
+  }
+
+  /**
+   * Runs a single parse currency test. On success, returns null. On failure, returns the error.
+   * This implementation just returns null. Subclasses should override.
+   *
+   * @param tuple contains the parameters of the format test.
+   */
+  @Override
+  public String parseCurrency(DataDrivenNumberFormatTestData tuple) {
+    String pattern = (tuple.pattern == null) ? "0" : tuple.pattern;
+    Properties properties;
+    ParsePosition ppos = new ParsePosition(0);
+    CurrencyAmount actual;
+    try {
+      properties = PatternString.parseToProperties(pattern, tuple.currency != null);
+      propertiesFromTuple(tuple, properties);
+      actual =
+          Parse.parseCurrency(
+              tuple.parse, ppos, properties, DecimalFormatSymbols.getInstance(tuple.locale));
+    } catch (ParseException e) {
+      e.printStackTrace();
+      return "parse exception: " + e.getMessage();
+    }
+    if (ppos.getIndex() == 0 || actual.getCurrency().getCurrencyCode().equals("XXX")) {
+      return "Parse failed; got " + actual + ", but expected " + tuple.output;
+    }
+    BigDecimal expectedNumber = new BigDecimal(tuple.output);
+    if (expectedNumber.compareTo(new BigDecimal(actual.getNumber().toString())) != 0) {
+      return "Wrong number: Expected: " + expectedNumber + ", got: " + actual;
+    }
+    String expectedCurrency = tuple.outputCurrency;
+    if (!expectedCurrency.equals(actual.getCurrency().toString())) {
+      return "Wrong currency: Expected: " + expectedCurrency + ", got: " + actual;
+    }
+    return null;
+  }
+
+  /**
+   * Runs a single select test. On success, returns null. On failure, returns the error. This
+   * implementation just returns null. Subclasses should override.
+   *
+   * @param tuple contains the parameters of the format test.
+   */
+  @Override
+  public String select(DataDrivenNumberFormatTestData tuple) {
+    return null;
+  }
+
+  private static void propertiesFromTuple(
+      DataDrivenNumberFormatTestData tuple, Properties properties) {
+    if (tuple.minIntegerDigits != null) {
+      properties.setMinimumIntegerDigits(tuple.minIntegerDigits);
+    }
+    if (tuple.maxIntegerDigits != null) {
+      properties.setMaximumIntegerDigits(tuple.maxIntegerDigits);
+    }
+    if (tuple.minFractionDigits != null) {
+      properties.setMinimumFractionDigits(tuple.minFractionDigits);
+    }
+    if (tuple.maxFractionDigits != null) {
+      properties.setMaximumFractionDigits(tuple.maxFractionDigits);
+    }
+    if (tuple.currency != null) {
+      properties.setCurrency(tuple.currency);
+    }
+    if (tuple.minGroupingDigits != null) {
+      properties.setMinimumGroupingDigits(tuple.minGroupingDigits);
+    }
+    if (tuple.useSigDigits != null) {
+      // TODO
+    }
+    if (tuple.minSigDigits != null) {
+      properties.setMinimumSignificantDigits(tuple.minSigDigits);
+    }
+    if (tuple.maxSigDigits != null) {
+      properties.setMaximumSignificantDigits(tuple.maxSigDigits);
+    }
+    if (tuple.useGrouping != null && tuple.useGrouping == 0) {
+      properties.setGroupingSize(Integer.MAX_VALUE);
+      properties.setSecondaryGroupingSize(Integer.MAX_VALUE);
+    }
+    if (tuple.multiplier != null) {
+      properties.setMultiplier(new BigDecimal(tuple.multiplier));
+    }
+    if (tuple.roundingIncrement != null) {
+      properties.setRoundingIncrement(new BigDecimal(tuple.roundingIncrement.toString()));
+    }
+    if (tuple.formatWidth != null) {
+      properties.setFormatWidth(tuple.formatWidth);
+    }
+    if (tuple.padCharacter != null && tuple.padCharacter.length() > 0) {
+      properties.setPadString(tuple.padCharacter.toString());
+    }
+    if (tuple.useScientific != null) {
+      properties.setMinimumExponentDigits(
+          tuple.useScientific != 0 ? 1 : Properties.DEFAULT_MINIMUM_EXPONENT_DIGITS);
+    }
+    if (tuple.grouping != null) {
+      properties.setGroupingSize(tuple.grouping);
+    }
+    if (tuple.grouping2 != null) {
+      properties.setSecondaryGroupingSize(tuple.grouping2);
+    }
+    if (tuple.roundingMode != null) {
+      properties.setRoundingMode(RoundingMode.valueOf(tuple.roundingMode));
+    }
+    if (tuple.currencyUsage != null) {
+      properties.setCurrencyUsage(tuple.currencyUsage);
+    }
+    if (tuple.minimumExponentDigits != null) {
+      properties.setMinimumExponentDigits(tuple.minimumExponentDigits.byteValue());
+    }
+    if (tuple.exponentSignAlwaysShown != null) {
+      properties.setExponentSignAlwaysShown(tuple.exponentSignAlwaysShown != 0);
+    }
+    if (tuple.decimalSeparatorAlwaysShown != null) {
+      properties.setDecimalSeparatorAlwaysShown(tuple.decimalSeparatorAlwaysShown != 0);
+    }
+    if (tuple.padPosition != null) {
+      properties.setPadPosition(PadPosition.fromOld(tuple.padPosition));
+    }
+    if (tuple.positivePrefix != null) {
+      properties.setPositivePrefix(tuple.positivePrefix);
+    }
+    if (tuple.positiveSuffix != null) {
+      properties.setPositiveSuffix(tuple.positiveSuffix);
+    }
+    if (tuple.negativePrefix != null) {
+      properties.setNegativePrefix(tuple.negativePrefix);
+    }
+    if (tuple.negativeSuffix != null) {
+      properties.setNegativeSuffix(tuple.negativeSuffix);
+    }
+    if (tuple.localizedPattern != null) {
+      // TODO
+    }
+    if (tuple.lenient != null) {
+      properties.setParseMode(tuple.lenient == 0 ? ParseMode.STRICT : ParseMode.LENIENT);
+    }
+    if (tuple.parseIntegerOnly != null) {
+      properties.setParseIntegerOnly(tuple.parseIntegerOnly != 0);
+    }
+    if (tuple.parseCaseSensitive != null) {
+      properties.setParseCaseSensitive(tuple.parseCaseSensitive != 0);
+    }
+    if (tuple.decimalPatternMatchRequired != null) {
+      properties.setDecimalPatternMatchRequired(tuple.decimalPatternMatchRequired != 0);
+    }
+    if (tuple.parseNoExponent != null) {
+      properties.setParseNoExponent(tuple.parseNoExponent != 0);
+    }
+  }
+}
index 9160b960dba7da40c33449a146ba1c1911ce9c85..08dc349e27d513eef3740a97c373ec059d61c513 100644 (file)
@@ -883,8 +883,9 @@ public class TestMessageFormat extends com.ibm.icu.dev.test.TestFmwk {
                             errln("parsed argument " + parsedArgs[0] + " != " + num);
                         }
                     }
-                    catch (Exception e) {
-                        errln("parse of '" + result + " returned exception: " + e.getMessage());
+                    catch (ParseException e) {
+                        errln("parse of '" + result + "' returned exception: "
+                                + e.getMessage() + " " + e.getErrorOffset());
                     }
                 }
             }
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/AffixPatternUtilsTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/AffixPatternUtilsTest.java
new file mode 100644 (file)
index 0000000..5472c2e
--- /dev/null
@@ -0,0 +1,126 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.dev.test.numbers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+
+import com.ibm.icu.impl.number.AffixPatternUtils;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.util.ULocale;
+
+public class AffixPatternUtilsTest {
+
+  @Test
+  public void testEscape() {
+    Object[][] cases = {
+      {"", ""},
+      {"abc", "abc"},
+      {"-", "'-'"},
+      {"-!", "'-'!"},
+      {"−", "−"},
+      {"---", "'---'"},
+      {"-%-", "'-%-'"},
+      {"'", "''"},
+      {"-'", "'-'''"},
+      {"-'-", "'-''-'"},
+      {"a-'-", "a'-''-'"}
+    };
+
+    StringBuilder sb = new StringBuilder();
+    for (Object[] cas : cases) {
+      String input = (String) cas[0];
+      String expected = (String) cas[1];
+      sb.setLength(0);
+      AffixPatternUtils.escape(input, sb);
+      assertEquals(expected, sb.toString());
+    }
+  }
+
+  @Test
+  public void testUnescape() {
+    Object[][] cases = {
+      {"", false, 0, ""},
+      {"abc", false, 3, "abc"},
+      {"-", false, 1, "−"},
+      {"-!", false, 2, "−!"},
+      {"+", false, 1, "\u061C+"},
+      {"+!", false, 2, "\u061C+!"},
+      {"‰", false, 1, "؉"},
+      {"‰!", false, 2, "؉!"},
+      {"-x", false, 2, "−x"},
+      {"'-'x", false, 2, "-x"},
+      {"'--''-'-x", false, 6, "--'-−x"},
+      {"''", false, 1, "'"},
+      {"''''", false, 2, "''"},
+      {"''''''", false, 3, "'''"},
+      {"''x''", false, 3, "'x'"},
+      {"¤", true, 1, "$"},
+      {"¤¤", true, 2, "XXX"},
+      {"¤¤¤", true, 3, "long name"},
+      {"¤¤¤¤", true, 4, "\uFFFD"},
+      {"¤¤¤¤¤", true, 5, "\uFFFD"},
+      {"¤¤¤a¤¤¤¤", true, 8, "long namea\uFFFD"},
+      {"a¤¤¤¤b¤¤¤¤¤c", true, 12, "a\uFFFDb\uFFFDc"},
+      {"¤!", true, 2, "$!"},
+      {"¤¤!", true, 3, "XXX!"},
+      {"¤¤¤!", true, 4, "long name!"},
+      {"-¤¤", true, 3, "−XXX"},
+      {"¤¤-", true, 3, "XXX−"},
+      {"'¤'", false, 1, "¤"},
+      {"%", false, 1, "٪\u061C"},
+      {"'%'", false, 1, "%"},
+      {"¤'-'%", true, 3, "$-٪\u061C"}
+    };
+
+    // ar_SA has an interesting percent sign and various Arabic letter marks
+    DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(new ULocale("ar_SA"));
+    NumberStringBuilder sb = new NumberStringBuilder();
+
+    for (Object[] cas : cases) {
+      String input = (String) cas[0];
+      boolean curr = (Boolean) cas[1];
+      int length = (Integer) cas[2];
+      String output = (String) cas[3];
+
+      assertEquals(
+          "Currency on <" + input + ">", curr, AffixPatternUtils.hasCurrencySymbols(input));
+      assertEquals("Length on <" + input + ">", length, AffixPatternUtils.unescapedLength(input));
+
+      sb.clear();
+      AffixPatternUtils.unescape(input, symbols, "$", "XXX", "long name", "−", sb);
+      assertEquals("Output on <" + input + ">", output, sb.toString());
+    }
+  }
+
+  @Test
+  public void testInvalid() {
+    String[] invalidExamples = {"'", "x'", "'x", "'x''", "''x'"};
+    DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(new ULocale("en_US"));
+    NumberStringBuilder sb = new NumberStringBuilder();
+
+    for (String str : invalidExamples) {
+      try {
+        AffixPatternUtils.hasCurrencySymbols(str);
+        fail("No exception was thrown on an invalid string");
+      } catch (IllegalArgumentException e) {
+        // OK
+      }
+      try {
+        AffixPatternUtils.unescapedLength(str);
+        fail("No exception was thrown on an invalid string");
+      } catch (IllegalArgumentException e) {
+        // OK
+      }
+      try {
+        AffixPatternUtils.unescape(str, symbols, "$", "XXX", "long name", "−", sb);
+        fail("No exception was thrown on an invalid string");
+      } catch (IllegalArgumentException e) {
+        // OK
+      }
+    }
+  }
+}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/FormatQuantityTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/FormatQuantityTest.java
new file mode 100644 (file)
index 0000000..d5c5d84
--- /dev/null
@@ -0,0 +1,475 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.dev.test.numbers;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+import org.junit.Test;
+
+import com.ibm.icu.dev.test.TestFmwk;
+import com.ibm.icu.impl.number.Endpoint;
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.FormatQuantity1;
+import com.ibm.icu.impl.number.FormatQuantity2;
+import com.ibm.icu.impl.number.FormatQuantity3;
+import com.ibm.icu.impl.number.FormatQuantity4;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
+
+/** TODO: This is a temporary name for this class. Suggestions for a better name? */
+public class FormatQuantityTest extends TestFmwk {
+
+  @Test
+  public void testBehavior() throws ParseException {
+
+    // Make a list of several formatters to test the behavior of FormatQuantity.
+    List<Format> formats = new ArrayList<Format>();
+
+    Properties properties = new Properties();
+    Format ndf = Endpoint.fromBTA(properties);
+    formats.add(ndf);
+
+    properties =
+        new Properties()
+            .setMinimumSignificantDigits(3)
+            .setMaximumSignificantDigits(3)
+            .setCompactStyle(CompactStyle.LONG);
+    Format cdf = Endpoint.fromBTA(properties);
+    formats.add(cdf);
+
+    properties =
+        new Properties()
+            .setMinimumExponentDigits(1)
+            .setMaximumIntegerDigits(3)
+            .setMaximumFractionDigits(1);
+    Format exf = Endpoint.fromBTA(properties);
+    formats.add(exf);
+
+    properties = new Properties().setRoundingIncrement(new BigDecimal("0.5"));
+    Format rif = Endpoint.fromBTA(properties);
+    formats.add(rif);
+
+    String[] cases = {
+      "1.0",
+      "2.01",
+      "1234.56",
+      "3000.0",
+      "0.00026418",
+      "0.01789261",
+      "468160.0",
+      "999000.0",
+      "999900.0",
+      "999990.0",
+      "0.0",
+      "12345678901.0",
+      "-5193.48",
+    };
+
+    String[] hardCases = {
+      "9999999999999900.0",
+      "789000000000000000000000.0",
+      "789123123567853156372158.0",
+      "987654321987654321987654321987654321987654311987654321.0",
+    };
+
+    String[] doubleCases = {
+      "512.0000000000017",
+      "4095.9999999999977",
+      "4095.999999999998",
+      "4095.9999999999986",
+      "4095.999999999999",
+      "4095.9999999999995",
+      "4096.000000000001",
+      "4096.000000000002",
+      "4096.000000000003",
+      "4096.000000000004",
+      "4096.000000000005",
+      "4096.0000000000055",
+      "4096.000000000006",
+      "4096.000000000007",
+    };
+
+    int i = 0;
+    for (String str : cases) {
+      testFormatQuantity(i++, str, formats, 0);
+    }
+
+    i = 0;
+    for (String str : hardCases) {
+      testFormatQuantity(i++, str, formats, 1);
+    }
+
+    i = 0;
+    for (String str : doubleCases) {
+      testFormatQuantity(i++, str, formats, 2);
+    }
+  }
+
+  static void testFormatQuantity(int t, String str, List<Format> formats, int mode) {
+    if (mode == 2) {
+      assertEquals("Double is not valid", Double.toString(Double.parseDouble(str)), str);
+    }
+
+    List<FormatQuantity> qs = new ArrayList<FormatQuantity>();
+    BigDecimal d = new BigDecimal(str);
+    qs.add(new FormatQuantity1(d));
+    if (mode == 0) qs.add(new FormatQuantity2(d));
+    qs.add(new FormatQuantity3(d));
+    qs.add(new FormatQuantity4(d));
+
+    if (new BigDecimal(Double.toString(d.doubleValue())).compareTo(d) == 0) {
+      double dv = d.doubleValue();
+      qs.add(new FormatQuantity1(dv));
+      if (mode == 0) qs.add(new FormatQuantity2(dv));
+      qs.add(new FormatQuantity3(dv));
+      qs.add(new FormatQuantity4(dv));
+    }
+
+    if (new BigDecimal(Long.toString(d.longValue())).compareTo(d) == 0) {
+      double lv = d.longValue();
+      qs.add(new FormatQuantity1(lv));
+      if (mode == 0) qs.add(new FormatQuantity2(lv));
+      qs.add(new FormatQuantity3(lv));
+      qs.add(new FormatQuantity4(lv));
+    }
+
+    testFormatQuantityExpectedOutput(qs.get(0), str);
+
+    if (qs.size() == 1) {
+      return;
+    }
+
+    for (int i = 1; i < qs.size(); i++) {
+      FormatQuantity q0 = qs.get(0);
+      FormatQuantity q1 = qs.get(i);
+      testFormatQuantityExpectedOutput(q1, str);
+      testFormatQuantityRounding(q0, q1);
+      testFormatQuantityRoundingInterval(q0, q1);
+      testFormatQuantityMath(q0, q1);
+      testFormatQuantityWithFormats(q0, q1, formats);
+    }
+  }
+
+  private static void testFormatQuantityExpectedOutput(FormatQuantity rq, String expected) {
+    StringBuilder sb = new StringBuilder();
+    FormatQuantity q0 = rq.clone();
+    // Force an accurate double
+    q0.roundToInfinity();
+    q0.setIntegerFractionLength(1, Integer.MAX_VALUE, 1, Integer.MAX_VALUE);
+    for (int m = q0.getUpperDisplayMagnitude(); m >= q0.getLowerDisplayMagnitude(); m--) {
+      sb.append(q0.getDigit(m));
+      if (m == 0) sb.append('.');
+    }
+    if (q0.isNegative()) {
+      sb.insert(0, '-');
+    }
+    String actual = sb.toString();
+    assertEquals("Unexpected output from simple string conversion (" + q0 + ")", expected, actual);
+  }
+
+  private static final MathContext MATH_CONTEXT_HALF_EVEN =
+      new MathContext(0, RoundingMode.HALF_EVEN);
+  private static final MathContext MATH_CONTEXT_CEILING = new MathContext(0, RoundingMode.CEILING);
+  private static final MathContext MATH_CONTEXT_PRECISION =
+      new MathContext(3, RoundingMode.HALF_UP);
+
+  private static void testFormatQuantityRounding(FormatQuantity rq0, FormatQuantity rq1) {
+    FormatQuantity q0 = rq0.clone();
+    FormatQuantity q1 = rq1.clone();
+    q0.roundToMagnitude(-1, MATH_CONTEXT_HALF_EVEN);
+    q1.roundToMagnitude(-1, MATH_CONTEXT_HALF_EVEN);
+    testFormatQuantityBehavior(q0, q1);
+
+    q0 = rq0.clone();
+    q1 = rq1.clone();
+    q0.roundToMagnitude(-1, MATH_CONTEXT_CEILING);
+    q1.roundToMagnitude(-1, MATH_CONTEXT_CEILING);
+    testFormatQuantityBehavior(q0, q1);
+
+    q0 = rq0.clone();
+    q1 = rq1.clone();
+    q0.roundToMagnitude(-1, MATH_CONTEXT_PRECISION);
+    q1.roundToMagnitude(-1, MATH_CONTEXT_PRECISION);
+    testFormatQuantityBehavior(q0, q1);
+  }
+
+  private static void testFormatQuantityRoundingInterval(FormatQuantity rq0, FormatQuantity rq1) {
+    FormatQuantity q0 = rq0.clone();
+    FormatQuantity q1 = rq1.clone();
+    q0.roundToIncrement(new BigDecimal("0.05"), MATH_CONTEXT_HALF_EVEN);
+    q1.roundToIncrement(new BigDecimal("0.05"), MATH_CONTEXT_HALF_EVEN);
+    testFormatQuantityBehavior(q0, q1);
+
+    q0 = rq0.clone();
+    q1 = rq1.clone();
+    q0.roundToIncrement(new BigDecimal("0.05"), MATH_CONTEXT_CEILING);
+    q1.roundToIncrement(new BigDecimal("0.05"), MATH_CONTEXT_CEILING);
+    testFormatQuantityBehavior(q0, q1);
+  }
+
+  private static void testFormatQuantityMath(FormatQuantity rq0, FormatQuantity rq1) {
+    FormatQuantity q0 = rq0.clone();
+    FormatQuantity q1 = rq1.clone();
+    q0.adjustMagnitude(-3);
+    q1.adjustMagnitude(-3);
+    testFormatQuantityBehavior(q0, q1);
+
+    q0 = rq0.clone();
+    q1 = rq1.clone();
+    q0.multiplyBy(new BigDecimal("3.14159"));
+    q1.multiplyBy(new BigDecimal("3.14159"));
+    testFormatQuantityBehavior(q0, q1);
+  }
+
+  private static void testFormatQuantityWithFormats(
+      FormatQuantity rq0, FormatQuantity rq1, List<Format> formats) {
+    for (Format format : formats) {
+      FormatQuantity q0 = rq0.clone();
+      FormatQuantity q1 = rq1.clone();
+      String s1 = format.format(q0);
+      String s2 = format.format(q1);
+      assertEquals("Different output from formatter (" + q0 + ", " + q1 + ")", s1, s2);
+    }
+  }
+
+  private static void testFormatQuantityBehavior(FormatQuantity rq0, FormatQuantity rq1) {
+    FormatQuantity q0 = rq0.clone();
+    FormatQuantity q1 = rq1.clone();
+
+    assertEquals("Different sign (" + q0 + ", " + q1 + ")", q0.isNegative(), q1.isNegative());
+
+    assertEquals(
+        "Different fingerprint (" + q0 + ", " + q1 + ")",
+        q0.getPositionFingerprint(),
+        q1.getPositionFingerprint());
+
+    assertEquals(
+        "Different upper range of digits (" + q0 + ", " + q1 + ")",
+        q0.getUpperDisplayMagnitude(),
+        q1.getUpperDisplayMagnitude());
+
+    assertDoubleEquals(
+        "Different double values (" + q0 + ", " + q1 + ")", q0.toDouble(), q1.toDouble());
+
+    assertBigDecimalEquals(
+        "Different BigDecimal values (" + q0 + ", " + q1 + ")",
+        q0.toBigDecimal(),
+        q1.toBigDecimal());
+
+    int equalityDigits = Math.min(q0.maxRepresentableDigits(), q1.maxRepresentableDigits());
+    for (int m = q0.getUpperDisplayMagnitude(), i = 0;
+        m >= Math.min(q0.getLowerDisplayMagnitude(), q1.getLowerDisplayMagnitude())
+            && i < equalityDigits;
+        m--, i++) {
+      assertEquals(
+          "Different digit at magnitude " + m + " (" + q0 + ", " + q1 + ")",
+          q0.getDigit(m),
+          q1.getDigit(m));
+    }
+
+    if (rq0 instanceof FormatQuantity4) {
+      String message = ((FormatQuantity4) rq0).checkHealth();
+      if (message != null) errln(message);
+    }
+    if (rq1 instanceof FormatQuantity4) {
+      String message = ((FormatQuantity4) rq1).checkHealth();
+      if (message != null) errln(message);
+    }
+  }
+
+  @Test
+  public void testSwitchStorage() {
+    FormatQuantity4 fq = new FormatQuantity4();
+
+    fq.setToLong(1234123412341234L);
+    assertFalse("Should not be using byte array", fq.usingBytes());
+    assertBigDecimalEquals("Failed on initialize", "1234123412341234", fq.toBigDecimal());
+    assertNull("Failed health check", fq.checkHealth());
+    // Long -> Bytes
+    fq.appendDigit((byte) 5, 0, true);
+    assertTrue("Should be using byte array", fq.usingBytes());
+    assertBigDecimalEquals("Failed on multiply", "12341234123412345", fq.toBigDecimal());
+    assertNull("Failed health check", fq.checkHealth());
+    // Bytes -> Long
+    fq.roundToMagnitude(5, MATH_CONTEXT_HALF_EVEN);
+    assertFalse("Should not be using byte array", fq.usingBytes());
+    assertBigDecimalEquals("Failed on round", "12341234123400000", fq.toBigDecimal());
+    assertNull("Failed health check", fq.checkHealth());
+  }
+
+  @Test
+  public void testAppend() {
+    FormatQuantity4 fq = new FormatQuantity4();
+    fq.appendDigit((byte) 1, 0, true);
+    assertBigDecimalEquals("Failed on append", "1.", fq.toBigDecimal());
+    assertNull("Failed health check", fq.checkHealth());
+    fq.appendDigit((byte) 2, 0, true);
+    assertBigDecimalEquals("Failed on append", "12.", fq.toBigDecimal());
+    assertNull("Failed health check", fq.checkHealth());
+    fq.appendDigit((byte) 3, 1, true);
+    assertBigDecimalEquals("Failed on append", "1203.", fq.toBigDecimal());
+    assertNull("Failed health check", fq.checkHealth());
+    fq.appendDigit((byte) 0, 1, true);
+    assertBigDecimalEquals("Failed on append", "120300.", fq.toBigDecimal());
+    assertNull("Failed health check", fq.checkHealth());
+    fq.appendDigit((byte) 4, 0, true);
+    assertBigDecimalEquals("Failed on append", "1203004.", fq.toBigDecimal());
+    assertNull("Failed health check", fq.checkHealth());
+    fq.appendDigit((byte) 0, 0, true);
+    assertBigDecimalEquals("Failed on append", "12030040.", fq.toBigDecimal());
+    assertNull("Failed health check", fq.checkHealth());
+    fq.appendDigit((byte) 5, 0, false);
+    assertBigDecimalEquals("Failed on append", "12030040.5", fq.toBigDecimal());
+    assertNull("Failed health check", fq.checkHealth());
+    fq.appendDigit((byte) 6, 0, false);
+    assertBigDecimalEquals("Failed on append", "12030040.56", fq.toBigDecimal());
+    assertNull("Failed health check", fq.checkHealth());
+    fq.appendDigit((byte) 7, 3, false);
+    assertBigDecimalEquals("Failed on append", "12030040.560007", fq.toBigDecimal());
+    assertNull("Failed health check", fq.checkHealth());
+    StringBuilder expected = new StringBuilder("12030040.560007");
+    for (int i = 0; i < 10; i++) {
+      fq.appendDigit((byte) 8, 0, false);
+      expected.append("8");
+      assertBigDecimalEquals("Failed on append", expected.toString(), fq.toBigDecimal());
+      assertNull("Failed health check", fq.checkHealth());
+    }
+  }
+
+  @Test
+  public void testConvertToAccurateDouble() {
+    // based on https://github.com/google/double-conversion/issues/28
+    double[] hardDoubles = {
+      1651087494906221570.0,
+      -5074790912492772E-327,
+      83602530019752571E-327,
+      2.207817077636718750000000000000,
+      1.818351745605468750000000000000,
+      3.941719055175781250000000000000,
+      3.738609313964843750000000000000,
+      3.967735290527343750000000000000,
+      1.328025817871093750000000000000,
+      3.920967102050781250000000000000,
+      1.015235900878906250000000000000,
+      1.335227966308593750000000000000,
+      1.344520568847656250000000000000,
+      2.879127502441406250000000000000,
+      3.695838928222656250000000000000,
+      1.845344543457031250000000000000,
+      3.793952941894531250000000000000,
+      3.211402893066406250000000000000,
+      2.565971374511718750000000000000,
+      0.965156555175781250000000000000,
+      2.700004577636718750000000000000,
+      0.767097473144531250000000000000,
+      1.780448913574218750000000000000,
+      2.624839782714843750000000000000,
+      1.305290222167968750000000000000,
+      3.834922790527343750000000000000,
+    };
+
+    double[] integerDoubles = {
+      51423,
+      51423e10,
+      4.503599627370496E15,
+      6.789512076111555E15,
+      9.007199254740991E15,
+      9.007199254740992E15
+    };
+
+    for (double d : hardDoubles) {
+      checkDoubleBehavior(d, true, "");
+    }
+
+    for (double d : integerDoubles) {
+      checkDoubleBehavior(d, false, "");
+    }
+
+    assertEquals("NaN check failed", Double.NaN, new FormatQuantity4(Double.NaN).toDouble());
+    assertEquals(
+        "Inf check failed",
+        Double.POSITIVE_INFINITY,
+        new FormatQuantity4(Double.POSITIVE_INFINITY).toDouble());
+    assertEquals(
+        "-Inf check failed",
+        Double.NEGATIVE_INFINITY,
+        new FormatQuantity4(Double.NEGATIVE_INFINITY).toDouble());
+
+    // Generate random doubles
+    String alert = "UNEXPECTED FAILURE: PLEASE REPORT THIS MESSAGE TO THE ICU TEAM: ";
+    for (int i = 0; i < 1000000; i++) {
+      double d = Double.longBitsToDouble(ThreadLocalRandom.current().nextLong());
+      if (Double.isNaN(d) || Double.isInfinite(d)) continue;
+      checkDoubleBehavior(d, false, alert);
+    }
+  }
+
+  private static void checkDoubleBehavior(double d, boolean explicitRequired, String alert) {
+    FormatQuantity4 fq = new FormatQuantity4(d);
+    if (explicitRequired)
+      assertTrue(alert + "Should be using approximate double", !fq.explicitExactDouble);
+    assertEquals(alert + "Initial construction from hard double", d, fq.toDouble());
+    fq.roundToInfinity();
+    if (explicitRequired)
+      assertTrue(alert + "Should not be using approximate double", fq.explicitExactDouble);
+    assertDoubleEquals(alert + "After conversion to exact BCD (double)", d, fq.toDouble());
+    assertBigDecimalEquals(
+        alert + "After conversion to exact BCD (BigDecimal)",
+        new BigDecimal(Double.toString(d)),
+        fq.toBigDecimal());
+  }
+
+  @Test
+  public void testUseApproximateDoubleWhenAble() {
+    Object[][] cases = {
+      {1.2345678, 1, MATH_CONTEXT_HALF_EVEN, false},
+      {1.2345678, 7, MATH_CONTEXT_HALF_EVEN, false},
+      {1.2345678, 12, MATH_CONTEXT_HALF_EVEN, false},
+      {1.2345678, 13, MATH_CONTEXT_HALF_EVEN, true},
+      {1.235, 1, MATH_CONTEXT_HALF_EVEN, false},
+      {1.235, 2, MATH_CONTEXT_HALF_EVEN, true},
+      {1.235, 3, MATH_CONTEXT_HALF_EVEN, false},
+      {1.000000000000001, 0, MATH_CONTEXT_HALF_EVEN, false},
+      {1.000000000000001, 0, MATH_CONTEXT_CEILING, true},
+      {1.235, 1, MATH_CONTEXT_CEILING, false},
+      {1.235, 2, MATH_CONTEXT_CEILING, false},
+      {1.235, 3, MATH_CONTEXT_CEILING, true}
+    };
+
+    for (Object[] cas : cases) {
+      double d = (Double) cas[0];
+      int maxFrac = (Integer) cas[1];
+      MathContext mc = (MathContext) cas[2];
+      boolean usesExact = (Boolean) cas[3];
+
+      FormatQuantity4 fq = new FormatQuantity4(d);
+      assertTrue("Should be using approximate double", !fq.explicitExactDouble);
+      fq.roundToMagnitude(-maxFrac, mc);
+      assertEquals(
+          "Using approximate double after rounding: " + d + " maxFrac=" + maxFrac + " " + mc,
+          usesExact,
+          fq.explicitExactDouble);
+    }
+  }
+
+  static void assertDoubleEquals(String message, double d1, double d2) {
+    boolean equal = (Math.abs(d1 - d2) < 1e-6) || (Math.abs((d1 - d2) / d1) < 1e-6);
+    handleAssert(equal, message, d1, d2, null, false);
+  }
+
+  static void assertBigDecimalEquals(String message, String d1, BigDecimal d2) {
+    assertBigDecimalEquals(message, new BigDecimal(d1), d2);
+  }
+
+  static void assertBigDecimalEquals(String message, BigDecimal d1, BigDecimal d2) {
+    boolean equal = d1.compareTo(d2) == 0;
+    handleAssert(equal, message, d1, d2, null, false);
+  }
+}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/NumberStringBuilderTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/NumberStringBuilderTest.java
new file mode 100644 (file)
index 0000000..c4178e2
--- /dev/null
@@ -0,0 +1,171 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.dev.test.numbers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.text.FieldPosition;
+import java.text.Format.Field;
+
+import org.junit.Test;
+
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.text.NumberFormat;
+
+/** @author sffc */
+public class NumberStringBuilderTest {
+  private static final String[] EXAMPLE_STRINGS = {
+    "",
+    "xyz",
+    "The quick brown fox jumps over the lazy dog",
+    "😁",
+    "mixed 😇 and ASCII",
+    "with combining characters like 🇦🇧🇨🇩"
+  };
+
+  @Test
+  public void testInsertAppendCharSequence() {
+
+    StringBuilder sb1 = new StringBuilder();
+    NumberStringBuilder sb2 = new NumberStringBuilder();
+    for (String str : EXAMPLE_STRINGS) {
+      NumberStringBuilder sb3 = new NumberStringBuilder();
+      sb1.append(str);
+      sb2.append(str, null);
+      sb3.append(str, null);
+      assertCharSequenceEquals(sb1, sb2);
+      assertCharSequenceEquals(sb3, str);
+
+      StringBuilder sb4 = new StringBuilder();
+      NumberStringBuilder sb5 = new NumberStringBuilder();
+      sb4.append("😇");
+      sb4.append(str);
+      sb4.append("xx");
+      sb5.append("😇xx", null);
+      sb5.insert(2, str, null);
+      assertCharSequenceEquals(sb4, sb5);
+
+      int start = Math.min(1, str.length());
+      int end = Math.min(10, str.length());
+      sb4.insert(3, str, start, end);
+      sb5.insert(3, str, start, end, null);
+      assertCharSequenceEquals(sb4, sb5);
+
+      sb4.append(str.toCharArray());
+      sb5.append(str.toCharArray(), null);
+      assertCharSequenceEquals(sb4, sb5);
+
+      sb4.insert(4, str.toCharArray());
+      sb5.insert(4, str.toCharArray(), null);
+      assertCharSequenceEquals(sb4, sb5);
+    }
+  }
+
+  @Test
+  public void testInsertAppendCodePoint() {
+    int[] cases = {0, 1, 60, 127, 128, 0x7fff, 0x8000, 0xffff, 0x10000, 0x1f000, 0x10ffff};
+
+    StringBuilder sb1 = new StringBuilder();
+    NumberStringBuilder sb2 = new NumberStringBuilder();
+    for (int cas : cases) {
+      NumberStringBuilder sb3 = new NumberStringBuilder();
+      sb1.appendCodePoint(cas);
+      sb2.appendCodePoint(cas, null);
+      sb3.appendCodePoint(cas, null);
+      assertCharSequenceEquals(sb1, sb2);
+      assertEquals(Character.codePointAt(sb3, 0), cas);
+
+      StringBuilder sb4 = new StringBuilder();
+      NumberStringBuilder sb5 = new NumberStringBuilder();
+      sb4.append("😇");
+      sb4.appendCodePoint(cas); // Java StringBuilder has no insertCodePoint()
+      sb4.append("xx");
+      sb5.append("😇xx", null);
+      sb5.insertCodePoint(2, cas, null);
+      assertCharSequenceEquals(sb4, sb5);
+    }
+  }
+
+  @Test
+  public void testFields() {
+    for (String str : EXAMPLE_STRINGS) {
+      NumberStringBuilder sb = new NumberStringBuilder();
+      sb.append(str, null);
+      sb.append(str, NumberFormat.Field.CURRENCY);
+      Field[] fields = sb.toFieldArray();
+      assertEquals(str.length() * 2, fields.length);
+      for (int i = 0; i < str.length(); i++) {
+        assertEquals(null, fields[i]);
+        assertEquals(NumberFormat.Field.CURRENCY, fields[i + str.length()]);
+      }
+
+      // Very basic FieldPosition test. More robust tests happen in NumberFormatTest.
+      // Let NumberFormatTest also take care of AttributedCharacterIterator material.
+      FieldPosition fp = new FieldPosition(NumberFormat.Field.CURRENCY);
+      sb.populateFieldPosition(fp, 0);
+      assertEquals(str.length(), fp.getBeginIndex());
+      assertEquals(str.length() * 2, fp.getEndIndex());
+
+      if (str.length() > 0) {
+        sb.insertCodePoint(2, 100, NumberFormat.Field.INTEGER);
+        fields = sb.toFieldArray();
+        assertEquals(str.length() * 2 + 1, fields.length);
+        assertEquals(fields[2], NumberFormat.Field.INTEGER);
+      }
+
+      sb.append(sb.clone());
+      sb.append(sb.toCharArray(), sb.toFieldArray());
+      int numNull = 0;
+      int numCurr = 0;
+      int numInt = 0;
+      Field[] oldFields = fields;
+      fields = sb.toFieldArray();
+      for (int i = 0; i < sb.length(); i++) {
+        assertEquals(oldFields[i % oldFields.length], fields[i]);
+        if (fields[i] == null) numNull++;
+        else if (fields[i] == NumberFormat.Field.CURRENCY) numCurr++;
+        else if (fields[i] == NumberFormat.Field.INTEGER) numInt++;
+        else throw new AssertionError("Encountered unknown field in " + str);
+      }
+      assertEquals(str.length() * 4, numNull);
+      assertEquals(numNull, numCurr);
+      assertEquals(str.length() > 0 ? 4 : 0, numInt);
+
+      NumberStringBuilder sb2 = new NumberStringBuilder();
+      sb2.append(sb);
+      assertTrue(sb.contentEquals(sb2));
+      assertTrue(sb.contentEquals(sb2.toCharArray(), sb2.toFieldArray()));
+
+      sb2.insertCodePoint(0, 50, NumberFormat.Field.FRACTION);
+      assertTrue(!sb.contentEquals(sb2));
+      assertTrue(!sb.contentEquals(sb2.toCharArray(), sb2.toFieldArray()));
+    }
+  }
+
+  @Test
+  public void testUnlimitedCapacity() {
+    NumberStringBuilder builder = new NumberStringBuilder();
+    // The builder should never fail upon repeated appends.
+    for (int i = 0; i < 1000; i++) {
+      assertEquals(builder.length(), i);
+      builder.appendCodePoint('x', null);
+      assertEquals(builder.length(), i + 1);
+    }
+  }
+
+  private static void assertCharSequenceEquals(CharSequence a, CharSequence b) {
+    assertEquals(a.toString(), b.toString());
+
+    assertEquals(a.length(), b.length());
+    for (int i = 0; i < a.length(); i++) {
+      assertEquals(a.charAt(i), b.charAt(i));
+    }
+
+    int start = Math.min(2, a.length());
+    int end = Math.min(12, a.length());
+    if (start != end) {
+      assertCharSequenceEquals(a.subSequence(start, end), b.subSequence(start, end));
+    }
+  }
+}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/PatternStringTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/PatternStringTest.java
new file mode 100644 (file)
index 0000000..d53de89
--- /dev/null
@@ -0,0 +1,110 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.dev.test.numbers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+
+import com.ibm.icu.impl.number.PatternString;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.util.ULocale;
+
+/** @author sffc */
+public class PatternStringTest {
+
+  @Test
+  public void testLocalized() {
+    DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(ULocale.ENGLISH);
+    symbols.setDecimalSeparatorString("a");
+    symbols.setPercentString("b");
+    symbols.setMinusSignString(".");
+    symbols.setPlusSignString("'");
+
+    String standard = "+-abcb''a''#,##0.0%'a%'";
+    String localized = "’.'ab'c'b''a'''#,##0a0b'a%'";
+    String toStandard = "+-'ab'c'b''a'''#,##0.0%'a%'";
+
+    assertEquals(localized, PatternString.convertLocalized(standard, symbols, true));
+    assertEquals(toStandard, PatternString.convertLocalized(localized, symbols, false));
+  }
+
+  @Test
+  public void testToPatternSimple() {
+    String[][] cases = {
+      {"#", "0"},
+      {"0", "0"},
+      {"#0", "0"},
+      {"###", "0"},
+      {"0.##", "0.##"},
+      {"0.00", "0.00"},
+      {"0.00#", "0.00#"},
+      {"#E0", "#E0"},
+      {"0E0", "0E0"},
+      {"#00E00", "#00E00"},
+      {"#,##0", "#,##0"},
+      {"#,##0E0", "#,##0E0"},
+      {"#;#", "0;0"},
+      {"#;-#", "0"}, // ignore a negative prefix pattern of '-' since that is the default
+      {"**##0", "**##0"},
+      {"*'x'##0", "*x##0"},
+      {"a''b0", "a''b0"},
+      {"*''##0", "*''##0"},
+      {"*📺##0", "*'📺'##0"},
+      {"*'நி'##0", "*'நி'##0"},
+    };
+
+    for (String[] cas : cases) {
+      String input = cas[0];
+      String output = cas[1];
+
+      Properties properties = PatternString.parseToProperties(input);
+      String actual = PatternString.propertiesToString(properties);
+      assertEquals(
+          "Failed on input pattern '" + input + "', properties " + properties, output, actual);
+    }
+  }
+
+  @Test
+  public void testToPatternWithProperties() {
+    Object[][] cases = {
+      {new Properties().setPositivePrefix("abc"), "abc#"},
+      {new Properties().setPositiveSuffix("abc"), "#abc"},
+      {new Properties().setPositivePrefixPattern("abc"), "abc#"},
+      {new Properties().setPositiveSuffixPattern("abc"), "#abc"},
+      {new Properties().setNegativePrefix("abc"), "#;abc#"},
+      {new Properties().setNegativeSuffix("abc"), "#;#abc"},
+      {new Properties().setNegativePrefixPattern("abc"), "#;abc#"},
+      {new Properties().setNegativeSuffixPattern("abc"), "#;#abc"},
+      {new Properties().setPositivePrefix("+"), "'+'#"},
+      {new Properties().setPositivePrefixPattern("+"), "+#"},
+      {new Properties().setPositivePrefix("+'"), "'+'''#"},
+      {new Properties().setPositivePrefix("'+"), "'''+'#"},
+      {new Properties().setPositivePrefix("'"), "''#"},
+      {new Properties().setPositivePrefixPattern("+''"), "+''#"},
+    };
+
+    for (Object[] cas : cases) {
+      Properties input = (Properties) cas[0];
+      String output = (String) cas[1];
+
+      String actual = PatternString.propertiesToString(input);
+      assertEquals("Failed on input properties " + input, output, actual);
+    }
+  }
+
+  @Test
+  public void testExceptionOnInvalid() {
+    String[] invalidPatterns = {"#.#.#", "0#", "0#.", ".#0", "0#.#0", "@0", "0@"};
+
+    for (String pattern : invalidPatterns) {
+      try {
+        PatternString.parseToProperties(pattern);
+        fail("Didn't throw IllegalArgumentException when parsing pattern: " + pattern);
+      } catch (IllegalArgumentException e) {
+      }
+    }
+  }
+}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/PropertiesTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/PropertiesTest.java
new file mode 100644 (file)
index 0000000..ad3edf2
--- /dev/null
@@ -0,0 +1,331 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.dev.test.numbers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Parameter;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.junit.Test;
+
+import com.ibm.icu.dev.test.serializable.SerializableTestUtility;
+import com.ibm.icu.impl.number.Parse.ParseMode;
+import com.ibm.icu.impl.number.PatternString;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.impl.number.formatters.CurrencyFormat.CurrencyStyle;
+import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
+import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder.SignificantDigitsMode;
+import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
+import com.ibm.icu.text.CurrencyPluralInfo;
+import com.ibm.icu.text.MeasureFormat.FormatWidth;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyUsage;
+import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.ULocale;
+
+public class PropertiesTest {
+
+  @Test
+  public void testBasicEquals() {
+    Properties p1 = new Properties();
+    Properties p2 = new Properties();
+    assertEquals(p1, p2);
+
+    p1.setPositivePrefix("abc");
+    assertNotEquals(p1, p2);
+    p2.setPositivePrefix("xyz");
+    assertNotEquals(p1, p2);
+    p1.setPositivePrefix("xyz");
+    assertEquals(p1, p2);
+  }
+
+  @Test
+  public void testFieldCoverage() {
+    Properties p0 = new Properties();
+    Properties p1 = new Properties();
+    Properties p2 = new Properties();
+    Properties p3 = new Properties();
+    Properties p4 = new Properties();
+
+    Set<Integer> hashCodes = new HashSet<Integer>();
+    Field[] fields = Properties.class.getDeclaredFields();
+    for (Field field : fields) {
+      if (Modifier.isStatic(field.getModifiers())) {
+        continue;
+      }
+
+      // Check for getters and setters
+      String fieldNamePascalCase =
+          Character.toUpperCase(field.getName().charAt(0)) + field.getName().substring(1);
+      String getterName = "get" + fieldNamePascalCase;
+      String setterName = "set" + fieldNamePascalCase;
+      Method getter, setter;
+      try {
+        getter = Properties.class.getMethod(getterName);
+        assertEquals(
+            "Getter does not return correct type", field.getType(), getter.getReturnType());
+      } catch (NoSuchMethodException e) {
+        fail("Could not find method " + getterName + " for field " + field);
+        continue;
+      } catch (SecurityException e) {
+        fail("Could not access method " + getterName + " for field " + field);
+        continue;
+      }
+      try {
+        setter = Properties.class.getMethod(setterName, field.getType());
+        assertEquals(
+            "Method " + setterName + " does not return correct type",
+            Properties.class,
+            setter.getReturnType());
+      } catch (NoSuchMethodException e) {
+        fail("Could not find method " + setterName + " for field " + field);
+        continue;
+      } catch (SecurityException e) {
+        fail("Could not access method " + setterName + " for field " + field);
+        continue;
+      }
+
+      // Check for parameter name equality.
+      // The parameter name is not always available, depending on compiler settings.
+      Parameter param = setter.getParameters()[0];
+      if (!param.getName().subSequence(0, 3).equals("arg")) {
+        assertEquals("Parameter name should equal field name", field.getName(), param.getName());
+      }
+
+      try {
+        // Check for default value (should be null for objects)
+        if (field.getType() != Integer.TYPE && field.getType() != Boolean.TYPE) {
+          Object default0 = getter.invoke(p0);
+          assertEquals("Field " + field + " has non-null default value:", null, default0);
+        }
+
+        // Check for getter, equals, and hash code behavior
+        Object val0 = getSampleValueForType(field.getType(), 0);
+        Object val1 = getSampleValueForType(field.getType(), 1);
+        Object val2 = getSampleValueForType(field.getType(), 2);
+        assertNotEquals(val0, val1);
+        setter.invoke(p1, val0);
+        setter.invoke(p2, val0);
+        assertEquals(p1, p2);
+        assertEquals(p1.hashCode(), p2.hashCode());
+        assertEquals(getter.invoke(p1), getter.invoke(p2));
+        assertEquals(getter.invoke(p1), val0);
+        assertNotEquals(getter.invoke(p1), val1);
+        hashCodes.add(p1.hashCode());
+        setter.invoke(p1, val1);
+        assertNotEquals("Field " + field + " is missing from equals()", p1, p2);
+        assertNotEquals(getter.invoke(p1), getter.invoke(p2));
+        assertNotEquals(getter.invoke(p1), val0);
+        assertEquals(getter.invoke(p1), val1);
+        setter.invoke(p1, val0);
+        assertEquals("Field " + field + " setter might have side effects", p1, p2);
+        assertEquals(p1.hashCode(), p2.hashCode());
+        assertEquals(getter.invoke(p1), getter.invoke(p2));
+        setter.invoke(p1, val1);
+        setter.invoke(p2, val1);
+        assertEquals(p1, p2);
+        assertEquals(p1.hashCode(), p2.hashCode());
+        assertEquals(getter.invoke(p1), getter.invoke(p2));
+        setter.invoke(p1, val2);
+        setter.invoke(p1, val1);
+        assertEquals("Field " + field + " setter might have side effects", p1, p2);
+        assertEquals(p1.hashCode(), p2.hashCode());
+        assertEquals(getter.invoke(p1), getter.invoke(p2));
+        hashCodes.add(p1.hashCode());
+
+        // Check for clone behavior
+        Properties copy = p1.clone();
+        assertEquals("Field " + field + " did not get copied in clone", p1, copy);
+        assertEquals(p1.hashCode(), copy.hashCode());
+        assertEquals(getter.invoke(p1), getter.invoke(copy));
+
+        // Check for copyFrom behavior
+        setter.invoke(p1, val0);
+        assertNotEquals(p1, p2);
+        assertNotEquals(getter.invoke(p1), getter.invoke(p2));
+        p2.copyFrom(p1);
+        assertEquals("Field " + field + " is missing from copyFrom()", p1, p2);
+        assertEquals(p1.hashCode(), p2.hashCode());
+        assertEquals(getter.invoke(p1), getter.invoke(p2));
+
+        // Load values into p3 and p4 for clear() behavior test
+        setter.invoke(p3, getSampleValueForType(field.getType(), 3));
+        hashCodes.add(p3.hashCode());
+        setter.invoke(p4, getSampleValueForType(field.getType(), 4));
+        hashCodes.add(p4.hashCode());
+      } catch (IllegalAccessException e) {
+        fail("Could not access method for field " + field);
+      } catch (IllegalArgumentException e) {
+        fail("Could call method for field " + field);
+      } catch (InvocationTargetException e) {
+        fail("Could invoke method on target for field " + field);
+      }
+    }
+
+    // Check for clear() behavior
+    assertNotEquals(p3, p4);
+    p3.clear();
+    p4.clear();
+    assertEquals("A field is missing from the clear() function", p3, p4);
+
+    // A good hashCode() implementation should produce very few collisions.  We added at most
+    // 4*fields.length codes to the set.  We'll say the implementation is good if we had at least
+    // fields.length unique values.
+    // TODO: Should the requirement be stronger than this?
+    assertTrue(
+        "Too many hash code collisions: " + hashCodes.size() + " out of " + (fields.length * 4),
+        hashCodes.size() >= fields.length);
+  }
+
+  /**
+   * Creates a valid sample instance of the given type. Used to simulate getters and setters.
+   *
+   * @param type The type to generate.
+   * @param seed An integer seed, guaranteed to be positive. The same seed should generate two
+   *     instances that are equal. A different seed should in general generate two instances that
+   *     are not equal; this might not always be possible, such as with booleans or enums where
+   *     there are limited possible values.
+   * @return An instance of the specified type.
+   */
+  Object getSampleValueForType(Class<?> type, int seed) {
+    if (type == Integer.TYPE) {
+      return seed * 1000001;
+
+    } else if (type == Boolean.TYPE) {
+      return (seed % 2) == 0;
+
+    } else if (type == BigDecimal.class) {
+      if (seed == 0) return null;
+      return new BigDecimal(seed * 1000002);
+
+    } else if (type == String.class) {
+      if (seed == 0) return null;
+      return BigInteger.valueOf(seed * 1000003).toString(32);
+
+    } else if (type == CompactStyle.class) {
+      if (seed == 0) return null;
+      CompactStyle[] values = CompactStyle.values();
+      return values[seed % values.length];
+
+    } else if (type == Currency.class) {
+      if (seed == 0) return null;
+      Object[] currencies = Currency.getAvailableCurrencies().toArray();
+      return currencies[seed % currencies.length];
+
+    } else if (type == CurrencyPluralInfo.class) {
+      if (seed == 0) return null;
+      ULocale[] locales = ULocale.getAvailableLocales();
+      return CurrencyPluralInfo.getInstance(locales[seed % locales.length]);
+
+    } else if (type == CurrencyStyle.class) {
+      if (seed == 0) return null;
+      CurrencyStyle[] values = CurrencyStyle.values();
+      return values[seed % values.length];
+
+    } else if (type == CurrencyUsage.class) {
+      if (seed == 0) return null;
+      CurrencyUsage[] values = CurrencyUsage.values();
+      return values[seed % values.length];
+
+    } else if (type == FormatWidth.class) {
+      if (seed == 0) return null;
+      FormatWidth[] values = FormatWidth.values();
+      return values[seed % values.length];
+
+    } else if (type == MathContext.class) {
+      if (seed == 0) return null;
+      RoundingMode[] modes = RoundingMode.values();
+      return new MathContext(seed, modes[seed % modes.length]);
+
+    } else if (type == MeasureUnit.class) {
+      if (seed == 0) return null;
+      Object[] units = MeasureUnit.getAvailable().toArray();
+      return units[seed % units.length];
+
+    } else if (type == PadPosition.class) {
+      if (seed == 0) return null;
+      PadPosition[] values = PadPosition.values();
+      return values[seed % values.length];
+
+    } else if (type == ParseMode.class) {
+      if (seed == 0) return null;
+      ParseMode[] values = ParseMode.values();
+      return values[seed % values.length];
+
+    } else if (type == RoundingMode.class) {
+      if (seed == 0) return null;
+      RoundingMode[] values = RoundingMode.values();
+      return values[seed % values.length];
+
+    } else if (type == SignificantDigitsMode.class) {
+      if (seed == 0) return null;
+      SignificantDigitsMode[] values = SignificantDigitsMode.values();
+      return values[seed % values.length];
+
+    } else {
+      fail("Don't know how to handle type " + type + ". Please add it to getSampleValueForType().");
+      return null;
+    }
+  }
+
+  @Test
+  public void TestBasicSerializationRoundTrip() throws IOException, ClassNotFoundException {
+    Properties props0 = new Properties();
+
+    // Write values to some of the fields
+    PatternString.parseToExistingProperties("A-**####,#00.00#b¤", props0);
+
+    // Write to byte stream
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    ObjectOutputStream oos = new ObjectOutputStream(baos);
+    oos.writeObject(props0);
+    oos.flush();
+    baos.close();
+    byte[] bytes = baos.toByteArray();
+
+    // Read from byte stream
+    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
+    Object obj = ois.readObject();
+    ois.close();
+    Properties props1 = (Properties) obj;
+
+    // Test equality
+    assertEquals("Did not round-trip through serialization", props0, props1);
+  }
+
+  /** Handler for serialization compatibility test suite. */
+  public static class PropertiesHandler implements SerializableTestUtility.Handler {
+
+    @Override
+    public Object[] getTestObjects() {
+      return new Object[] {
+        new Properties(),
+        PatternString.parseToProperties("x#,##0.00%"),
+        new Properties().setCompactStyle(CompactStyle.LONG).setMinimumExponentDigits(2)
+      };
+    }
+
+    @Override
+    public boolean hasSameBehavior(Object a, Object b) {
+      return a.equals(b);
+    }
+  }
+}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/RounderTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/numbers/RounderTest.java
new file mode 100644 (file)
index 0000000..872fb1a
--- /dev/null
@@ -0,0 +1,134 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.dev.test.numbers;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.FormatQuantity4;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder;
+import com.ibm.icu.impl.number.rounders.SignificantDigitsRounder.SignificantDigitsMode;
+
+public class RounderTest {
+
+  @Test
+  public void testSignificantDigitsRounder() {
+    Object[][][][] cases = {
+      {
+        {{1, -1}, {0, 2}, {2, 4}}, // minInt, maxInt, minFrac, maxFrac, minSig, maxSig
+        {
+          {0.0, "0.0", "0.0", "0"},
+          {0.054321, "0.05432", "0.05", "0.054"},
+          {0.54321, "0.5432", "0.54", "0.54"},
+          {1.0, "1.0", "1.0", "1"},
+          {5.4321, "5.432", "5.43", "5.43"},
+          {10.0, "10", "10", "10"},
+          {11.0, "11", "11", "11"},
+          {100.0, "100", "100", "100"},
+          {100.23, "100.2", "100.2", "100.2"},
+          {543210.0, "543200", "543200", "543200"},
+        }
+      },
+      {
+        {{1, -1}, {0, 0}, {2, -1}}, // minInt, maxInt, minFrac, maxFrac, minSig, maxSig
+        {
+          {0.0, "0.0", "0", "0"},
+          {0.054321, "0.054321", "0", "0.054"},
+          {0.54321, "0.54321", "1", "0.54"},
+          {1.0, "1.0", "1", "1"},
+          {5.4321, "5.4321", "5", "5.4"},
+          {10.0, "10", "10", "10"},
+          {11.0, "11", "11", "11"},
+          {100.0, "100", "100", "100"},
+          {100.23, "100.23", "100", "100"},
+          {543210.0, "543210", "543210", "543210"},
+        }
+      },
+      {
+        {{0, 2}, {1, 2}, {3, 3}}, // minInt, maxInt, minFrac, maxFrac, minSig, maxSig
+        {
+          {0.0, ".000", ".00", ".0"},
+          {0.054321, ".0543", ".05", ".0543"},
+          {0.54321, ".543", ".54", ".543"},
+          {1.0, "1.00", "1.00", "1.0"},
+          {5.4321, "5.43", "5.43", "5.43"},
+          {10.0, "10.0", "10.0", "10.0"},
+          {11.0, "11.0", "11.0", "11.0"},
+          {100.0, "00.0", "00.0", "00.0"},
+          {100.23, "00.2", "00.2", "00.2"},
+          {543210.0, "10.0", "10.0", "10.0"}
+        }
+      }
+    };
+
+    int caseNumber = 0;
+    for (Object[][][] cas : cases) {
+      int minInt = (Integer) cas[0][0][0];
+      int maxInt = (Integer) cas[0][0][1];
+      int minFrac = (Integer) cas[0][1][0];
+      int maxFrac = (Integer) cas[0][1][1];
+      int minSig = (Integer) cas[0][2][0];
+      int maxSig = (Integer) cas[0][2][1];
+
+      Properties properties = new Properties();
+      FormatQuantity4 fq = new FormatQuantity4();
+      properties.setMinimumIntegerDigits(minInt);
+      properties.setMaximumIntegerDigits(maxInt);
+      properties.setMinimumFractionDigits(minFrac);
+      properties.setMaximumFractionDigits(maxFrac);
+      properties.setMinimumSignificantDigits(minSig);
+      properties.setMaximumSignificantDigits(maxSig);
+
+      int runNumber = 0;
+      for (Object[] run : cas[1]) {
+        double input = (Double) run[0];
+        String expected1 = (String) run[1];
+        String expected2 = (String) run[2];
+        String expected3 = (String) run[3];
+
+        properties.setSignificantDigitsMode(SignificantDigitsMode.OVERRIDE_MAXIMUM_FRACTION);
+        fq.setToDouble(input);
+        SignificantDigitsRounder.getInstance(properties).apply(fq);
+        assertEquals(
+            "Case " + caseNumber + ", run " + runNumber + ", mode 0: " + fq,
+            expected1,
+            formatQuantityToString(fq));
+
+        properties.setSignificantDigitsMode(SignificantDigitsMode.RESPECT_MAXIMUM_FRACTION);
+        fq.setToDouble(input);
+        SignificantDigitsRounder.getInstance(properties).apply(fq);
+        assertEquals(
+            "Case " + caseNumber + ", run " + runNumber + ", mode 1: " + fq,
+            expected2,
+            formatQuantityToString(fq));
+
+        properties.setSignificantDigitsMode(SignificantDigitsMode.ENSURE_MINIMUM_SIGNIFICANT);
+        fq.setToDouble(input);
+        SignificantDigitsRounder.getInstance(properties).apply(fq);
+        assertEquals(
+            "Case " + caseNumber + ", run " + runNumber + ", mode 2: " + fq,
+            expected3,
+            formatQuantityToString(fq));
+
+        runNumber++;
+      }
+
+      caseNumber++;
+    }
+  }
+
+  private String formatQuantityToString(FormatQuantity fq) {
+    StringBuilder sb = new StringBuilder();
+    int udm = fq.getUpperDisplayMagnitude();
+    int ldm = fq.getLowerDisplayMagnitude();
+    if (udm == -1) sb.append('.');
+    for (int m = udm; m >= ldm; m--) {
+      sb.append(fq.getDigit(m));
+      if (m == 0 && m > ldm) sb.append('.');
+    }
+    return sb.toString();
+  }
+}
index f79d4fceaafd55adcd54898d8201572701a7a0be..ea291facc004165038b08b357534506d12cde0e7 100644 (file)
@@ -1106,7 +1106,14 @@ public class FormatHandler
             NumberFormat format_b = (NumberFormat) b;
             double number = 1234.56;
 
-            return format_a.format(number).equals(format_b.format(number));
+            String result_a = format_a.format(number);
+            String result_b = format_b.format(number);
+            boolean equal = result_a.equals(result_b);
+            if (!equal) {
+                System.out.println(format_a+" "+format_b);
+                System.out.println(result_a+" "+result_b);
+            }
+            return equal;
         }
     }
 
@@ -1710,7 +1717,17 @@ public class FormatHandler
             char chars_a[] = getCharSymbols(dfs_a);
             char chars_b[] = getCharSymbols(dfs_b);
 
-            return SerializableTestUtility.compareStrings(strings_a, strings_b) && SerializableTestUtility.compareChars(chars_a, chars_b);
+            // Spot-check char-to-string conversion (ICU 58)
+            String percent_a1 = Character.toString(dfs_a.getPercent());
+            String percent_a2 = dfs_a.getPercentString();
+            String percent_b1 = Character.toString(dfs_b.getPercent());
+            String percent_b2 = dfs_b.getPercentString();
+
+            return SerializableTestUtility.compareStrings(strings_a, strings_b)
+                    && SerializableTestUtility.compareChars(chars_a, chars_b)
+                    && percent_a1.equals(percent_b1)
+                    && percent_a2.equals(percent_b2)
+                    && percent_a1.equals(percent_a2);
         }
     }
 
index 99660b0c05a7df76efdc1047040cda56b4d5986b..ba11db331b1c9092e310bd5ddd4178b135bd91a8 100644 (file)
@@ -30,6 +30,7 @@ import java.util.Locale;
 
 import com.ibm.icu.dev.test.format.MeasureUnitTest;
 import com.ibm.icu.dev.test.format.PluralRulesTest;
+import com.ibm.icu.dev.test.numbers.PropertiesTest;
 import com.ibm.icu.impl.JavaTimeZone;
 import com.ibm.icu.impl.OlsonTimeZone;
 import com.ibm.icu.impl.TimeZoneAdapter;
@@ -827,6 +828,7 @@ public class SerializableTestUtility {
         map.put("com.ibm.icu.util.MeasureUnit", new MeasureUnitTest.MeasureUnitHandler());
         map.put("com.ibm.icu.util.TimeUnit", new MeasureUnitTest.MeasureUnitHandler());
         map.put("com.ibm.icu.text.MeasureFormat", new MeasureUnitTest.MeasureFormatHandler());
+        map.put("com.ibm.icu.impl.number.Properties", new PropertiesTest.PropertiesHandler());
 
         map.put("com.ibm.icu.util.ICUException", new ICUExceptionHandler());
         map.put("com.ibm.icu.util.ICUUncheckedIOException", new ICUUncheckedIOExceptionHandler());
@@ -925,6 +927,11 @@ public class SerializableTestUtility {
                 return;
             }
 
+            if (className.equals("com.ibm.icu.text.DecimalFormat_ICU58")) {
+                // Do not test the legacy DecimalFormat class in ICU 59
+                return;
+            }
+
             if (c.isEnum() || !serializable.isAssignableFrom(c)) {
                 //System.out.println("@@@ Skipping: " + className);
                 return;
index 39e5a9d28ea2468a940d42f29261c95f9d97e814..d95812a66bf8cc1cadc6d1e9c8050e14792a82a2 100644 (file)
@@ -8,6 +8,7 @@
 */
 package com.ibm.icu.dev.test.util;
 
+import java.util.Arrays;
 import java.util.Iterator;
 
 import org.junit.Test;
@@ -25,9 +26,14 @@ public class TextTrieMapTest extends TestFmwk {
     private static final Integer FRI = new Integer(6);
     private static final Integer SAT = new Integer(7);
 
+    private static final Integer SUP1 = new Integer(8);
+    private static final Integer SUP2 = new Integer(9);
+    private static final Integer SUP3 = new Integer(10);
+    private static final Integer SUP4 = new Integer(11);
+
     private static final Integer FOO = new Integer(-1);
     private static final Integer BAR = new Integer(-2);
-    
+
     private static final Object[][] TESTDATA = {
         {"Sunday", SUN},
         {"Monday", MON},
@@ -49,7 +55,11 @@ public class TextTrieMapTest extends TestFmwk {
         {"W", WED},
         {"T", THU},
         {"F", FRI},
-        {"S", SAT}
+        {"S", SAT},
+        {"L📺", SUP1}, // L, 0xD83D, 0xDCFA
+        {"L📺1", SUP2}, // L, 0xD83D, 0xDCFA, 1
+        {"L📻", SUP3}, // L, 0xD83D, 0xDCFB
+        {"L🃏", SUP4}, // L, 0xD83C, 0xDCCF
     };
 
     private static final Object[][] TESTCASES = {
@@ -62,7 +72,70 @@ public class TextTrieMapTest extends TestFmwk {
         {"TEST", new Object[]{TUE, THU}, new Object[]{TUE, THU}},
         {"SUN", new Object[]{SUN, SAT}, SUN},
         {"super", null, SUN},
-        {"NO", null, null}
+        {"NO", null, null},
+        {"L📺", SUP1, SUP1},
+        {"l📺", null, SUP1},
+    };
+
+    private static final Object[][] TESTCASES_PARSE = {
+            {
+                "Sunday",
+                new Object[]{
+                        new Object[]{SAT,SUN}, new Object[]{SAT,SUN}, // matches on "S"
+                        null, null, // matches on "Su"
+                        SUN, SUN, // matches on "Sun"
+                        null, null, // matches on "Sund"
+                        null, null, // matches on "Sunda"
+                        SUN, SUN, // matches on "Sunday"
+                }
+            },
+            {
+                "sunday",
+                new Object[]{
+                        null, new Object[]{SAT,SUN}, // matches on "s"
+                        null, null, // matches on "su"
+                        null, SUN, // matches on "sun"
+                        null, null, // matches on "sund"
+                        null, null, // matches on "sunda"
+                        null, SUN, // matches on "sunday"
+                }
+            },
+            {
+                "MMM",
+                new Object[]{
+                        MON, MON, // matches on "M"
+                        // no more matches in data
+                }
+            },
+            {
+                "BBB",
+                new Object[]{
+                        // no matches in data
+                }
+            },
+            {
+                "l📺12",
+                new Object[]{
+                        null, null, // matches on "L"
+                        null, SUP1, // matches on "L📺"
+                        null, SUP2, // matches on "L📺1"
+                        // no more matches in data
+                }
+            },
+            {
+                "L📻",
+                new Object[] {
+                        null, null, // matches on "L"
+                        SUP3, SUP3, // matches on "L📻"
+                }
+            },
+            {
+                "L🃏",
+                new Object[] {
+                        null, null, // matches on "L"
+                        SUP4, SUP4, // matches on "L🃏"
+                }
+            }
     };
 
     @Test
@@ -76,7 +149,7 @@ public class TextTrieMapTest extends TestFmwk {
         logln("Test for get(String)");
         for (int i = 0; i < TESTCASES.length; i++) {
             itr = map.get((String)TESTCASES[i][0]);
-            checkResult(itr, TESTCASES[i][1]);
+            checkResult("get(String) case " + i, itr, TESTCASES[i][1]);
         }
 
         logln("Test for get(String, int)");
@@ -88,7 +161,14 @@ public class TextTrieMapTest extends TestFmwk {
             }
             textBuf.append(TESTCASES[i][0]);
             itr = map.get(textBuf.toString(), i);
-            checkResult(itr, TESTCASES[i][1]);
+            checkResult("get(String, int) case " + i, itr, TESTCASES[i][1]);
+        }
+
+        logln("Test for ParseState");
+        for (int i = 0; i < TESTCASES_PARSE.length; i++) {
+            String test = (String) TESTCASES_PARSE[i][0];
+            Object[] expecteds = (Object[]) TESTCASES_PARSE[i][1];
+            checkParse(map, test, expecteds, true);
         }
 
         // Add duplicated entry
@@ -98,7 +178,7 @@ public class TextTrieMapTest extends TestFmwk {
 
         // Make sure the all entries are returned
         itr = map.get("Sunday");
-        checkResult(itr, new Object[]{FOO, SUN});
+        checkResult("Get Sunday", itr, new Object[]{FOO, SUN});
     }
 
     @Test
@@ -112,9 +192,9 @@ public class TextTrieMapTest extends TestFmwk {
         logln("Test for get(String)");
         for (int i = 0; i < TESTCASES.length; i++) {
             itr = map.get((String)TESTCASES[i][0]);
-            checkResult(itr, TESTCASES[i][2]);
+            checkResult("get(String) case " + i, itr, TESTCASES[i][2]);
         }
-        
+
         logln("Test for get(String, int)");
         StringBuffer textBuf = new StringBuffer();
         for (int i = 0; i < TESTCASES.length; i++) {
@@ -124,7 +204,14 @@ public class TextTrieMapTest extends TestFmwk {
             }
             textBuf.append(TESTCASES[i][0]);
             itr = map.get(textBuf.toString(), i);
-            checkResult(itr, TESTCASES[i][2]);
+            checkResult("get(String, int) case " + i, itr, TESTCASES[i][2]);
+        }
+
+        logln("Test for ParseState");
+        for (int i = 0; i < TESTCASES_PARSE.length; i++) {
+            String test = (String) TESTCASES_PARSE[i][0];
+            Object[] expecteds = (Object[]) TESTCASES_PARSE[i][1];
+            checkParse(map, test, expecteds, false);
         }
 
         // Add duplicated entry
@@ -134,7 +221,55 @@ public class TextTrieMapTest extends TestFmwk {
 
         // Make sure the all entries are returned
         itr = map.get("Sunday");
-        checkResult(itr, new Object[]{SUN, FOO, BAR});
+        checkResult("Get Sunday", itr, new Object[]{SUN, FOO, BAR});
+    }
+
+    private void checkParse(TextTrieMap map, String text, Object[] rawExpecteds, boolean caseSensitive) {
+        // rawExpecteds has even-valued indices for case sensitive and odd-valued indicies for case insensitive
+        // Get out only the values that we want.
+        Object[] expecteds = null;
+        for (int i=rawExpecteds.length/2-1; i>=0; i--) {
+            int j = i*2+(caseSensitive?0:1);
+            if (rawExpecteds[j] != null) {
+                if (expecteds == null) {
+                    expecteds = new Object[i+1];
+                }
+                expecteds[i] = rawExpecteds[j];
+            }
+        }
+        if (expecteds == null) {
+            expecteds = new Object[0];
+        }
+
+        TextTrieMap.ParseState state = null;
+        for (int charOffset=0, cpOffset=0; charOffset < text.length(); cpOffset++) {
+            int cp = Character.codePointAt(text, charOffset);
+            if (state == null) {
+                state = map.openParseState(cp);
+            }
+            if (state == null) {
+                assertEquals("Expected matches, but no matches are available", 0, expecteds.length);
+                break;
+            }
+            state.accept(cp);
+            if (cpOffset < expecteds.length - 1) {
+                assertFalse(
+                        "In middle of parse sequence, but atEnd() is true: '" + text + "' offset " + charOffset,
+                        state.atEnd());
+            } else if (cpOffset == expecteds.length) {
+                // Note: it possible for atEnd() to be either true or false at expecteds.length - 1;
+                // if true, we are at the end of the input string; if false, there is still input string
+                // left to be consumed, but we don't know if there are remaining matches.
+                assertTrue(
+                        "At end of parse sequence, but atEnd() is false: '" + text + "' offset " + charOffset,
+                        state.atEnd());
+                break;
+            }
+            Object expected = expecteds[cpOffset];
+            Iterator actual = state.getCurrentMatches();
+            checkResult("ParseState '" + text + "' offset " + charOffset, actual, expected);
+            charOffset += Character.charCount(cp);
+        }
     }
 
     private boolean eql(Object o1, Object o2) {
@@ -147,10 +282,13 @@ public class TextTrieMapTest extends TestFmwk {
         return o1.equals(o2);
     }
 
-    private void checkResult(Iterator itr, Object expected) {
+    private void checkResult(String memo, Iterator itr, Object expected) {
         if (itr == null) {
             if (expected != null) {
-                errln("FAIL: Empty results - Expected: " + expected);
+                String expectedStr = (expected instanceof Object[])
+                        ? Arrays.toString((Object[]) expected)
+                        : expected.toString();
+                errln("FAIL: Empty results: " + memo + ": Expected: " + expectedStr);
             }
             return;
         }