]> granicus.if.org Git - icu/commitdiff
ICU-13177 Renaming classes and moving things around. No or very few behavior changes.
authorShane Carr <shane@unicode.org>
Fri, 1 Sep 2017 08:30:17 +0000 (08:30 +0000)
committerShane Carr <shane@unicode.org>
Fri, 1 Sep 2017 08:30:17 +0000 (08:30 +0000)
X-SVN-Rev: 40364

54 files changed:
icu4j/main/classes/core/src/com/ibm/icu/impl/number/AffixPatternUtils.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/LdmlPatternInfo.java [deleted file]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/Modifier.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/NumberStringBuilder.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/Parse.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternAndPropertyUtils.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternParser.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternString.java [deleted file]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/Properties.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/ThingsNeedingNewHome.java [deleted file]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/ConstantAffixModifier.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/ConstantMultiFieldModifier.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/CurrencySpacingEnabledModifier.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/GeneralPluralModifier.java [deleted file]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/PositiveNegativeAffixModifier.java [deleted file]
icu4j/main/classes/core/src/com/ibm/icu/impl/number/modifiers/SimpleModifier.java
icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormat.java
icu4j/main/classes/core/src/com/ibm/icu/text/DecimalFormatSymbols.java
icu4j/main/classes/core/src/com/ibm/icu/util/Dimensionless.java [deleted file]
icu4j/main/classes/core/src/com/ibm/icu/util/MeasureUnit.java
icu4j/main/classes/core/src/com/ibm/icu/util/NoUnit.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/CompactNotation.java
icu4j/main/classes/core/src/newapi/FormattedNumber.java
icu4j/main/classes/core/src/newapi/FractionRounder.java
icu4j/main/classes/core/src/newapi/Grouper.java
icu4j/main/classes/core/src/newapi/LongNameHandler.java [moved from icu4j/main/classes/core/src/newapi/MurkyLongNameHandler.java with 64% similarity]
icu4j/main/classes/core/src/newapi/MutablePatternModifier.java [moved from icu4j/main/classes/core/src/newapi/MurkyModifier.java with 90% similarity]
icu4j/main/classes/core/src/newapi/NumberFormatter.java
icu4j/main/classes/core/src/newapi/NumberFormatterSettings.java
icu4j/main/classes/core/src/newapi/NumberPropertyMapper.java
icu4j/main/classes/core/src/newapi/Rounder.java
icu4j/main/classes/core/src/newapi/ScientificNotation.java
icu4j/main/classes/core/src/newapi/SkeletonBuilder.java
icu4j/main/classes/core/src/newapi/Worker1.java
icu4j/main/classes/core/src/newapi/impl/MacroProps.java
icu4j/main/classes/core/src/newapi/impl/MeasureData.java
icu4j/main/classes/core/src/newapi/impl/MicroProps.java
icu4j/main/classes/core/src/newapi/impl/MicroPropsGenerator.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/MicroPropsMutator.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/MultiplierImpl.java
icu4j/main/classes/core/src/newapi/impl/Padder.java
icu4j/main/classes/core/src/newapi/impl/QuantityChain.java [deleted file]
icu4j/main/classes/core/src/newapi/impl/demo.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
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/NumberFormatTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/AffixPatternUtilsTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/ModifierTest.java [new file with mode: 0644]
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/MurkyModifierTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberStringBuilderTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PatternStringTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/PropertiesTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java

index 56c824c08ced057f11eccc5b68391dfac814032b..e0eef171262ba8eefd4e2036b8b6c1fc93126e0a 100644 (file)
@@ -89,6 +89,10 @@ public class AffixPatternUtils {
   /** Represents a sequence of six or more currency symbols. */
   public static final int TYPE_CURRENCY_OVERFLOW = -15;
 
+  public static interface SymbolProvider {
+    public CharSequence getSymbol(int type);
+  }
+
   /**
    * 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
@@ -255,10 +259,6 @@ public class AffixPatternUtils {
     }
   }
 
-  public static interface SymbolProvider {
-    public CharSequence getSymbol(int type);
-  }
-
   /**
    * Executes the unescape state machine. Replaces the unquoted characters "-", "+", "%", "‰", and
    * "¤" with the corresponding symbols provided by the {@link SymbolProvider}, and inserts the
@@ -276,26 +276,22 @@ public class AffixPatternUtils {
       NumberStringBuilder output,
       int position,
       SymbolProvider provider) {
-    // TODO: Is it worth removing this extra local object instantiation here?
-    NumberStringBuilder local = new NumberStringBuilder(10);
     assert affixPattern != null;
+    int length = 0;
     long tag = 0L;
     while (hasNext(tag, affixPattern)) {
       tag = nextToken(tag, affixPattern);
       int typeOrCp = getTypeOrCp(tag);
       if (typeOrCp == TYPE_CURRENCY_OVERFLOW) {
         // Don't go to the provider for this special case
-        local.appendCodePoint(0xFFFD, NumberFormat.Field.CURRENCY);
+        length += output.insertCodePoint(position + length, 0xFFFD, NumberFormat.Field.CURRENCY);
       } else if (typeOrCp < 0) {
-        local.append(provider.getSymbol(typeOrCp), getFieldForType(typeOrCp));
+        length += output.insert(position + length, provider.getSymbol(typeOrCp), getFieldForType(typeOrCp));
       } else {
-        local.appendCodePoint(typeOrCp, null);
+        length += output.insertCodePoint(position + length, typeOrCp, null);
       }
     }
-    if (output != null) {
-      output.insert(position, local);
-    }
-    return local.length();
+    return length;
   }
 
   /**
@@ -307,7 +303,9 @@ public class AffixPatternUtils {
    * @return true if the affix pattern contains the given token type; false otherwise.
    */
   public static boolean containsType(CharSequence affixPattern, int type) {
-    if (affixPattern == null || affixPattern.length() == 0) return false;
+    if (affixPattern == null || affixPattern.length() == 0) {
+        return false;
+    }
     long tag = 0L;
     while (hasNext(tag, affixPattern)) {
       tag = nextToken(tag, affixPattern);
@@ -548,7 +546,7 @@ public class AffixPatternUtils {
   public static int getTypeOrCp(long tag) {
     assert tag >= 0;
     int type = getType(tag);
-    return (type == 0) ? getCodePoint(tag) : -type;
+    return (type == TYPE_CODEPOINT) ? getCodePoint(tag) : -type;
   }
 
   /**
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LdmlPatternInfo.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/LdmlPatternInfo.java
deleted file mode 100644 (file)
index 745b4d3..0000000
+++ /dev/null
@@ -1,447 +0,0 @@
-// © 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.ThingsNeedingNewHome.PadPosition;
-
-import newapi.impl.AffixPatternProvider;
-
-/** Implements a recursive descent parser for decimal format patterns. */
-public class LdmlPatternInfo {
-
-  public static PatternParseResult parse(String patternString) {
-    ParserState state = new ParserState(patternString);
-    PatternParseResult result = new PatternParseResult(patternString);
-    consumePattern(state, result);
-    return result;
-  }
-
-  /**
-   * An internal, intermediate data structure used for storing parse results before they are
-   * finalized into a DecimalFormatPattern.Builder.
-   */
-  public static class PatternParseResult implements AffixPatternProvider {
-    public String pattern;
-    public LdmlPatternInfo.SubpatternParseResult positive;
-    public LdmlPatternInfo.SubpatternParseResult negative;
-
-    private PatternParseResult(String pattern) {
-      this.pattern = pattern;
-    }
-
-    @Override
-    public char charAt(int flags, int index) {
-      long endpoints = getEndpoints(flags);
-      int left = (int) (endpoints & 0xffffffff);
-      int right = (int) (endpoints >>> 32);
-      if (index < 0 || index >= right - left) {
-        throw new IndexOutOfBoundsException();
-      }
-      return pattern.charAt(left + index);
-    }
-
-    @Override
-    public int length(int flags) {
-      return getLengthFromEndpoints(getEndpoints(flags));
-    }
-
-    public static int getLengthFromEndpoints(long endpoints) {
-      int left = (int) (endpoints & 0xffffffff);
-      int right = (int) (endpoints >>> 32);
-      return right - left;
-    }
-
-    public String getString(int flags) {
-      long endpoints = getEndpoints(flags);
-      int left = (int) (endpoints & 0xffffffff);
-      int right = (int) (endpoints >>> 32);
-      if (left == right) {
-        return "";
-      }
-      return pattern.substring(left, right);
-    }
-
-    private long getEndpoints(int flags) {
-      boolean prefix = (flags & Flags.PREFIX) != 0;
-      boolean isNegative = (flags & Flags.NEGATIVE_SUBPATTERN) != 0;
-      boolean padding = (flags & Flags.PADDING) != 0;
-      if (isNegative && padding) {
-        return negative.paddingEndpoints;
-      } else if (padding) {
-        return positive.paddingEndpoints;
-      } else if (prefix && isNegative) {
-        return negative.prefixEndpoints;
-      } else if (prefix) {
-        return positive.prefixEndpoints;
-      } else if (isNegative) {
-        return negative.suffixEndpoints;
-      } else {
-        return positive.suffixEndpoints;
-      }
-    }
-
-    @Override
-    public boolean positiveHasPlusSign() {
-      return positive.hasPlusSign;
-    }
-
-    @Override
-    public boolean hasNegativeSubpattern() {
-      return negative != null;
-    }
-
-    @Override
-    public boolean negativeHasMinusSign() {
-      return negative.hasMinusSign;
-    }
-
-    @Override
-    public boolean hasCurrencySign() {
-      return positive.hasCurrencySign || (negative != null && negative.hasCurrencySign);
-    }
-
-    @Override
-    public boolean containsSymbolType(int type) {
-      return AffixPatternUtils.containsType(pattern, type);
-    }
-  }
-
-  public static class SubpatternParseResult {
-    public long groupingSizes = 0x0000ffffffff0000L;
-    public int minimumIntegerDigits = 0;
-    public int totalIntegerDigits = 0;
-    public int minimumFractionDigits = 0;
-    public int maximumFractionDigits = 0;
-    public int minimumSignificantDigits = 0;
-    public int maximumSignificantDigits = 0;
-    public boolean hasDecimal = false;
-    public int paddingWidth = 0;
-    public PadPosition paddingLocation = null;
-    public FormatQuantity4 rounding = null;
-    public boolean exponentShowPlusSign = false;
-    public int exponentDigits = 0;
-    public boolean hasPercentSign = false;
-    public boolean hasPerMilleSign = false;
-    public boolean hasCurrencySign = false;
-    public boolean hasMinusSign = false;
-    public boolean hasPlusSign = false;
-
-    public long prefixEndpoints = 0;
-    public long suffixEndpoints = 0;
-    public long paddingEndpoints = 0;
-  }
-
-  /** 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("Malformed pattern for ICU DecimalFormat: \"");
-      sb.append(pattern);
-      sb.append("\": ");
-      sb.append(message);
-      sb.append(" at position ");
-      sb.append(offset);
-      return new IllegalArgumentException(sb.toString());
-    }
-  }
-
-  private static void consumePattern(
-      LdmlPatternInfo.ParserState state, LdmlPatternInfo.PatternParseResult result) {
-    // pattern := subpattern (';' subpattern)?
-    result.positive = new SubpatternParseResult();
-    consumeSubpattern(state, result.positive);
-    if (state.peek() == ';') {
-      state.next(); // consume the ';'
-      // Don't consume the negative subpattern if it is empty (trailing ';')
-      if (state.peek() != -1) {
-        result.negative = new SubpatternParseResult();
-        consumeSubpattern(state, result.negative);
-      }
-    }
-    if (state.peek() != -1) {
-      throw state.toParseException("Found unquoted special character");
-    }
-  }
-
-  private static void consumeSubpattern(
-      LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) {
-    // subpattern := literals? number exponent? literals?
-    consumePadding(state, result, PadPosition.BEFORE_PREFIX);
-    result.prefixEndpoints = consumeAffix(state, result);
-    consumePadding(state, result, PadPosition.AFTER_PREFIX);
-    consumeFormat(state, result);
-    consumeExponent(state, result);
-    consumePadding(state, result, PadPosition.BEFORE_SUFFIX);
-    result.suffixEndpoints = consumeAffix(state, result);
-    consumePadding(state, result, PadPosition.AFTER_SUFFIX);
-  }
-
-  private static void consumePadding(
-      LdmlPatternInfo.ParserState state,
-      LdmlPatternInfo.SubpatternParseResult result,
-      PadPosition paddingLocation) {
-    if (state.peek() != '*') {
-      return;
-    }
-    result.paddingLocation = paddingLocation;
-    state.next(); // consume the '*'
-    result.paddingEndpoints |= state.offset;
-    consumeLiteral(state);
-    result.paddingEndpoints |= ((long) state.offset) << 32;
-  }
-
-  private static long consumeAffix(
-      LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) {
-    // literals := { literal }
-    long endpoints = state.offset;
-    outer:
-    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
-          break outer;
-
-        case '%':
-          result.hasPercentSign = true;
-          break;
-
-        case '‰':
-          result.hasPerMilleSign = true;
-          break;
-
-        case '¤':
-          result.hasCurrencySign = true;
-          break;
-
-        case '-':
-          result.hasMinusSign = true;
-          break;
-
-        case '+':
-          result.hasPlusSign = true;
-          break;
-      }
-      consumeLiteral(state);
-    }
-    endpoints |= ((long) state.offset) << 32;
-    return endpoints;
-  }
-
-  private static void consumeLiteral(LdmlPatternInfo.ParserState state) {
-    if (state.peek() == -1) {
-      throw state.toParseException("Expected unquoted literal but found EOL");
-    } else if (state.peek() == '\'') {
-      state.next(); // consume the starting quote
-      while (state.peek() != '\'') {
-        if (state.peek() == -1) {
-          throw state.toParseException("Expected quoted literal but found EOL");
-        } else {
-          state.next(); // consume a quoted character
-        }
-      }
-      state.next(); // consume the ending quote
-    } else {
-      // consume a non-quoted literal character
-      state.next();
-    }
-  }
-
-  private static void consumeFormat(
-      LdmlPatternInfo.ParserState state, LdmlPatternInfo.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(
-      LdmlPatternInfo.ParserState state, LdmlPatternInfo.SubpatternParseResult result) {
-    boolean seenSignificantDigitMarker = false;
-    boolean seenDigit = false;
-
-    outer:
-    while (true) {
-      switch (state.peek()) {
-        case ',':
-          result.paddingWidth += 1;
-          result.groupingSizes <<= 16;
-          break;
-
-        case '#':
-          if (seenDigit) throw state.toParseException("# cannot follow 0 before decimal point");
-          result.paddingWidth += 1;
-          result.groupingSizes += 1;
-          result.totalIntegerDigits += (seenSignificantDigitMarker ? 0 : 1);
-          // no change to result.minimumIntegerDigits
-          // no change to result.minimumSignificantDigits
-          result.maximumSignificantDigits += (seenSignificantDigitMarker ? 1 : 0);
-          if (result.rounding != null) {
-            result.rounding.appendDigit((byte) 0, 0, true);
-          }
-          break;
-
-        case '@':
-          seenSignificantDigitMarker = true;
-          if (seenDigit) throw state.toParseException("Cannot mix 0 and @");
-          result.paddingWidth += 1;
-          result.groupingSizes += 1;
-          result.totalIntegerDigits += 1;
-          // no change to result.minimumIntegerDigits
-          result.minimumSignificantDigits += 1;
-          result.maximumSignificantDigits += 1;
-          if (result.rounding != null) {
-            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("Cannot mix @ and 0");
-          // TODO: Crash here if we've seen the significant digit marker? See NumberFormatTestCases.txt
-          result.paddingWidth += 1;
-          result.groupingSizes += 1;
-          result.totalIntegerDigits += 1;
-          result.minimumIntegerDigits += 1;
-          // no change to result.minimumSignificantDigits
-          // no change to result.maximumSignificantDigits
-          if (state.peek() != '0' && result.rounding == null) {
-            result.rounding = new FormatQuantity4();
-          }
-          if (result.rounding != null) {
-            result.rounding.appendDigit((byte) (state.peek() - '0'), 0, true);
-          }
-          break;
-
-        default:
-          break outer;
-      }
-      state.next(); // consume the symbol
-    }
-
-    // Disallow patterns with a trailing ',' or with two ',' next to each other
-    short grouping1 = (short) (result.groupingSizes & 0xffff);
-    short grouping2 = (short) ((result.groupingSizes >>> 16) & 0xffff);
-    short grouping3 = (short) ((result.groupingSizes >>> 32) & 0xffff);
-    if (grouping1 == 0 && grouping2 != -1) {
-      throw state.toParseException("Trailing grouping separator is invalid");
-    }
-    if (grouping2 == 0 && grouping3 != -1) {
-      throw state.toParseException("Grouping width of zero is invalid");
-    }
-  }
-
-  private static void consumeFractionFormat(
-      LdmlPatternInfo.ParserState state, LdmlPatternInfo.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 {
-            if (result.rounding == null) {
-              result.rounding = new FormatQuantity4();
-            }
-            result.rounding.appendDigit((byte) (state.peek() - '0'), zeroCounter, false);
-            zeroCounter = 0;
-          }
-          break;
-
-        default:
-          return;
-      }
-      state.next(); // consume the symbol
-    }
-  }
-
-  private static void consumeExponent(
-      LdmlPatternInfo.ParserState state, LdmlPatternInfo.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++;
-    }
-  }
-}
index 8bb55e145ddc1e14a20f98230964a3c38090bf3e..11eb56bc85b825711af1e5af51d32728cbc6b792 100644 (file)
@@ -2,21 +2,15 @@
 // 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;
+import newapi.MutablePatternModifier;
 
 /**
- * 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.
+ * A Modifier is an 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
+ * A Modifier is usually immutable, except in cases such as {@link MutablePatternModifier}, which are mutable for performance
+ * reasons.
  */
 public interface Modifier {
 
@@ -28,8 +22,8 @@ public interface 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.
+     *            The right index of the string within the string builder. Equal to length 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);
@@ -50,62 +44,4 @@ public interface Modifier {
      * @return Whether the modifier is strong.
      */
     public boolean isStrong();
-
-    /**
-     * 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 {
-        /**
-         * 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 {
-        /**
-         * 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 implements Modifier, PositiveNegativeModifier {
-
-        @Override
-        public Modifier getModifier(boolean isNegative) {
-            return this;
-        }
-    }
 }
index 2d4878fb5e65fab08d76b6869e19a65f88edb99f..f103b5eb889acd4ba4d7b3302bab2e1f73694ef7 100644 (file)
@@ -13,460 +13,490 @@ import com.ibm.icu.text.NumberFormat;
 import com.ibm.icu.text.NumberFormat.Field;
 
 /**
- * A StringBuilder optimized for number formatting. It implements the following key features beyond
- * a normal JDK StringBuilder:
+ * A StringBuilder optimized for number formatting. It implements the following key features beyond a normal JDK
+ * StringBuilder:
  *
  * <ol>
- *   <li>Efficient prepend as well as append.
- *   <li>Keeps tracks of Fields in an efficient manner.
- *   <li>String operations are fast-pathed to code point operations when possible.
+ * <li>Efficient prepend as well as append.
+ * <li>Keeps tracks of Fields in an efficient manner.
+ * <li>String operations are fast-pathed to code point operations when possible.
  * </ol>
  */
 public class NumberStringBuilder implements CharSequence {
 
-  /** A constant, empty NumberStringBuilder. Do NOT call mutative operations on this. */
-  public static final NumberStringBuilder EMPTY = new NumberStringBuilder();
-
-  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;
-  }
-
-  public NumberStringBuilder(NumberStringBuilder source) {
-    copyFrom(source);
-  }
-
-  public void copyFrom(NumberStringBuilder source) {
-    chars = Arrays.copyOf(source.chars, source.chars.length);
-    fields = Arrays.copyOf(source.fields, source.fields.length);
-    zero = source.zero;
-    length = source.length;
-  }
-
-  @Override
-  public int length() {
-    return length;
-  }
-
-  public int codePointCount() {
-    return Character.codePointCount(this, 0, length());
-  }
-
-  @Override
-  public char charAt(int index) {
-    assert index >= 0;
-    assert index < length;
-    return chars[zero + index];
-  }
-
-  public Field fieldAt(int index) {
-    assert index >= 0;
-    assert index < length;
-    return fields[zero + index];
-  }
-
-  public NumberStringBuilder clear() {
-    zero = chars.length / 2;
-    length = 0;
-    return this;
-  }
-
-  /**
-   * 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) {
-    if (this == other) {
-      throw new IllegalArgumentException("Cannot call insert/append on myself");
-    }
-    int count = other.length;
-    if (count == 0) {
-      // Nothing to insert.
-      return 0;
-    }
-    int position = prepareForInsert(index, count);
-    for (int i = 0; i < count; i++) {
-      this.chars[position + i] = other.charAt(i);
-      this.fields[position + i] = other.fieldAt(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) {
-    // Java note: Keeping this code out of prepareForInsert() increases the speed of append operations.
-    int oldCapacity = chars.length;
-    int oldZero = zero;
-    char[] oldChars = chars;
-    Field[] oldFields = fields;
-    if (length + count > oldCapacity) {
-      int newCapacity = (length + count) * 2;
-      int newZero = newCapacity / 2 - (length + count) / 2;
-
-      char[] newChars = new char[newCapacity];
-      Field[] newFields = new Field[newCapacity];
-
-      // First copy the prefix and then the suffix, leaving room for the new chars that the
-      // caller wants to insert.
-      System.arraycopy(oldChars, oldZero, newChars, newZero, index);
-      System.arraycopy(
-          oldChars, oldZero + index, newChars, newZero + index + count, length - index);
-      System.arraycopy(oldFields, oldZero, newFields, newZero, index);
-      System.arraycopy(
-          oldFields, oldZero + index, newFields, newZero + index + count, length - index);
-
-      chars = newChars;
-      fields = newFields;
-      zero = newZero;
-      length += count;
-    } else {
-      int newZero = oldCapacity / 2 - (length + count) / 2;
-
-      // First copy the entire string to the location of the prefix, and then move the suffix
-      // to make room for the new chars that the caller wants to insert.
-      System.arraycopy(oldChars, oldZero, oldChars, newZero, length);
-      System.arraycopy(
-          oldChars, newZero + index, oldChars, newZero + index + count, length - index);
-      System.arraycopy(oldFields, oldZero, oldFields, newZero, length);
-      System.arraycopy(
-          oldFields, newZero + index, oldFields, 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 = new NumberStringBuilder(this);
-    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 (charAt(i) != other.charAt(i) || fieldAt(i) != other.fieldAt(i)) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  @Override
-  public int hashCode() {
-    throw new UnsupportedOperationException("Don't call #hashCode() or #equals() on a mutable.");
-  }
-
-  @Override
-  public boolean equals(Object other) {
-    throw new UnsupportedOperationException("Don't call #hashCode() or #equals() on a mutable.");
-  }
-
-  /**
-   * 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;
+    /** A constant, empty NumberStringBuilder. Do NOT call mutative operations on this. */
+    public static final NumberStringBuilder EMPTY = new NumberStringBuilder();
+
+    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;
+    }
+
+    public NumberStringBuilder(NumberStringBuilder source) {
+        copyFrom(source);
+    }
+
+    public void copyFrom(NumberStringBuilder source) {
+        chars = Arrays.copyOf(source.chars, source.chars.length);
+        fields = Arrays.copyOf(source.fields, source.fields.length);
+        zero = source.zero;
+        length = source.length;
+    }
+
+    @Override
+    public int length() {
+        return length;
+    }
+
+    public int codePointCount() {
+        return Character.codePointCount(this, 0, length());
+    }
+
+    @Override
+    public char charAt(int index) {
+        assert index >= 0;
+        assert index < length;
+        return chars[zero + index];
+    }
+
+    public Field fieldAt(int index) {
+        assert index >= 0;
+        assert index < length;
+        return fields[zero + index];
+    }
+
+    public int getFirstCodePoint() {
+        if (length == 0) {
+            return -1;
         }
-        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 + offset);
-      fp.setEndIndex(fractionStart + offset);
-    }
-  }
-
-  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);
+        return Character.codePointAt(chars, zero, zero + length);
+    }
+
+    public int getLastCodePoint() {
+        if (length == 0) {
+            return -1;
+        }
+        return Character.codePointBefore(chars, zero + length, zero);
+    }
+
+    public int codePointAt(int index) {
+        return Character.codePointAt(chars, zero + index, zero + length);
+    }
+
+    public int codePointBefore(int index) {
+        return Character.codePointBefore(chars, zero + index, zero);
+    }
+
+    public NumberStringBuilder clear() {
+        zero = getCapacity() / 2;
+        length = 0;
+        return this;
+    }
+
+    /**
+     * 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) {
+        if (this == other) {
+            throw new IllegalArgumentException("Cannot call insert/append on myself");
+        }
+        int count = other.length;
+        if (count == 0) {
+            // Nothing to insert.
+            return 0;
+        }
+        int position = prepareForInsert(index, count);
+        for (int i = 0; i < count; i++) {
+            this.chars[position + i] = other.charAt(i);
+            this.fields[position + i] = other.fieldAt(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 < getCapacity()) {
+            // 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) {
+        // Java note: Keeping this code out of prepareForInsert() increases the speed of append operations.
+        int oldCapacity = getCapacity();
+        int oldZero = zero;
+        char[] oldChars = chars;
+        Field[] oldFields = fields;
+        if (length + count > oldCapacity) {
+            int newCapacity = (length + count) * 2;
+            int newZero = newCapacity / 2 - (length + count) / 2;
+
+            char[] newChars = new char[newCapacity];
+            Field[] newFields = new Field[newCapacity];
+
+            // First copy the prefix and then the suffix, leaving room for the new chars that the
+            // caller wants to insert.
+            System.arraycopy(oldChars, oldZero, newChars, newZero, index);
+            System.arraycopy(oldChars, oldZero + index, newChars, newZero + index + count, length - index);
+            System.arraycopy(oldFields, oldZero, newFields, newZero, index);
+            System.arraycopy(oldFields, oldZero + index, newFields, newZero + index + count, length - index);
+
+            chars = newChars;
+            fields = newFields;
+            zero = newZero;
+            length += count;
+        } else {
+            int newZero = oldCapacity / 2 - (length + count) / 2;
+
+            // First copy the entire string to the location of the prefix, and then move the suffix
+            // to make room for the new chars that the caller wants to insert.
+            System.arraycopy(oldChars, oldZero, oldChars, newZero, length);
+            System.arraycopy(oldChars, newZero + index, oldChars, newZero + index + count, length - index);
+            System.arraycopy(oldFields, oldZero, oldFields, newZero, length);
+            System.arraycopy(oldFields, newZero + index, oldFields, newZero + index + count, length - index);
+
+            zero = newZero;
+            length += count;
+        }
+        return zero + index;
+    }
+
+    private int getCapacity() {
+        return chars.length;
+    }
+
+    @Override
+    public CharSequence subSequence(int start, int end) {
+        if (start < 0 || end > length || end < start) {
+            throw new IndexOutOfBoundsException();
+        }
+        NumberStringBuilder other = new NumberStringBuilder(this);
+        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 (charAt(i) != other.charAt(i) || fieldAt(i) != other.fieldAt(i)) {
+                return false;
+            }
         }
-        current = field;
-        currentStart = i;
-      }
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        throw new UnsupportedOperationException("Don't call #hashCode() or #equals() on a mutable.");
     }
-    if (current != null) {
-      as.addAttribute(current, current, currentStart, length);
+
+    @Override
+    public boolean equals(Object other) {
+        throw new UnsupportedOperationException("Don't call #hashCode() or #equals() on a mutable.");
+    }
+
+    /**
+     * 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 + offset);
+            fp.setEndIndex(fractionStart + offset);
+        }
     }
 
-    return as.getIterator();
-  }
+    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();
+    }
 }
index e921a2eedeb91cd934246dbd0ea9e3bb68f91800..87b588ae0f9d8d938780bd932c736151ad7446ea 100644 (file)
@@ -837,7 +837,7 @@ public class Parse {
     private void addPattern(String pattern) {
       Properties properties = threadLocalProperties.get();
       try {
-        PatternString.parseToExistingProperties(pattern, properties);
+        PatternAndPropertyUtils.parseToExistingProperties(pattern, properties);
       } catch (IllegalArgumentException e) {
         // This should only happen if there is a bug in CLDR data. Fail silently.
       }
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternAndPropertyUtils.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternAndPropertyUtils.java
new file mode 100644 (file)
index 0000000..06a0dd1
--- /dev/null
@@ -0,0 +1,624 @@
+// © 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.PatternParser.ParsedPatternInfo;
+import com.ibm.icu.impl.number.PatternParser.ParsedSubpatternInfo;
+import com.ibm.icu.text.DecimalFormatSymbols;
+
+import newapi.impl.AffixPatternProvider;
+import newapi.impl.Padder;
+import newapi.impl.Padder.PadPosition;
+
+/**
+ * Handles parsing and creation of the compact pattern string representation of a decimal format.
+ */
+public class PatternAndPropertyUtils {
+
+    /**
+     * 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. One of {@link #IGNORE_ROUNDING_ALWAYS}, {@link #IGNORE_ROUNDING_IF_CURRENCY}, or
+     *            {@link #IGNORE_ROUNDING_NEVER}.
+     * @return A property bag object.
+     * @throws IllegalArgumentException
+     *             If there is a syntax error in the pattern string.
+     */
+    public static Properties parseToProperties(String pattern, int ignoreRounding) {
+        Properties properties = new Properties();
+        parse(pattern, properties, ignoreRounding);
+        return properties;
+    }
+
+    public static Properties parseToProperties(String pattern) {
+        return parseToProperties(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_NEVER);
+    }
+
+    /**
+     * 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. One of {@link #IGNORE_ROUNDING_ALWAYS}, {@link #IGNORE_ROUNDING_IF_CURRENCY}, or
+     *            {@link #IGNORE_ROUNDING_NEVER}.
+     * @throws IllegalArgumentException
+     *             If there was a syntax error in the pattern string.
+     */
+    public static void parseToExistingProperties(String pattern, Properties properties, int ignoreRounding) {
+        parse(pattern, properties, ignoreRounding);
+    }
+
+    public static void parseToExistingProperties(String pattern, Properties properties) {
+        parseToExistingProperties(pattern, properties, PatternAndPropertyUtils.IGNORE_ROUNDING_NEVER);
+    }
+
+    /**
+     * 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, -1) && firstGroupingSize != Math.min(dosMax, -1)
+                && groupingSize != firstGroupingSize) {
+            grouping = groupingSize;
+            grouping1 = groupingSize;
+            grouping2 = firstGroupingSize;
+        } else if (groupingSize != Math.min(dosMax, -1)) {
+            grouping = groupingSize;
+            grouping1 = 0;
+            grouping2 = groupingSize;
+        } else if (firstGroupingSize != Math.min(dosMax, -1)) {
+            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, -1)) {
+            // Significant Digits.
+            while (digitsString.length() < minSig) {
+                digitsString.append('@');
+            }
+            while (digitsString.length() < maxSig) {
+                digitsString.append('#');
+            }
+        } else if (roundingInterval != null) {
+            // 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, -1)) {
+            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 != -1) {
+            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 = Padder.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.
+     *
+     * <p>
+     * A greedy string-substitution strategy is used to substitute locale symbols. If two symbols are ambiguous or have
+     * the same prefix, the result is not well-defined.
+     *
+     * <p>
+     * Locale symbols are not allowed to contain the ASCII quote character.
+     *
+     * @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(String input, DecimalFormatSymbols symbols, boolean toLocalized) {
+        if (input == null)
+            return null;
+
+        // Construct a table of strings to be converted between localized and standard.
+        String[][] table = new String[21][2];
+        int standIdx = toLocalized ? 0 : 1;
+        int localIdx = toLocalized ? 1 : 0;
+        table[0][standIdx] = "%";
+        table[0][localIdx] = symbols.getPercentString();
+        table[1][standIdx] = "‰";
+        table[1][localIdx] = symbols.getPerMillString();
+        table[2][standIdx] = ".";
+        table[2][localIdx] = symbols.getDecimalSeparatorString();
+        table[3][standIdx] = ",";
+        table[3][localIdx] = symbols.getGroupingSeparatorString();
+        table[4][standIdx] = "-";
+        table[4][localIdx] = symbols.getMinusSignString();
+        table[5][standIdx] = "+";
+        table[5][localIdx] = symbols.getPlusSignString();
+        table[6][standIdx] = ";";
+        table[6][localIdx] = Character.toString(symbols.getPatternSeparator());
+        table[7][standIdx] = "@";
+        table[7][localIdx] = Character.toString(symbols.getSignificantDigit());
+        table[8][standIdx] = "E";
+        table[8][localIdx] = symbols.getExponentSeparator();
+        table[9][standIdx] = "*";
+        table[9][localIdx] = Character.toString(symbols.getPadEscape());
+        table[10][standIdx] = "#";
+        table[10][localIdx] = Character.toString(symbols.getDigit());
+        for (int i = 0; i < 10; i++) {
+            table[11 + i][standIdx] = Character.toString((char) ('0' + i));
+            table[11 + i][localIdx] = symbols.getDigitStringsLocal()[i];
+        }
+
+        // Special case: quotes are NOT allowed to be in any localIdx strings.
+        // Substitute them with '’' instead.
+        for (int i = 0; i < table.length; i++) {
+            table[i][localIdx] = table[i][localIdx].replace('\'', '’');
+        }
+
+        // Iterate through the string and convert.
+        // State table:
+        // 0 => base state
+        // 1 => first char inside a quoted sequence in input and output string
+        // 2 => inside a quoted sequence in input and output string
+        // 3 => first char after a close quote in input string;
+        // close quote still needs to be written to output string
+        // 4 => base state in input string; inside quoted sequence in output string
+        // 5 => first char inside a quoted sequence in input string;
+        // inside quoted sequence in output string
+        StringBuilder result = new StringBuilder();
+        int state = 0;
+        outer: for (int offset = 0; offset < input.length(); offset++) {
+            char ch = input.charAt(offset);
+
+            // Handle a quote character (state shift)
+            if (ch == '\'') {
+                if (state == 0) {
+                    result.append('\'');
+                    state = 1;
+                    continue;
+                } else if (state == 1) {
+                    result.append('\'');
+                    state = 0;
+                    continue;
+                } else if (state == 2) {
+                    state = 3;
+                    continue;
+                } else if (state == 3) {
+                    result.append('\'');
+                    result.append('\'');
+                    state = 1;
+                    continue;
+                } else if (state == 4) {
+                    state = 5;
+                    continue;
+                } else {
+                    assert state == 5;
+                    result.append('\'');
+                    result.append('\'');
+                    state = 4;
+                    continue;
+                }
+            }
+
+            if (state == 0 || state == 3 || state == 4) {
+                for (String[] pair : table) {
+                    // Perform a greedy match on this symbol string
+                    if (input.regionMatches(offset, pair[0], 0, pair[0].length())) {
+                        // Skip ahead past this region for the next iteration
+                        offset += pair[0].length() - 1;
+                        if (state == 3 || state == 4) {
+                            result.append('\'');
+                            state = 0;
+                        }
+                        result.append(pair[1]);
+                        continue outer;
+                    }
+                }
+                // No replacement found. Check if a special quote is necessary
+                for (String[] pair : table) {
+                    if (input.regionMatches(offset, pair[1], 0, pair[1].length())) {
+                        if (state == 0) {
+                            result.append('\'');
+                            state = 4;
+                        }
+                        result.append(ch);
+                        continue outer;
+                    }
+                }
+                // Still nothing. Copy the char verbatim. (Add a close quote if necessary)
+                if (state == 3 || state == 4) {
+                    result.append('\'');
+                    state = 0;
+                }
+                result.append(ch);
+            } else {
+                assert state == 1 || state == 2 || state == 5;
+                result.append(ch);
+                state = 2;
+            }
+        }
+        // Resolve final quotes
+        if (state == 3 || state == 4) {
+            result.append('\'');
+            state = 0;
+        }
+        if (state != 0) {
+            throw new IllegalArgumentException("Malformed localized pattern: unterminated quote");
+        }
+        return result.toString();
+    }
+
+    public static final int IGNORE_ROUNDING_NEVER = 0;
+    public static final int IGNORE_ROUNDING_IF_CURRENCY = 1;
+    public static final int IGNORE_ROUNDING_ALWAYS = 2;
+
+    static void parse(String pattern, Properties properties, int 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 thread locals here?
+        ParsedPatternInfo patternInfo = PatternParser.parse(pattern);
+        saveToProperties(properties, patternInfo, ignoreRounding);
+    }
+
+    /** Finalizes the temporary data stored in the ParsedPatternInfo to the Properties. */
+    private static void saveToProperties(Properties properties, ParsedPatternInfo patternInfo, int _ignoreRounding) {
+        // Translate from PatternParseResult to Properties.
+        // Note that most data from "negative" is ignored per the specification of DecimalFormat.
+
+        ParsedSubpatternInfo positive = patternInfo.positive;
+        ParsedSubpatternInfo negative = patternInfo.negative;
+
+        boolean ignoreRounding;
+        if (_ignoreRounding == IGNORE_ROUNDING_NEVER) {
+            ignoreRounding = false;
+        } else if (_ignoreRounding == IGNORE_ROUNDING_IF_CURRENCY) {
+            ignoreRounding = positive.hasCurrencySign;
+        } else {
+            assert _ignoreRounding == IGNORE_ROUNDING_ALWAYS;
+            ignoreRounding = true;
+        }
+
+        // Grouping settings
+        short grouping1 = (short) (positive.groupingSizes & 0xffff);
+        short grouping2 = (short) ((positive.groupingSizes >>> 16) & 0xffff);
+        short grouping3 = (short) ((positive.groupingSizes >>> 32) & 0xffff);
+        if (grouping2 != -1) {
+            properties.setGroupingSize(grouping1);
+        } else {
+            properties.setGroupingSize(-1);
+        }
+        if (grouping3 != -1) {
+            properties.setSecondaryGroupingSize(grouping2);
+        } else {
+            properties.setSecondaryGroupingSize(-1);
+        }
+
+        // For backwards compatibility, require that the pattern emit at least one min digit.
+        int minInt, minFrac;
+        if (positive.integerTotal == 0 && positive.fractionTotal > 0) {
+            // patterns like ".##"
+            minInt = 0;
+            minFrac = Math.max(1, positive.fractionNumerals);
+        } else if (positive.integerNumerals == 0 && positive.fractionNumerals == 0) {
+            // patterns like "#.##"
+            minInt = 1;
+            minFrac = 0;
+        } else {
+            minInt = positive.integerNumerals;
+            minFrac = positive.fractionNumerals;
+        }
+
+        // Rounding settings
+        // Don't set basic rounding when there is a currency sign; defer to CurrencyUsage
+        if (positive.integerAtSigns > 0) {
+            properties.setMinimumFractionDigits(-1);
+            properties.setMaximumFractionDigits(-1);
+            properties.setRoundingIncrement(null);
+            properties.setMinimumSignificantDigits(positive.integerAtSigns);
+            properties.setMaximumSignificantDigits(positive.integerAtSigns + positive.integerTrailingHashSigns);
+        } else if (positive.rounding != null) {
+            if (!ignoreRounding) {
+                properties.setMinimumFractionDigits(minFrac);
+                properties.setMaximumFractionDigits(positive.fractionTotal);
+                properties.setRoundingIncrement(
+                        positive.rounding.toBigDecimal().setScale(positive.fractionNumerals));
+            } else {
+                properties.setMinimumFractionDigits(-1);
+                properties.setMaximumFractionDigits(-1);
+                properties.setRoundingIncrement(null);
+            }
+            properties.setMinimumSignificantDigits(-1);
+            properties.setMaximumSignificantDigits(-1);
+        } else {
+            if (!ignoreRounding) {
+                properties.setMinimumFractionDigits(minFrac);
+                properties.setMaximumFractionDigits(positive.fractionTotal);
+                properties.setRoundingIncrement(null);
+            } else {
+                properties.setMinimumFractionDigits(-1);
+                properties.setMaximumFractionDigits(-1);
+                properties.setRoundingIncrement(null);
+            }
+            properties.setMinimumSignificantDigits(-1);
+            properties.setMaximumSignificantDigits(-1);
+        }
+
+        // If the pattern ends with a '.' then force the decimal point.
+        if (positive.hasDecimal && positive.fractionTotal == 0) {
+            properties.setDecimalSeparatorAlwaysShown(true);
+        } else {
+            properties.setDecimalSeparatorAlwaysShown(false);
+        }
+
+        // Scientific notation settings
+        if (positive.exponentZeros > 0) {
+            properties.setExponentSignAlwaysShown(positive.exponentHasPlusSign);
+            properties.setMinimumExponentDigits(positive.exponentZeros);
+            if (positive.integerAtSigns == 0) {
+                // patterns without '@' can define max integer digits, used for engineering notation
+                properties.setMinimumIntegerDigits(positive.integerNumerals);
+                properties.setMaximumIntegerDigits(positive.integerTotal);
+            } else {
+                // patterns with '@' cannot define max integer digits
+                properties.setMinimumIntegerDigits(1);
+                properties.setMaximumIntegerDigits(-1);
+            }
+        } else {
+            properties.setExponentSignAlwaysShown(false);
+            properties.setMinimumExponentDigits(-1);
+            properties.setMinimumIntegerDigits(minInt);
+            properties.setMaximumIntegerDigits(-1);
+        }
+
+        // Compute the affix patterns (required for both padding and affixes)
+        String posPrefix = patternInfo.getString(AffixPatternProvider.Flags.PREFIX);
+        String posSuffix = patternInfo.getString(0);
+
+        // Padding settings
+        if (positive.paddingEndpoints != 0) {
+            // The width of the positive prefix and suffix templates are included in the padding
+            int paddingWidth = positive.widthExceptAffixes + AffixPatternUtils.estimateLength(posPrefix)
+                    + AffixPatternUtils.estimateLength(posSuffix);
+            properties.setFormatWidth(paddingWidth);
+            String rawPaddingString = patternInfo.getString(AffixPatternProvider.Flags.PADDING);
+            if (rawPaddingString.length() == 1) {
+                properties.setPadString(rawPaddingString);
+            } else if (rawPaddingString.length() == 2) {
+                if (rawPaddingString.charAt(0) == '\'') {
+                    properties.setPadString("'");
+                } else {
+                    properties.setPadString(rawPaddingString);
+                }
+            } else {
+                properties.setPadString(rawPaddingString.substring(1, rawPaddingString.length() - 1));
+            }
+            assert positive.paddingLocation != null;
+            properties.setPadPosition(positive.paddingLocation);
+        } else {
+            properties.setFormatWidth(-1);
+            properties.setPadString(null);
+            properties.setPadPosition(null);
+        }
+
+        // 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(posPrefix);
+        properties.setPositiveSuffixPattern(posSuffix);
+        if (negative != null) {
+            properties.setNegativePrefixPattern(patternInfo
+                    .getString(AffixPatternProvider.Flags.NEGATIVE_SUBPATTERN | AffixPatternProvider.Flags.PREFIX));
+            properties.setNegativeSuffixPattern(patternInfo.getString(AffixPatternProvider.Flags.NEGATIVE_SUBPATTERN));
+        } 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(0);
+        }
+    }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternParser.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternParser.java
new file mode 100644 (file)
index 0000000..f469d75
--- /dev/null
@@ -0,0 +1,443 @@
+// © 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 newapi.impl.AffixPatternProvider;
+import newapi.impl.Padder.PadPosition;
+
+/** Implements a recursive descent parser for decimal format patterns. */
+public class PatternParser {
+
+    /**
+     * Runs the recursive descent parser on the given pattern string, returning a data structure with raw information
+     * about the pattern string.
+     *
+     * <p>
+     * To obtain a more useful form of the data, consider using {@link PatternAndPropertyUtils#parse} instead.
+     *
+     * @param patternString
+     *            The LDML decimal format pattern (Excel-style pattern) to parse.
+     * @return The results of the parse.
+     */
+    public static ParsedPatternInfo parse(String patternString) {
+        ParserState state = new ParserState(patternString);
+        ParsedPatternInfo result = new ParsedPatternInfo(patternString);
+        consumePattern(state, result);
+        return result;
+    }
+
+    /**
+     * Contains information about
+     * @author sffc
+     *
+     */
+    public static class ParsedPatternInfo implements AffixPatternProvider {
+        public String pattern;
+        public ParsedSubpatternInfo positive;
+        public ParsedSubpatternInfo negative;
+
+        private ParsedPatternInfo(String pattern) {
+            this.pattern = pattern;
+        }
+
+        @Override
+        public char charAt(int flags, int index) {
+            long endpoints = getEndpoints(flags);
+            int left = (int) (endpoints & 0xffffffff);
+            int right = (int) (endpoints >>> 32);
+            if (index < 0 || index >= right - left) {
+                throw new IndexOutOfBoundsException();
+            }
+            return pattern.charAt(left + index);
+        }
+
+        @Override
+        public int length(int flags) {
+            return getLengthFromEndpoints(getEndpoints(flags));
+        }
+
+        public static int getLengthFromEndpoints(long endpoints) {
+            int left = (int) (endpoints & 0xffffffff);
+            int right = (int) (endpoints >>> 32);
+            return right - left;
+        }
+
+        public String getString(int flags) {
+            long endpoints = getEndpoints(flags);
+            int left = (int) (endpoints & 0xffffffff);
+            int right = (int) (endpoints >>> 32);
+            if (left == right) {
+                return "";
+            }
+            return pattern.substring(left, right);
+        }
+
+        private long getEndpoints(int flags) {
+            boolean prefix = (flags & Flags.PREFIX) != 0;
+            boolean isNegative = (flags & Flags.NEGATIVE_SUBPATTERN) != 0;
+            boolean padding = (flags & Flags.PADDING) != 0;
+            if (isNegative && padding) {
+                return negative.paddingEndpoints;
+            } else if (padding) {
+                return positive.paddingEndpoints;
+            } else if (prefix && isNegative) {
+                return negative.prefixEndpoints;
+            } else if (prefix) {
+                return positive.prefixEndpoints;
+            } else if (isNegative) {
+                return negative.suffixEndpoints;
+            } else {
+                return positive.suffixEndpoints;
+            }
+        }
+
+        @Override
+        public boolean positiveHasPlusSign() {
+            return positive.hasPlusSign;
+        }
+
+        @Override
+        public boolean hasNegativeSubpattern() {
+            return negative != null;
+        }
+
+        @Override
+        public boolean negativeHasMinusSign() {
+            return negative.hasMinusSign;
+        }
+
+        @Override
+        public boolean hasCurrencySign() {
+            return positive.hasCurrencySign || (negative != null && negative.hasCurrencySign);
+        }
+
+        @Override
+        public boolean containsSymbolType(int type) {
+            return AffixPatternUtils.containsType(pattern, type);
+        }
+    }
+
+    public static class ParsedSubpatternInfo {
+        public long groupingSizes = 0x0000ffffffff0000L;
+        public int integerLeadingHashSigns = 0;
+        public int integerTrailingHashSigns = 0;
+        public int integerNumerals = 0;
+        public int integerAtSigns = 0;
+        public int integerTotal = 0; // for convenience
+        public int fractionNumerals = 0;
+        public int fractionHashSigns = 0;
+        public int fractionTotal = 0; // for convenience
+        public boolean hasDecimal = false;
+        public int widthExceptAffixes = 0;
+        public PadPosition paddingLocation = null;
+        public FormatQuantity4 rounding = null;
+        public boolean exponentHasPlusSign = false;
+        public int exponentZeros = 0;
+        public boolean hasPercentSign = false;
+        public boolean hasPerMilleSign = false;
+        public boolean hasCurrencySign = false;
+        public boolean hasMinusSign = false;
+        public boolean hasPlusSign = false;
+
+        public long prefixEndpoints = 0;
+        public long suffixEndpoints = 0;
+        public long paddingEndpoints = 0;
+    }
+
+    /** 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("Malformed pattern for ICU DecimalFormat: \"");
+            sb.append(pattern);
+            sb.append("\": ");
+            sb.append(message);
+            sb.append(" at position ");
+            sb.append(offset);
+            return new IllegalArgumentException(sb.toString());
+        }
+    }
+
+    private static void consumePattern(ParserState state, ParsedPatternInfo result) {
+        // pattern := subpattern (';' subpattern)?
+        result.positive = new ParsedSubpatternInfo();
+        consumeSubpattern(state, result.positive);
+        if (state.peek() == ';') {
+            state.next(); // consume the ';'
+            // Don't consume the negative subpattern if it is empty (trailing ';')
+            if (state.peek() != -1) {
+                result.negative = new ParsedSubpatternInfo();
+                consumeSubpattern(state, result.negative);
+            }
+        }
+        if (state.peek() != -1) {
+            throw state.toParseException("Found unquoted special character");
+        }
+    }
+
+    private static void consumeSubpattern(ParserState state, ParsedSubpatternInfo result) {
+        // subpattern := literals? number exponent? literals?
+        consumePadding(state, result, PadPosition.BEFORE_PREFIX);
+        result.prefixEndpoints = consumeAffix(state, result);
+        consumePadding(state, result, PadPosition.AFTER_PREFIX);
+        consumeFormat(state, result);
+        consumeExponent(state, result);
+        consumePadding(state, result, PadPosition.BEFORE_SUFFIX);
+        result.suffixEndpoints = consumeAffix(state, result);
+        consumePadding(state, result, PadPosition.AFTER_SUFFIX);
+    }
+
+    private static void consumePadding(ParserState state, ParsedSubpatternInfo result, PadPosition paddingLocation) {
+        if (state.peek() != '*') {
+            return;
+        }
+        result.paddingLocation = paddingLocation;
+        state.next(); // consume the '*'
+        result.paddingEndpoints |= state.offset;
+        consumeLiteral(state);
+        result.paddingEndpoints |= ((long) state.offset) << 32;
+    }
+
+    private static long consumeAffix(ParserState state, ParsedSubpatternInfo result) {
+        // literals := { literal }
+        long endpoints = state.offset;
+        outer: 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
+                break outer;
+
+            case '%':
+                result.hasPercentSign = true;
+                break;
+
+            case '‰':
+                result.hasPerMilleSign = true;
+                break;
+
+            case '¤':
+                result.hasCurrencySign = true;
+                break;
+
+            case '-':
+                result.hasMinusSign = true;
+                break;
+
+            case '+':
+                result.hasPlusSign = true;
+                break;
+            }
+            consumeLiteral(state);
+        }
+        endpoints |= ((long) state.offset) << 32;
+        return endpoints;
+    }
+
+    private static void consumeLiteral(ParserState state) {
+        if (state.peek() == -1) {
+            throw state.toParseException("Expected unquoted literal but found EOL");
+        } else if (state.peek() == '\'') {
+            state.next(); // consume the starting quote
+            while (state.peek() != '\'') {
+                if (state.peek() == -1) {
+                    throw state.toParseException("Expected quoted literal but found EOL");
+                } else {
+                    state.next(); // consume a quoted character
+                }
+            }
+            state.next(); // consume the ending quote
+        } else {
+            // consume a non-quoted literal character
+            state.next();
+        }
+    }
+
+    private static void consumeFormat(ParserState state, ParsedSubpatternInfo result) {
+        consumeIntegerFormat(state, result);
+        if (state.peek() == '.') {
+            state.next(); // consume the decimal point
+            result.hasDecimal = true;
+            result.widthExceptAffixes += 1;
+            consumeFractionFormat(state, result);
+        }
+    }
+
+    private static void consumeIntegerFormat(ParserState state, ParsedSubpatternInfo result) {
+        outer: while (true) {
+            switch (state.peek()) {
+            case ',':
+                result.widthExceptAffixes += 1;
+                result.groupingSizes <<= 16;
+                break;
+
+            case '#':
+                if (result.integerNumerals > 0) {
+                    throw state.toParseException("# cannot follow 0 before decimal point");
+                }
+                result.widthExceptAffixes += 1;
+                result.groupingSizes += 1;
+                if (result.integerAtSigns > 0) {
+                    result.integerTrailingHashSigns += 1;
+                } else {
+                    result.integerLeadingHashSigns += 1;
+                }
+                result.integerTotal += 1;
+                break;
+
+            case '@':
+                if (result.integerNumerals > 0) {
+                    throw state.toParseException("Cannot mix 0 and @");
+                }
+                if (result.integerTrailingHashSigns > 0) {
+                    throw state.toParseException("Cannot nest # inside of a run of @");
+                }
+                result.widthExceptAffixes += 1;
+                result.groupingSizes += 1;
+                result.integerAtSigns += 1;
+                result.integerTotal += 1;
+                break;
+
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                if (result.integerAtSigns > 0) {
+                    throw state.toParseException("Cannot mix @ and 0");
+                }
+                result.widthExceptAffixes += 1;
+                result.groupingSizes += 1;
+                result.integerNumerals += 1;
+                result.integerTotal += 1;
+                if (state.peek() != '0' && result.rounding == null) {
+                    result.rounding = new FormatQuantity4();
+                }
+                if (result.rounding != null) {
+                    result.rounding.appendDigit((byte) (state.peek() - '0'), 0, true);
+                }
+                break;
+
+            default:
+                break outer;
+            }
+            state.next(); // consume the symbol
+        }
+
+        // Disallow patterns with a trailing ',' or with two ',' next to each other
+        short grouping1 = (short) (result.groupingSizes & 0xffff);
+        short grouping2 = (short) ((result.groupingSizes >>> 16) & 0xffff);
+        short grouping3 = (short) ((result.groupingSizes >>> 32) & 0xffff);
+        if (grouping1 == 0 && grouping2 != -1) {
+            throw state.toParseException("Trailing grouping separator is invalid");
+        }
+        if (grouping2 == 0 && grouping3 != -1) {
+            throw state.toParseException("Grouping width of zero is invalid");
+        }
+    }
+
+    private static void consumeFractionFormat(ParserState state, ParsedSubpatternInfo result) {
+        int zeroCounter = 0;
+        while (true) {
+            switch (state.peek()) {
+            case '#':
+                result.widthExceptAffixes += 1;
+                result.fractionHashSigns += 1;
+                result.fractionTotal += 1;
+                zeroCounter++;
+                break;
+
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                if (result.fractionHashSigns > 0) {
+                    throw state.toParseException("0 cannot follow # after decimal point");
+                }
+                result.widthExceptAffixes += 1;
+                result.fractionNumerals += 1;
+                result.fractionTotal += 1;
+                if (state.peek() == '0') {
+                    zeroCounter++;
+                } else {
+                    if (result.rounding == null) {
+                        result.rounding = new FormatQuantity4();
+                    }
+                    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, ParsedSubpatternInfo result) {
+        if (state.peek() != 'E') {
+            return;
+        }
+        state.next(); // consume the E
+        result.widthExceptAffixes++;
+        if (state.peek() == '+') {
+            state.next(); // consume the +
+            result.exponentHasPlusSign = true;
+            result.widthExceptAffixes++;
+        }
+        while (state.peek() == '0') {
+            state.next(); // consume the 0
+            result.exponentZeros += 1;
+            result.widthExceptAffixes++;
+        }
+    }
+}
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
deleted file mode 100644 (file)
index 417e1e0..0000000
+++ /dev/null
@@ -1,618 +0,0 @@
-// © 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.ThingsNeedingNewHome.PadPosition;
-import com.ibm.icu.text.DecimalFormatSymbols;
-
-import newapi.impl.AffixPatternProvider;
-
-/**
- * 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. One of {@link #IGNORE_ROUNDING_ALWAYS}, {@link
-   *     #IGNORE_ROUNDING_IF_CURRENCY}, or {@link #IGNORE_ROUNDING_NEVER}.
-   * @return A property bag object.
-   * @throws IllegalArgumentException If there is a syntax error in the pattern string.
-   */
-  public static Properties parseToProperties(String pattern, int ignoreRounding) {
-    Properties properties = new Properties();
-    parse(pattern, properties, ignoreRounding);
-    return properties;
-  }
-
-  public static Properties parseToProperties(String pattern) {
-    return parseToProperties(pattern, PatternString.IGNORE_ROUNDING_NEVER);
-  }
-
-  /**
-   * 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. One of {@link #IGNORE_ROUNDING_ALWAYS}, {@link
-   *     #IGNORE_ROUNDING_IF_CURRENCY}, or {@link #IGNORE_ROUNDING_NEVER}.
-   * @throws IllegalArgumentException If there was a syntax error in the pattern string.
-   */
-  public static void parseToExistingProperties(
-      String pattern, Properties properties, int ignoreRounding) {
-    parse(pattern, properties, ignoreRounding);
-  }
-
-  public static void parseToExistingProperties(String pattern, Properties properties) {
-    parseToExistingProperties(pattern, properties, PatternString.IGNORE_ROUNDING_NEVER);
-  }
-
-  /**
-   * 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, -1)
-        && firstGroupingSize != Math.min(dosMax, -1)
-        && groupingSize != firstGroupingSize) {
-      grouping = groupingSize;
-      grouping1 = groupingSize;
-      grouping2 = firstGroupingSize;
-    } else if (groupingSize != Math.min(dosMax, -1)) {
-      grouping = groupingSize;
-      grouping1 = 0;
-      grouping2 = groupingSize;
-    } else if (firstGroupingSize != Math.min(dosMax, -1)) {
-      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, -1)) {
-      // Significant Digits.
-      while (digitsString.length() < minSig) {
-        digitsString.append('@');
-      }
-      while (digitsString.length() < maxSig) {
-        digitsString.append('#');
-      }
-    } else if (roundingInterval != null) {
-      // 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, -1)) {
-      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 != -1) {
-      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 = ThingsNeedingNewHome.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.
-   *
-   * <p>A greedy string-substitution strategy is used to substitute locale symbols. If two symbols
-   * are ambiguous or have the same prefix, the result is not well-defined.
-   *
-   * <p>Locale symbols are not allowed to contain the ASCII quote character.
-   *
-   * @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(
-      String input, DecimalFormatSymbols symbols, boolean toLocalized) {
-    if (input == null) return null;
-
-    // Construct a table of strings to be converted between localized and standard.
-    String[][] table = new String[21][2];
-    int standIdx = toLocalized ? 0 : 1;
-    int localIdx = toLocalized ? 1 : 0;
-    table[0][standIdx] = "%";
-    table[0][localIdx] = symbols.getPercentString();
-    table[1][standIdx] = "‰";
-    table[1][localIdx] = symbols.getPerMillString();
-    table[2][standIdx] = ".";
-    table[2][localIdx] = symbols.getDecimalSeparatorString();
-    table[3][standIdx] = ",";
-    table[3][localIdx] = symbols.getGroupingSeparatorString();
-    table[4][standIdx] = "-";
-    table[4][localIdx] = symbols.getMinusSignString();
-    table[5][standIdx] = "+";
-    table[5][localIdx] = symbols.getPlusSignString();
-    table[6][standIdx] = ";";
-    table[6][localIdx] = Character.toString(symbols.getPatternSeparator());
-    table[7][standIdx] = "@";
-    table[7][localIdx] = Character.toString(symbols.getSignificantDigit());
-    table[8][standIdx] = "E";
-    table[8][localIdx] = symbols.getExponentSeparator();
-    table[9][standIdx] = "*";
-    table[9][localIdx] = Character.toString(symbols.getPadEscape());
-    table[10][standIdx] = "#";
-    table[10][localIdx] = Character.toString(symbols.getDigit());
-    for (int i = 0; i < 10; i++) {
-      table[11 + i][standIdx] = Character.toString((char) ('0' + i));
-      table[11 + i][localIdx] = symbols.getDigitStringsLocal()[i];
-    }
-
-    // Special case: quotes are NOT allowed to be in any localIdx strings.
-    // Substitute them with '’' instead.
-    for (int i = 0; i < table.length; i++) {
-      table[i][localIdx] = table[i][localIdx].replace('\'', '’');
-    }
-
-    // Iterate through the string and convert.
-    // State table:
-    //  0 => base state
-    //  1 => first char inside a quoted sequence in input and output string
-    //  2 => inside a quoted sequence in input and output string
-    //  3 => first char after a close quote in input string;
-    //       close quote still needs to be written to output string
-    //  4 => base state in input string; inside quoted sequence in output string
-    //  5 => first char inside a quoted sequence in input string;
-    //       inside quoted sequence in output string
-    StringBuilder result = new StringBuilder();
-    int state = 0;
-    outer:
-    for (int offset = 0; offset < input.length(); offset++) {
-      char ch = input.charAt(offset);
-
-      // Handle a quote character (state shift)
-      if (ch == '\'') {
-        if (state == 0) {
-          result.append('\'');
-          state = 1;
-          continue;
-        } else if (state == 1) {
-          result.append('\'');
-          state = 0;
-          continue;
-        } else if (state == 2) {
-          state = 3;
-          continue;
-        } else if (state == 3) {
-          result.append('\'');
-          result.append('\'');
-          state = 1;
-          continue;
-        } else if (state == 4) {
-          state = 5;
-          continue;
-        } else {
-          assert state == 5;
-          result.append('\'');
-          result.append('\'');
-          state = 4;
-          continue;
-        }
-      }
-
-      if (state == 0 || state == 3 || state == 4) {
-        for (String[] pair : table) {
-          // Perform a greedy match on this symbol string
-          if (input.regionMatches(offset, pair[0], 0, pair[0].length())) {
-            // Skip ahead past this region for the next iteration
-            offset += pair[0].length() - 1;
-            if (state == 3 || state == 4) {
-              result.append('\'');
-              state = 0;
-            }
-            result.append(pair[1]);
-            continue outer;
-          }
-        }
-        // No replacement found.  Check if a special quote is necessary
-        for (String[] pair : table) {
-          if (input.regionMatches(offset, pair[1], 0, pair[1].length())) {
-            if (state == 0) {
-              result.append('\'');
-              state = 4;
-            }
-            result.append(ch);
-            continue outer;
-          }
-        }
-        // Still nothing.  Copy the char verbatim.  (Add a close quote if necessary)
-        if (state == 3 || state == 4) {
-          result.append('\'');
-          state = 0;
-        }
-        result.append(ch);
-      } else {
-        assert state == 1 || state == 2 || state == 5;
-        result.append(ch);
-        state = 2;
-      }
-    }
-    // Resolve final quotes
-    if (state == 3 || state == 4) {
-      result.append('\'');
-      state = 0;
-    }
-    if (state != 0) {
-      throw new IllegalArgumentException("Malformed localized pattern: unterminated quote");
-    }
-    return result.toString();
-  }
-
-  public static final int IGNORE_ROUNDING_NEVER = 0;
-  public static final int IGNORE_ROUNDING_IF_CURRENCY = 1;
-  public static final int IGNORE_ROUNDING_ALWAYS = 2;
-
-  static void parse(String pattern, Properties properties, int 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.
-    LdmlPatternInfo.PatternParseResult result = LdmlPatternInfo.parse(pattern);
-    saveToProperties(properties, result, ignoreRounding);
-  }
-
-  /** Finalizes the temporary data stored in the PatternParseResult to the Builder. */
-  private static void saveToProperties(
-      Properties properties, LdmlPatternInfo.PatternParseResult ppr, int _ignoreRounding) {
-    // Translate from PatternParseResult to Properties.
-    // Note that most data from "negative" is ignored per the specification of DecimalFormat.
-
-    LdmlPatternInfo.SubpatternParseResult positive = ppr.positive;
-    LdmlPatternInfo.SubpatternParseResult negative = ppr.negative;
-    String pattern = ppr.pattern;
-
-    boolean ignoreRounding;
-    if (_ignoreRounding == IGNORE_ROUNDING_NEVER) {
-      ignoreRounding = false;
-    } else if (_ignoreRounding == IGNORE_ROUNDING_IF_CURRENCY) {
-      ignoreRounding = positive.hasCurrencySign;
-    } else {
-      assert _ignoreRounding == IGNORE_ROUNDING_ALWAYS;
-      ignoreRounding = true;
-    }
-
-    // Grouping settings
-    short grouping1 = (short) (positive.groupingSizes & 0xffff);
-    short grouping2 = (short) ((positive.groupingSizes >>> 16) & 0xffff);
-    short grouping3 = (short) ((positive.groupingSizes >>> 32) & 0xffff);
-    if (grouping2 != -1) {
-      properties.setGroupingSize(grouping1);
-    } else {
-      properties.setGroupingSize(-1);
-    }
-    if (grouping3 != -1) {
-      properties.setSecondaryGroupingSize(grouping2);
-    } else {
-      properties.setSecondaryGroupingSize(-1);
-    }
-
-    // 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(-1);
-      properties.setMaximumFractionDigits(-1);
-      properties.setRoundingIncrement(null);
-      properties.setMinimumSignificantDigits(positive.minimumSignificantDigits);
-      properties.setMaximumSignificantDigits(positive.maximumSignificantDigits);
-    } else if (positive.rounding != null) {
-      if (!ignoreRounding) {
-        properties.setMinimumFractionDigits(minFrac);
-        properties.setMaximumFractionDigits(positive.maximumFractionDigits);
-        properties.setRoundingIncrement(
-            positive.rounding.toBigDecimal().setScale(positive.minimumFractionDigits));
-      } else {
-        properties.setMinimumFractionDigits(-1);
-        properties.setMaximumFractionDigits(-1);
-        properties.setRoundingIncrement(null);
-      }
-      properties.setMinimumSignificantDigits(-1);
-      properties.setMaximumSignificantDigits(-1);
-    } else {
-      if (!ignoreRounding) {
-        properties.setMinimumFractionDigits(minFrac);
-        properties.setMaximumFractionDigits(positive.maximumFractionDigits);
-        properties.setRoundingIncrement(null);
-      } else {
-        properties.setMinimumFractionDigits(-1);
-        properties.setMaximumFractionDigits(-1);
-        properties.setRoundingIncrement(null);
-      }
-      properties.setMinimumSignificantDigits(-1);
-      properties.setMaximumSignificantDigits(-1);
-    }
-
-    // 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(-1);
-      }
-    } else {
-      properties.setExponentSignAlwaysShown(false);
-      properties.setMinimumExponentDigits(-1);
-      properties.setMinimumIntegerDigits(minInt);
-      properties.setMaximumIntegerDigits(-1);
-    }
-
-    // Compute the affix patterns (required for both padding and affixes)
-    String posPrefix = ppr.getString(AffixPatternProvider.Flags.PREFIX);
-    String posSuffix = ppr.getString(0);
-
-    // Padding settings
-    if (positive.paddingEndpoints != 0) {
-      // The width of the positive prefix and suffix templates are included in the padding
-      int paddingWidth =
-          positive.paddingWidth
-              + AffixPatternUtils.estimateLength(posPrefix)
-              + AffixPatternUtils.estimateLength(posSuffix);
-      properties.setFormatWidth(paddingWidth);
-      String rawPaddingString = ppr.getString(AffixPatternProvider.Flags.PADDING);
-      if (rawPaddingString.length() == 1) {
-        properties.setPadString(rawPaddingString);
-      } else if (rawPaddingString.length() == 2) {
-        if (rawPaddingString.charAt(0) == '\'') {
-          properties.setPadString("'");
-        } else {
-          properties.setPadString(rawPaddingString);
-        }
-      } else {
-        properties.setPadString(rawPaddingString.substring(1, rawPaddingString.length() - 1));
-      }
-      assert positive.paddingLocation != null;
-      properties.setPadPosition(positive.paddingLocation);
-    } else {
-      properties.setFormatWidth(-1);
-      properties.setPadString(null);
-      properties.setPadPosition(null);
-    }
-
-    // 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(posPrefix);
-    properties.setPositiveSuffixPattern(posSuffix);
-    if (negative != null) {
-      properties.setNegativePrefixPattern(
-          ppr.getString(
-              AffixPatternProvider.Flags.NEGATIVE_SUBPATTERN | AffixPatternProvider.Flags.PREFIX));
-      properties.setNegativeSuffixPattern(
-          ppr.getString(AffixPatternProvider.Flags.NEGATIVE_SUBPATTERN));
-    } 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(0);
-    }
-  }
-}
index d86c66945aa00073d5725d91b8ebbc01df8fc931..dc36c545d7ebe75ade96e05f5eb2f77d81033dba 100644 (file)
@@ -17,13 +17,14 @@ import java.util.Map;
 
 import com.ibm.icu.impl.number.Parse.GroupingMode;
 import com.ibm.icu.impl.number.Parse.ParseMode;
-import com.ibm.icu.impl.number.ThingsNeedingNewHome.PadPosition;
 import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
 import com.ibm.icu.text.CurrencyPluralInfo;
 import com.ibm.icu.text.PluralRules;
 import com.ibm.icu.util.Currency;
 import com.ibm.icu.util.Currency.CurrencyUsage;
 
+import newapi.impl.Padder.PadPosition;
+
 public class Properties implements Cloneable, Serializable {
 
   private static final Properties DEFAULT = new Properties();
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ThingsNeedingNewHome.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ThingsNeedingNewHome.java
deleted file mode 100644 (file)
index e322c87..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-// © 2017 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html#License
-package com.ibm.icu.impl.number;
-
-/** @author sffc */
-public class ThingsNeedingNewHome {
-  public static final String FALLBACK_PADDING_STRING = "\u0020"; // i.e. a space
-
-  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
-      }
-    }
-  }
-
-  /**
-   * 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(Properties properties) {
-    return ((properties.getCurrency() != null)
-        || properties.getCurrencyPluralInfo() != null
-        || properties.getCurrencyUsage() != null
-        || AffixPatternUtils.hasCurrencySymbols(properties.getPositivePrefixPattern())
-        || AffixPatternUtils.hasCurrencySymbols(properties.getPositiveSuffixPattern())
-        || AffixPatternUtils.hasCurrencySymbols(properties.getNegativePrefixPattern())
-        || AffixPatternUtils.hasCurrencySymbols(properties.getNegativeSuffixPattern()));
-  }
-}
index bc2f9ae3e669f8dfafbb76adfe079bc08ae6a368..876fa72b40482dc60f6b6580014c65f98f0b65d8 100644 (file)
@@ -3,15 +3,20 @@
 package com.ibm.icu.impl.number.modifiers;
 
 import com.ibm.icu.impl.number.Modifier;
-import com.ibm.icu.impl.number.Modifier.AffixModifier;
+
+// TODO: This class is currently unused, but it might be useful for something in the future.
+// Should probably be moved to a different package.
+
 import com.ibm.icu.impl.number.NumberStringBuilder;
 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 {
+/**
+ * The canonical implementation of {@link Modifier}, containing a prefix and suffix string.
+ */
+public class ConstantAffixModifier implements Modifier {
 
     // TODO: Avoid making a new instance by default if prefix and suffix are empty
-    public static final AffixModifier EMPTY = new ConstantAffixModifier();
+    public static final ConstantAffixModifier EMPTY = new ConstantAffixModifier();
 
     private final String prefix;
     private final String suffix;
@@ -69,26 +74,6 @@ public class ConstantAffixModifier extends Modifier.BaseModifier implements Affi
         return strong;
     }
 
-    public boolean contentEquals(CharSequence _prefix, CharSequence _suffix) {
-        if (_prefix == null && !prefix.isEmpty())
-            return false;
-        if (_suffix == null && !suffix.isEmpty())
-            return false;
-        if (_prefix != null && prefix.length() != _prefix.length())
-            return false;
-        if (_suffix != null && 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 prefix:'%s' suffix:'%s'>", prefix, suffix);
index b2e0164c2b5d25b686c263253ac490936f038e69..26f57d7870f8bef5f685716cef688a21c3be6af1 100644 (file)
@@ -3,7 +3,6 @@
 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.text.NumberFormat.Field;
 
@@ -11,11 +10,10 @@ 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();
+public class ConstantMultiFieldModifier implements Modifier {
 
+    // NOTE: In Java, these are stored as array pointers. In C++, the NumberStringBuilder is stored by
+    // value and is treated internally as immutable.
     protected final char[] prefixChars;
     protected final char[] suffixChars;
     protected final Field[] prefixFields;
@@ -30,14 +28,6 @@ public class ConstantMultiFieldModifier extends Modifier.BaseModifier implements
         this.strong = strong;
     }
 
-    private ConstantMultiFieldModifier() {
-        prefixChars = new char[0];
-        suffixChars = new char[0];
-        prefixFields = new Field[0];
-        suffixFields = new Field[0];
-        strong = false;
-    }
-
     @Override
     public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
         // Insert the suffix first since inserting the prefix will change the rightIndex
@@ -56,10 +46,6 @@ public class ConstantMultiFieldModifier extends Modifier.BaseModifier implements
         return strong;
     }
 
-    public boolean contentEquals(NumberStringBuilder prefix, NumberStringBuilder suffix) {
-        return prefix.contentEquals(prefixChars, prefixFields) && suffix.contentEquals(suffixChars, suffixFields);
-    }
-
     @Override
     public String toString() {
         NumberStringBuilder temp = new NumberStringBuilder();
index 66b7c2d6204aa8f44e80696fba513f4ab39d9bf8..d9318849a623750550058bdd24bcc44f2b252ac6 100644 (file)
@@ -10,163 +10,144 @@ import com.ibm.icu.text.UnicodeSet;
 /** Identical to {@link ConstantMultiFieldModifier}, but supports currency spacing. */
 public class CurrencySpacingEnabledModifier extends ConstantMultiFieldModifier {
 
-  // These are the default currency spacing UnicodeSets in CLDR.
-  // Pre-compute them for performance.
-  // TODO: Is there a way to write a unit test to make sure these hard-coded values
-  // stay consistent with CLDR?
-  private static final UnicodeSet UNISET_DIGIT = new UnicodeSet("[:digit:]").freeze();
-  private static final UnicodeSet UNISET_NOTS = new UnicodeSet("[:^S:]").freeze();
+    // These are the default currency spacing UnicodeSets in CLDR.
+    // Pre-compute them for performance.
+    // The unit test testCurrencySpacingPatternStability() will start failing if these change in CLDR.
+    private static final UnicodeSet UNISET_DIGIT = new UnicodeSet("[:digit:]").freeze();
+    private static final UnicodeSet UNISET_NOTS = new UnicodeSet("[:^S:]").freeze();
 
-  // Constants for better readability.  Types are for compiler checking.
-  static final byte PREFIX = 0;
-  static final byte SUFFIX = 1;
-  static final short IN_CURRENCY = 0;
-  static final short IN_NUMBER = 1;
+    // Constants for better readability. Types are for compiler checking.
+    static final byte PREFIX = 0;
+    static final byte SUFFIX = 1;
+    static final short IN_CURRENCY = 0;
+    static final short IN_NUMBER = 1;
 
-  private final UnicodeSet afterPrefixUnicodeSet;
-  private final String afterPrefixInsert;
-  private final UnicodeSet beforeSuffixUnicodeSet;
-  private final String beforeSuffixInsert;
+    private final UnicodeSet afterPrefixUnicodeSet;
+    private final String afterPrefixInsert;
+    private final UnicodeSet beforeSuffixUnicodeSet;
+    private final String beforeSuffixInsert;
 
-  /** Build code path */
-  public CurrencySpacingEnabledModifier(
-      NumberStringBuilder prefix,
-      NumberStringBuilder suffix,
-      boolean strong,
-      DecimalFormatSymbols symbols) {
-    super(prefix, suffix, strong);
+    /** Safe code path */
+    public CurrencySpacingEnabledModifier(NumberStringBuilder prefix, NumberStringBuilder suffix, boolean strong,
+            DecimalFormatSymbols symbols) {
+        super(prefix, suffix, strong);
 
-    // Check for currency spacing. Do not build the UnicodeSets unless there is
-    // a currency code point at a boundary.
-    if (prefixFields.length > 0
-        && prefixFields[prefixFields.length - 1] == NumberFormat.Field.CURRENCY) {
-      int prefixCp = Character.codePointBefore(prefixChars, prefixChars.length);
-      UnicodeSet prefixUnicodeSet = getUnicodeSet(symbols, IN_CURRENCY, PREFIX);
-      if (prefixUnicodeSet.contains(prefixCp)) {
-        afterPrefixUnicodeSet = getUnicodeSet(symbols, IN_NUMBER, PREFIX);
-        afterPrefixInsert = getInsertString(symbols, PREFIX);
-      } else {
-        afterPrefixUnicodeSet = null;
-        afterPrefixInsert = null;
-      }
-    } else {
-      afterPrefixUnicodeSet = null;
-      afterPrefixInsert = null;
+        // Check for currency spacing. Do not build the UnicodeSets unless there is
+        // a currency code point at a boundary.
+        if (prefix.length() > 0 && prefix.fieldAt(prefix.length() - 1) == NumberFormat.Field.CURRENCY) {
+            int prefixCp = prefix.getLastCodePoint();
+            UnicodeSet prefixUnicodeSet = getUnicodeSet(symbols, IN_CURRENCY, PREFIX);
+            if (prefixUnicodeSet.contains(prefixCp)) {
+                afterPrefixUnicodeSet = getUnicodeSet(symbols, IN_NUMBER, PREFIX);
+                afterPrefixUnicodeSet.freeze(); // no-op if set is already frozen
+                afterPrefixInsert = getInsertString(symbols, PREFIX);
+            } else {
+                afterPrefixUnicodeSet = null;
+                afterPrefixInsert = null;
+            }
+        } else {
+            afterPrefixUnicodeSet = null;
+            afterPrefixInsert = null;
+        }
+        if (suffix.length() > 0 && suffix.fieldAt(0) == NumberFormat.Field.CURRENCY) {
+            int suffixCp = suffix.getLastCodePoint();
+            UnicodeSet suffixUnicodeSet = getUnicodeSet(symbols, IN_CURRENCY, SUFFIX);
+            if (suffixUnicodeSet.contains(suffixCp)) {
+                beforeSuffixUnicodeSet = getUnicodeSet(symbols, IN_NUMBER, SUFFIX);
+                beforeSuffixUnicodeSet.freeze(); // no-op if set is already frozen
+                beforeSuffixInsert = getInsertString(symbols, SUFFIX);
+            } else {
+                beforeSuffixUnicodeSet = null;
+                beforeSuffixInsert = null;
+            }
+        } else {
+            beforeSuffixUnicodeSet = null;
+            beforeSuffixInsert = null;
+        }
     }
-    if (suffixFields.length > 0 && suffixFields[0] == NumberFormat.Field.CURRENCY) {
-      int suffixCp = Character.codePointAt(suffixChars, 0);
-      UnicodeSet suffixUnicodeSet = getUnicodeSet(symbols, IN_CURRENCY, SUFFIX);
-      if (suffixUnicodeSet.contains(suffixCp)) {
-        beforeSuffixUnicodeSet = getUnicodeSet(symbols, IN_NUMBER, SUFFIX);
-        beforeSuffixInsert = getInsertString(symbols, SUFFIX);
-      } else {
-        beforeSuffixUnicodeSet = null;
-        beforeSuffixInsert = null;
-      }
-    } else {
-      beforeSuffixUnicodeSet = null;
-      beforeSuffixInsert = null;
-    }
-  }
 
-  /** Non-build code path */
-  public static int applyCurrencySpacing(
-      NumberStringBuilder output,
-      int prefixStart,
-      int prefixLen,
-      int suffixStart,
-      int suffixLen,
-      DecimalFormatSymbols symbols) {
-    int length = 0;
-    boolean hasPrefix = (prefixLen > 0);
-    boolean hasSuffix = (suffixLen > 0);
-    boolean hasNumber = (suffixStart - prefixStart - prefixLen > 0); // could be empty string
-    if (hasPrefix && hasNumber) {
-      length += applyCurrencySpacingAffix(output, prefixStart + prefixLen, PREFIX, symbols);
-    }
-    if (hasSuffix && hasNumber) {
-      length += applyCurrencySpacingAffix(output, suffixStart + length, SUFFIX, symbols);
-    }
-    return length;
-  }
+    /** Safe code path */
+    @Override
+    public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
+        // Currency spacing logic
+        int length = 0;
+        if (rightIndex - leftIndex > 0 && afterPrefixUnicodeSet != null
+                && afterPrefixUnicodeSet.contains(output.codePointAt(leftIndex))) {
+            // TODO: Should we use the CURRENCY field here?
+            length += output.insert(leftIndex, afterPrefixInsert, null);
+        }
+        if (rightIndex - leftIndex > 0 && beforeSuffixUnicodeSet != null
+                && beforeSuffixUnicodeSet.contains(output.codePointBefore(rightIndex))) {
+            // TODO: Should we use the CURRENCY field here?
+            length += output.insert(rightIndex + length, beforeSuffixInsert, null);
+        }
 
-  private static int applyCurrencySpacingAffix(
-      NumberStringBuilder output, int index, byte affix, DecimalFormatSymbols symbols) {
-    // NOTE: For prefix, output.fieldAt(index-1) gets the last field type in the prefix.
-    // This works even if the last code point in the prefix is 2 code units because the
-    // field value gets populated to both indices in the field array.
-    NumberFormat.Field affixField =
-        (affix == PREFIX) ? output.fieldAt(index - 1) : output.fieldAt(index);
-    if (affixField != NumberFormat.Field.CURRENCY) {
-      return 0;
-    }
-    int affixCp =
-        (affix == PREFIX)
-            ? Character.codePointBefore(output, index)
-            : Character.codePointAt(output, index);
-    UnicodeSet affixUniset = getUnicodeSet(symbols, IN_CURRENCY, affix);
-    if (!affixUniset.contains(affixCp)) {
-      return 0;
+        // Call super for the remaining logic
+        length += super.apply(output, leftIndex, rightIndex + length);
+        return length;
     }
-    int numberCp =
-        (affix == PREFIX)
-            ? Character.codePointAt(output, index)
-            : Character.codePointBefore(output, index);
-    UnicodeSet numberUniset = getUnicodeSet(symbols, IN_NUMBER, affix);
-    if (!numberUniset.contains(numberCp)) {
-      return 0;
-    }
-    String spacingString = getInsertString(symbols, affix);
-
-    // NOTE: This next line *inserts* the spacing string, triggering an arraycopy.
-    // It would be more efficient if this could be done before affixes were attached,
-    // so that it could be prepended/appended instead of inserted.
-    // However, the build code path is more efficient, and this is the most natural
-    // place to put currency spacing in the non-build code path.
-    // TODO: Should we use the CURRENCY field here?
-    return output.insert(index, spacingString, null);
-  }
 
-  private static UnicodeSet getUnicodeSet(DecimalFormatSymbols symbols, short position, byte affix) {
-    String pattern =
-        symbols.getPatternForCurrencySpacing(
-            position == IN_CURRENCY
-                ? DecimalFormatSymbols.CURRENCY_SPC_CURRENCY_MATCH
-                : DecimalFormatSymbols.CURRENCY_SPC_SURROUNDING_MATCH,
-            affix == SUFFIX);
-    if (pattern.equals("[:digit:]")) {
-      return UNISET_DIGIT;
-    } else if (pattern.equals("[:^S:]")) {
-      return UNISET_NOTS;
-    } else {
-      return new UnicodeSet(pattern);
+    /** Unsafe code path */
+    public static int applyCurrencySpacing(NumberStringBuilder output, int prefixStart, int prefixLen, int suffixStart,
+            int suffixLen, DecimalFormatSymbols symbols) {
+        int length = 0;
+        boolean hasPrefix = (prefixLen > 0);
+        boolean hasSuffix = (suffixLen > 0);
+        boolean hasNumber = (suffixStart - prefixStart - prefixLen > 0); // could be empty string
+        if (hasPrefix && hasNumber) {
+            length += applyCurrencySpacingAffix(output, prefixStart + prefixLen, PREFIX, symbols);
+        }
+        if (hasSuffix && hasNumber) {
+            length += applyCurrencySpacingAffix(output, suffixStart + length, SUFFIX, symbols);
+        }
+        return length;
     }
-  }
 
-  private static String getInsertString(DecimalFormatSymbols symbols, byte affix) {
-    return symbols.getPatternForCurrencySpacing(
-        DecimalFormatSymbols.CURRENCY_SPC_INSERT, affix == SUFFIX);
-  }
+    /** Unsafe code path */
+    private static int applyCurrencySpacingAffix(NumberStringBuilder output, int index, byte affix,
+            DecimalFormatSymbols symbols) {
+        // NOTE: For prefix, output.fieldAt(index-1) gets the last field type in the prefix.
+        // This works even if the last code point in the prefix is 2 code units because the
+        // field value gets populated to both indices in the field array.
+        NumberFormat.Field affixField = (affix == PREFIX) ? output.fieldAt(index - 1) : output.fieldAt(index);
+        if (affixField != NumberFormat.Field.CURRENCY) {
+            return 0;
+        }
+        int affixCp = (affix == PREFIX) ? output.codePointBefore(index) : output.codePointAt(index);
+        UnicodeSet affixUniset = getUnicodeSet(symbols, IN_CURRENCY, affix);
+        if (!affixUniset.contains(affixCp)) {
+            return 0;
+        }
+        int numberCp = (affix == PREFIX) ? output.codePointAt(index) : output.codePointBefore(index);
+        UnicodeSet numberUniset = getUnicodeSet(symbols, IN_NUMBER, affix);
+        if (!numberUniset.contains(numberCp)) {
+            return 0;
+        }
+        String spacingString = getInsertString(symbols, affix);
 
-  @Override
-  public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
-    // Currency spacing logic
-    int length = 0;
-    if (rightIndex - leftIndex > 0
-        && afterPrefixUnicodeSet != null
-        && afterPrefixUnicodeSet.contains(Character.codePointAt(output, leftIndex))) {
-      // TODO: Should we use the CURRENCY field here?
-      length += output.insert(leftIndex, afterPrefixInsert, null);
+        // NOTE: This next line *inserts* the spacing string, triggering an arraycopy.
+        // It would be more efficient if this could be done before affixes were attached,
+        // so that it could be prepended/appended instead of inserted.
+        // However, the build code path is more efficient, and this is the most natural
+        // place to put currency spacing in the non-build code path.
+        // TODO: Should we use the CURRENCY field here?
+        return output.insert(index, spacingString, null);
     }
-    if (rightIndex - leftIndex > 0
-        && beforeSuffixUnicodeSet != null
-        && beforeSuffixUnicodeSet.contains(Character.codePointBefore(output, rightIndex))) {
-      // TODO: Should we use the CURRENCY field here?
-      length += output.insert(rightIndex + length, beforeSuffixInsert, null);
+
+    private static UnicodeSet getUnicodeSet(DecimalFormatSymbols symbols, short position, byte affix) {
+        String pattern = symbols
+                .getPatternForCurrencySpacing(position == IN_CURRENCY ? DecimalFormatSymbols.CURRENCY_SPC_CURRENCY_MATCH
+                        : DecimalFormatSymbols.CURRENCY_SPC_SURROUNDING_MATCH, affix == SUFFIX);
+        if (pattern.equals("[:digit:]")) {
+            return UNISET_DIGIT;
+        } else if (pattern.equals("[:^S:]")) {
+            return UNISET_NOTS;
+        } else {
+            return new UnicodeSet(pattern);
+        }
     }
 
-    // Call super for the remaining logic
-    length += super.apply(output, leftIndex, rightIndex + length);
-    return length;
-  }
+    private static String getInsertString(DecimalFormatSymbols symbols, byte affix) {
+        return symbols.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_INSERT, affix == SUFFIX);
+    }
 }
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
deleted file mode 100644 (file)
index 15ba186..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-// © 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.Modifier;
-
-// 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.
-
-// TODO: This class is currently unused.  Probably should be deleted.
-
-/**
- * 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 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;
-  }
-}
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
deleted file mode 100644 (file)
index 6ccd243..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-// © 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;
-
-// TODO: This class is currently unused.  Should probably be deleted.
-
-/** A class containing a positive form and a negative form of {@link ConstantAffixModifier}. */
-public class PositiveNegativeAffixModifier 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;
-  }
-}
index b88b112ad446cf45dd48db87311f53313a513e30..f49360b9195f473b61b5cff44a5d8fc25d7e85a5 100644 (file)
@@ -11,15 +11,15 @@ 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 {
+public class SimpleModifier implements Modifier {
     private final String compiledPattern;
     private final Field field;
     private final boolean strong;
-
     private final int prefixLength;
     private final int suffixOffset;
     private final int suffixLength;
 
+    /** TODO: This is copied from SimpleFormatterImpl. */
     private static final int ARG_NUM_LIMIT = 0x100;
 
     /** Creates a modifier that uses the SimpleFormatter string formats. */
@@ -79,7 +79,6 @@ public class SimpleModifier extends Modifier.BaseModifier {
      * @return The number of characters (UTF-16 code points) that were added to the StringBuilder.
      */
     public int formatAsPrefixSuffix(NumberStringBuilder result, int startIndex, int endIndex, Field field) {
-        assert SimpleFormatterImpl.getArgumentLimit(compiledPattern) == 1;
         if (prefixLength > 0) {
             result.insert(startIndex, compiledPattern, 2, 2 + prefixLength, field);
         }
@@ -89,28 +88,4 @@ public class SimpleModifier extends Modifier.BaseModifier {
         }
         return prefixLength + suffixLength;
     }
-
-    /** 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);
-                new SimpleModifier(compiledPattern, null, false).apply(output, (Integer) outputs[j][1],
-                        (Integer) outputs[j][2]);
-                String expected = expecteds[j][i];
-                String actual = output.toString();
-                assert expected.equals(actual);
-            }
-        }
-    }
 }
index d56f5d7e2f6eb707a5561ddfc1d99a001c009cb3..d84fdbe0f75de338a013f13bd38266dd0af374b8 100644 (file)
@@ -13,11 +13,10 @@ import java.text.FieldPosition;
 import java.text.ParseException;
 import java.text.ParsePosition;
 
+import com.ibm.icu.impl.number.AffixPatternUtils;
 import com.ibm.icu.impl.number.Parse;
-import com.ibm.icu.impl.number.PatternString;
+import com.ibm.icu.impl.number.PatternAndPropertyUtils;
 import com.ibm.icu.impl.number.Properties;
-import com.ibm.icu.impl.number.ThingsNeedingNewHome;
-import com.ibm.icu.impl.number.ThingsNeedingNewHome.PadPosition;
 import com.ibm.icu.lang.UCharacter;
 import com.ibm.icu.math.BigDecimal;
 import com.ibm.icu.math.MathContext;
@@ -33,6 +32,7 @@ import newapi.LocalizedNumberFormatter;
 import newapi.NumberFormatter;
 import newapi.NumberPropertyMapper;
 import newapi.impl.MacroProps;
+import newapi.impl.Padder.PadPosition;
 
 /**
  * {@icuenhanced java.text.DecimalFormat}.{@icu _usage_} <code>DecimalFormat</code> is the primary
@@ -301,7 +301,7 @@ public class DecimalFormat extends NumberFormat {
     properties = new Properties();
     exportedProperties = new Properties();
     // Regression: ignore pattern rounding information if the pattern has currency symbols.
-    setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_IF_CURRENCY);
+    setPropertiesFromPattern(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_IF_CURRENCY);
     refreshFormatter();
   }
 
@@ -330,7 +330,7 @@ public class DecimalFormat extends NumberFormat {
     properties = new Properties();
     exportedProperties = new Properties();
     // Regression: ignore pattern rounding information if the pattern has currency symbols.
-    setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_IF_CURRENCY);
+    setPropertiesFromPattern(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_IF_CURRENCY);
     refreshFormatter();
   }
 
@@ -359,7 +359,7 @@ public class DecimalFormat extends NumberFormat {
     properties = new Properties();
     exportedProperties = new Properties();
     // Regression: ignore pattern rounding information if the pattern has currency symbols.
-    setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_IF_CURRENCY);
+    setPropertiesFromPattern(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_IF_CURRENCY);
     refreshFormatter();
   }
 
@@ -402,9 +402,9 @@ public class DecimalFormat extends NumberFormat {
         || choice == CASHCURRENCYSTYLE
         || choice == STANDARDCURRENCYSTYLE
         || choice == PLURALCURRENCYSTYLE) {
-      setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_ALWAYS);
+      setPropertiesFromPattern(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_ALWAYS);
     } else {
-      setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_IF_CURRENCY);
+      setPropertiesFromPattern(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_IF_CURRENCY);
     }
     refreshFormatter();
   }
@@ -445,7 +445,7 @@ public class DecimalFormat extends NumberFormat {
    * @stable ICU 2.0
    */
   public synchronized void applyPattern(String pattern) {
-    setPropertiesFromPattern(pattern, PatternString.IGNORE_ROUNDING_NEVER);
+    setPropertiesFromPattern(pattern, PatternAndPropertyUtils.IGNORE_ROUNDING_NEVER);
     // Backwards compatibility: clear out user-specified prefix and suffix,
     // as well as CurrencyPluralInfo.
     properties.setPositivePrefix(null);
@@ -469,7 +469,7 @@ public class DecimalFormat extends NumberFormat {
    * @stable ICU 2.0
    */
   public synchronized void applyLocalizedPattern(String localizedPattern) {
-    String pattern = PatternString.convertLocalized(localizedPattern, symbols, false);
+    String pattern = PatternAndPropertyUtils.convertLocalized(localizedPattern, symbols, false);
     applyPattern(pattern);
   }
 
@@ -752,7 +752,7 @@ public class DecimalFormat extends NumberFormat {
     if (!(obj instanceof Number)) throw new IllegalArgumentException();
     Number number = (Number) obj;
     FormattedNumber output = formatter.format(number);
-    return output.toAttributedCharacterIterator();
+    return output.getAttributes();
   }
 
   /**
@@ -2378,12 +2378,12 @@ public class DecimalFormat extends NumberFormat {
     // so that CurrencyUsage is reflected properly.
     // TODO: Consider putting this logic in PatternString.java instead.
     Properties tprops = threadLocalProperties.get().copyFrom(properties);
-    if (ThingsNeedingNewHome.useCurrency(properties)) {
+    if (useCurrency(properties)) {
       tprops.setMinimumFractionDigits(exportedProperties.getMinimumFractionDigits());
       tprops.setMaximumFractionDigits(exportedProperties.getMaximumFractionDigits());
       tprops.setRoundingIncrement(exportedProperties.getRoundingIncrement());
     }
-    return PatternString.propertiesToString(tprops);
+    return PatternAndPropertyUtils.propertiesToString(tprops);
   }
 
   /**
@@ -2396,7 +2396,21 @@ public class DecimalFormat extends NumberFormat {
    */
   public synchronized String toLocalizedPattern() {
     String pattern = toPattern();
-    return PatternString.convertLocalized(pattern, symbols, true);
+    return PatternAndPropertyUtils.convertLocalized(pattern, symbols, true);
+  }
+
+  /**
+   * Converts this DecimalFormat to a NumberFormatter.  Starting in ICU 60,
+   * NumberFormatter is the recommended way to format numbers.
+   *
+   * @return An instance of {@link LocalizedNumberFormatter} with the same behavior as this instance of
+   * DecimalFormat.
+   * @see NumberFormatter
+   * @provisional This API might change or be removed in a future release.
+   * @draft ICU 60
+   */
+  public LocalizedNumberFormatter toNumberFormatter() {
+      return formatter;
   }
 
   /**
@@ -2460,6 +2474,20 @@ public class DecimalFormat extends NumberFormat {
     }
   }
 
+  /**
+   * Returns true if the currency is set in The property bag or if currency symbols are present in
+   * the prefix/suffix pattern.
+   */
+  private static boolean useCurrency(Properties properties) {
+    return ((properties.getCurrency() != null)
+        || properties.getCurrencyPluralInfo() != null
+        || properties.getCurrencyUsage() != null
+        || AffixPatternUtils.hasCurrencySymbols(properties.getPositivePrefixPattern())
+        || AffixPatternUtils.hasCurrencySymbols(properties.getPositiveSuffixPattern())
+        || AffixPatternUtils.hasCurrencySymbols(properties.getNegativePrefixPattern())
+        || AffixPatternUtils.hasCurrencySymbols(properties.getNegativeSuffixPattern()));
+  }
+
   /**
    * Updates the property bag with settings from the given pattern.
    *
@@ -2467,15 +2495,15 @@ public class DecimalFormat extends NumberFormat {
    * @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. One of {@link
-   *     PatternString#IGNORE_ROUNDING_ALWAYS}, {@link PatternString#IGNORE_ROUNDING_IF_CURRENCY},
-   *     or {@link PatternString#IGNORE_ROUNDING_NEVER}.
-   * @see PatternString#parseToExistingProperties
+   *     PatternAndPropertyUtils#IGNORE_ROUNDING_ALWAYS}, {@link PatternAndPropertyUtils#IGNORE_ROUNDING_IF_CURRENCY},
+   *     or {@link PatternAndPropertyUtils#IGNORE_ROUNDING_NEVER}.
+   * @see PatternAndPropertyUtils#parseToExistingProperties
    */
   void setPropertiesFromPattern(String pattern, int ignoreRounding) {
     if (pattern == null) {
       throw new NullPointerException();
     }
-    PatternString.parseToExistingProperties(pattern, properties, ignoreRounding);
+    PatternAndPropertyUtils.parseToExistingProperties(pattern, properties, ignoreRounding);
   }
 
   /**
index 430b13f05fd667a2b3d7b8d7909d3bd564c8b78d..d28454db6ee8f5103428641ed04d098a16c09603 100644 (file)
@@ -1134,8 +1134,6 @@ public class DecimalFormatSymbols implements Cloneable, Serializable {
      * <p>For more information, see <a href="http://www.unicode.org/reports/tr35/#Currencies"
      * >UTS#35 section 5.10.2</a>.
      *
-     * <p><strong>Note:</strong> ICU4J does not currently use this information.
-     *
      * @param itemType one of CURRENCY_SPC_CURRENCY_MATCH, CURRENCY_SPC_SURROUNDING_MATCH
      * or CURRENCY_SPC_INSERT
      * @param beforeCurrency true to get the <code>beforeCurrency</code> values, false
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/Dimensionless.java b/icu4j/main/classes/core/src/com/ibm/icu/util/Dimensionless.java
deleted file mode 100644 (file)
index 96cb431..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-// © 2017 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html#License
-package com.ibm.icu.util;
-
-public class Dimensionless extends MeasureUnit {
-
-    public static final Dimensionless BASE =
-        (Dimensionless) MeasureUnit.internalGetInstance("dimensionless", "base");
-
-    public static final Dimensionless PERCENT =
-        (Dimensionless) MeasureUnit.internalGetInstance("dimensionless", "percent");
-
-    public static final Dimensionless PERMILLE =
-        (Dimensionless) MeasureUnit.internalGetInstance("dimensionless", "permille");
-
-    protected Dimensionless(String subType) {
-        super("dimensionless", subType);
-    }
-}
index ce5021581296504d213031ea04c6d0aed814332f..4cd71a258ac9f25bfc654ec22311cab4d4df95b2 100644 (file)
@@ -195,8 +195,8 @@ public class MeasureUnit implements Serializable {
             factory = CURRENCY_FACTORY;
         } else if ("duration".equals(type)) {
             factory = TIMEUNIT_FACTORY;
-        } else if ("dimensionless".equals(type)) {
-            factory = DIMENSIONLESS_FACTORY;
+        } else if ("none".equals(type)) {
+            factory = NOUNIT_FACTORY;
         } else {
             factory = UNIT_FACTORY;
         }
@@ -251,10 +251,10 @@ public class MeasureUnit implements Serializable {
         }
     };
 
-    static Factory DIMENSIONLESS_FACTORY = new Factory() {
+    static Factory NOUNIT_FACTORY = new Factory() {
         @Override
         public MeasureUnit create(String type, String subType) {
-           return new Dimensionless(subType);
+           return new NoUnit(subType);
         }
     };
 
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/NoUnit.java b/icu4j/main/classes/core/src/com/ibm/icu/util/NoUnit.java
new file mode 100644 (file)
index 0000000..41d4ae5
--- /dev/null
@@ -0,0 +1,19 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.util;
+
+public class NoUnit extends MeasureUnit {
+
+    public static final NoUnit BASE =
+        (NoUnit) MeasureUnit.internalGetInstance("none", "base");
+
+    public static final NoUnit PERCENT =
+        (NoUnit) MeasureUnit.internalGetInstance("none", "percent");
+
+    public static final NoUnit PERMILLE =
+        (NoUnit) MeasureUnit.internalGetInstance("none", "permille");
+
+    protected NoUnit(String subType) {
+        super("none", subType);
+    }
+}
index c45bb3778c9e12c08422861e11ba152fcc89c9c2..e0830ec91da1da06a45f04f5fcf3880f93aba824 100644 (file)
@@ -8,17 +8,17 @@ import java.util.Set;
 
 import com.ibm.icu.impl.StandardPlural;
 import com.ibm.icu.impl.number.FormatQuantity;
-import com.ibm.icu.impl.number.LdmlPatternInfo;
-import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult;
+import com.ibm.icu.impl.number.PatternParser;
+import com.ibm.icu.impl.number.PatternParser.ParsedPatternInfo;
 import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
 import com.ibm.icu.text.CompactDecimalFormat.CompactType;
 import com.ibm.icu.text.PluralRules;
 import com.ibm.icu.util.ULocale;
 
-import newapi.MurkyModifier.ImmutableMurkyModifier;
+import newapi.MutablePatternModifier.ImmutableMurkyModifier;
 import newapi.impl.CompactData;
 import newapi.impl.MicroProps;
-import newapi.impl.QuantityChain;
+import newapi.impl.MicroPropsGenerator;
 
 public class CompactNotation extends Notation {
 
@@ -35,8 +35,8 @@ public class CompactNotation extends Notation {
         this.compactCustomData = compactCustomData;
     }
 
-    /* package-private */ QuantityChain withLocaleData(ULocale dataLocale, CompactType compactType, PluralRules rules,
-            MurkyModifier buildReference, QuantityChain parent) {
+    /* package-private */ MicroPropsGenerator withLocaleData(ULocale dataLocale, CompactType compactType, PluralRules rules,
+            MutablePatternModifier buildReference, MicroPropsGenerator parent) {
         CompactData data;
         if (compactStyle != null) {
             data = CompactData.getInstance(dataLocale, compactType, compactStyle);
@@ -46,7 +46,7 @@ public class CompactNotation extends Notation {
         return new CompactImpl(data, rules, buildReference, parent);
     }
 
-    private static class CompactImpl implements QuantityChain {
+    private static class CompactImpl implements MicroPropsGenerator {
 
         private static class CompactModInfo {
             public ImmutableMurkyModifier mod;
@@ -56,9 +56,9 @@ public class CompactNotation extends Notation {
         final PluralRules rules;
         final CompactData data;
         final Map<String, CompactModInfo> precomputedMods;
-        final QuantityChain parent;
+        final MicroPropsGenerator parent;
 
-        private CompactImpl(CompactData data, PluralRules rules, MurkyModifier buildReference, QuantityChain parent) {
+        private CompactImpl(CompactData data, PluralRules rules, MutablePatternModifier buildReference, MicroPropsGenerator parent) {
             this.data = data;
             this.rules = rules;
             if (buildReference != null) {
@@ -73,23 +73,23 @@ public class CompactNotation extends Notation {
 
         /** Used by the safe code path */
         private static Map<String, CompactModInfo> precomputeAllModifiers(CompactData data,
-                MurkyModifier buildReference) {
+                MutablePatternModifier buildReference) {
             Map<String, CompactModInfo> precomputedMods = new HashMap<String, CompactModInfo>();
             Set<String> allPatterns = data.getAllPatterns();
             for (String patternString : allPatterns) {
                 CompactModInfo info = new CompactModInfo();
-                PatternParseResult patternInfo = LdmlPatternInfo.parse(patternString);
+                ParsedPatternInfo patternInfo = PatternParser.parse(patternString);
                 buildReference.setPatternInfo(patternInfo);
                 info.mod = buildReference.createImmutable();
-                info.numDigits = patternInfo.positive.totalIntegerDigits;
+                info.numDigits = patternInfo.positive.integerTotal;
                 precomputedMods.put(patternString, info);
             }
             return precomputedMods;
         }
 
         @Override
-        public MicroProps withQuantity(FormatQuantity input) {
-            MicroProps micros = parent.withQuantity(input);
+        public MicroProps processQuantity(FormatQuantity input) {
+            MicroProps micros = parent.processQuantity(input);
             assert micros.rounding != null;
 
             // Treat zero as if it had magnitude 0
@@ -111,17 +111,17 @@ public class CompactNotation extends Notation {
                 // Use the default (non-compact) modifier.
                 // No need to take any action.
             } else if (precomputedMods != null) {
-                // Build code path.
+                // Safe code path.
                 CompactModInfo info = precomputedMods.get(patternString);
                 info.mod.applyToMicros(micros, input);
                 numDigits = info.numDigits;
             } else {
-                // Non-build code path.
+                // Unsafe code path.
                 // Overwrite the PatternInfo in the existing modMiddle
-                assert micros.modMiddle instanceof MurkyModifier;
-                PatternParseResult patternInfo = LdmlPatternInfo.parse(patternString);
-                ((MurkyModifier) micros.modMiddle).setPatternInfo(patternInfo);
-                numDigits = patternInfo.positive.totalIntegerDigits;
+                assert micros.modMiddle instanceof MutablePatternModifier;
+                ParsedPatternInfo patternInfo = PatternParser.parse(patternString);
+                ((MutablePatternModifier) micros.modMiddle).setPatternInfo(patternInfo);
+                numDigits = patternInfo.positive.integerTotal;
             }
 
             // FIXME: Deal with numDigits == 0 (Awaiting a test case)
index 33f432361d6d498bc132e504d7d0c7ba50908a87..310086c0490e760adc62c3d722ff7c864c6230a7 100644 (file)
@@ -55,7 +55,7 @@ public class FormattedNumber {
         fq.populateUFieldPosition(fieldPosition);
     }
 
-    public AttributedCharacterIterator toAttributedCharacterIterator() {
+    public AttributedCharacterIterator getAttributes() {
         return nsb.getIterator();
     }
 
index b6dd1f466003b73d54a9ecb2cf092fa544324a06..3d2a5f7eac2430b0956d43040b8e9ec1bc4dfa24 100644 (file)
@@ -22,13 +22,13 @@ public abstract class FractionRounder extends Rounder {
      * <p>
      * This setting does not affect the number of trailing zeros. For example, 3.01 would print as "3", not "3.0".
      *
-     * @param minFigures
+     * @param minSignificantDigits
      *            The number of significant figures to guarantee.
      * @return An immutable object for chaining.
      */
-    public Rounder withMinFigures(int minFigures) {
-        if (minFigures > 0 && minFigures <= MAX_VALUE) {
-            return constructFractionSignificant(this, minFigures, -1);
+    public Rounder withMinDigits(int minSignificantDigits) {
+        if (minSignificantDigits > 0 && minSignificantDigits <= MAX_VALUE) {
+            return constructFractionSignificant(this, minSignificantDigits, -1);
         } else {
             throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
         }
@@ -46,13 +46,13 @@ public abstract class FractionRounder extends Rounder {
      * This setting does not affect the number of trailing zeros. For example, with fixed fraction of 2, 123.4 would
      * become "120.00".
      *
-     * @param maxFigures
+     * @param maxSignificantDigits
      *            Round the number to no more than this number of significant figures.
      * @return An immutable object for chaining.
      */
-    public Rounder withMaxFigures(int maxFigures) {
-        if (maxFigures > 0 && maxFigures <= MAX_VALUE) {
-            return constructFractionSignificant(this, -1, maxFigures);
+    public Rounder withMaxDigits(int maxSignificantDigits) {
+        if (maxSignificantDigits > 0 && maxSignificantDigits <= MAX_VALUE) {
+            return constructFractionSignificant(this, -1, maxSignificantDigits);
         } else {
             throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
         }
index 0db2586769e8ab997c98822b3a380c0df29f2348..e4aeadc712b0eaf84d3dbd013452353165431509 100644 (file)
@@ -3,7 +3,7 @@
 package newapi;
 
 import com.ibm.icu.impl.number.FormatQuantity;
-import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult;
+import com.ibm.icu.impl.number.PatternParser.ParsedPatternInfo;
 
 public class Grouper {
 
@@ -64,11 +64,11 @@ public class Grouper {
         }
     }
 
-    static Grouper normalizeType(Grouper grouping, PatternParseResult patternInfo) {
+    static Grouper normalizeType(Grouper grouping, ParsedPatternInfo patternInfo) {
         return grouping.withLocaleData(patternInfo);
     }
 
-    Grouper withLocaleData(PatternParseResult patternInfo) {
+    Grouper withLocaleData(ParsedPatternInfo patternInfo) {
         if (grouping1 != -2) {
             return this;
         }
similarity index 64%
rename from icu4j/main/classes/core/src/newapi/MurkyLongNameHandler.java
rename to icu4j/main/classes/core/src/newapi/LongNameHandler.java
index 8fee3a347e1bb12bbcaf824289ab0da05510823b..222c6f1f8c91ec8aeb694b31f90f0e64badefe24 100644 (file)
@@ -11,28 +11,33 @@ import com.ibm.icu.impl.StandardPlural;
 import com.ibm.icu.impl.number.FormatQuantity;
 import com.ibm.icu.impl.number.Modifier;
 import com.ibm.icu.impl.number.modifiers.SimpleModifier;
-import com.ibm.icu.text.MeasureFormat.FormatWidth;
 import com.ibm.icu.text.NumberFormat.Field;
 import com.ibm.icu.text.PluralRules;
 import com.ibm.icu.util.Currency;
 import com.ibm.icu.util.MeasureUnit;
 import com.ibm.icu.util.ULocale;
 
+import newapi.NumberFormatter.UnitWidth;
 import newapi.impl.MeasureData;
 import newapi.impl.MicroProps;
-import newapi.impl.QuantityChain;
+import newapi.impl.MicroPropsGenerator;
 
-class MurkyLongNameHandler implements QuantityChain {
+class LongNameHandler implements MicroPropsGenerator {
 
     private final Map<StandardPlural, Modifier> data;
     /* unsafe */ PluralRules rules;
-    /* unsafe */ QuantityChain parent;
+    /* unsafe */ MicroPropsGenerator parent;
 
-    private MurkyLongNameHandler(Map<StandardPlural, Modifier> data) {
+    private LongNameHandler(Map<StandardPlural, Modifier> data) {
         this.data = data;
     }
 
-    public static MurkyLongNameHandler getCurrencyLongNameModifiers(ULocale loc, Currency currency) {
+    /** For use by the "safe" code path */
+    private LongNameHandler(LongNameHandler other) {
+        this.data = other.data;
+    }
+
+    public static LongNameHandler getCurrencyLongNameModifiers(ULocale loc, Currency currency) {
         Map<String, String> data = CurrencyData.provider.getInstance(loc, true).getUnitPatterns();
         Map<StandardPlural, Modifier> result = new EnumMap<StandardPlural, Modifier>(StandardPlural.class);
         StringBuilder sb = new StringBuilder();
@@ -46,10 +51,10 @@ class MurkyLongNameHandler implements QuantityChain {
             Modifier mod = new SimpleModifier(compiled, Field.CURRENCY, false);
             result.put(plural, mod);
         }
-        return new MurkyLongNameHandler(result);
+        return new LongNameHandler(result);
     }
 
-    public static MurkyLongNameHandler getMeasureUnitModifiers(ULocale loc, MeasureUnit unit, FormatWidth width) {
+    public static LongNameHandler getMeasureUnitModifiers(ULocale loc, MeasureUnit unit, UnitWidth width) {
         Map<StandardPlural, String> simpleFormats = MeasureData.getMeasureData(loc, unit, width);
         Map<StandardPlural, Modifier> result = new EnumMap<StandardPlural, Modifier>(StandardPlural.class);
         StringBuilder sb = new StringBuilder();
@@ -62,13 +67,28 @@ class MurkyLongNameHandler implements QuantityChain {
             Modifier mod = new SimpleModifier(compiled, Field.CURRENCY, false);
             result.put(plural, mod);
         }
-        return new MurkyLongNameHandler(result);
+        return new LongNameHandler(result);
     }
 
-    public QuantityChain withLocaleData(PluralRules rules, boolean safe, QuantityChain parent) {
+    /**
+     * Applies locale data and inserts a long-name handler into the quantity chain.
+     *
+     * @param rules
+     *            The PluralRules instance to reference.
+     * @param safe
+     *            If true, creates a new object to insert into the quantity chain. If false, re-uses <em>this</em>
+     *            object in the quantity chain.
+     * @param parent
+     *            The old head of the quantity chain.
+     * @return The new head of the quantity chain.
+     */
+    public MicroPropsGenerator withLocaleData(PluralRules rules, boolean safe, MicroPropsGenerator parent) {
         if (safe) {
             // Safe code path: return a new object
-            return new ImmutableLongNameHandler(data, rules, parent);
+            LongNameHandler copy = new LongNameHandler(this);
+            copy.rules = rules;
+            copy.parent = parent;
+            return copy;
         } else {
             // Unsafe code path: re-use this object!
             this.rules = rules;
@@ -78,34 +98,12 @@ class MurkyLongNameHandler implements QuantityChain {
     }
 
     @Override
-    public MicroProps withQuantity(FormatQuantity quantity) {
-        MicroProps micros = parent.withQuantity(quantity);
+    public MicroProps processQuantity(FormatQuantity quantity) {
+        MicroProps micros = parent.processQuantity(quantity);
         // TODO: Avoid the copy here?
         FormatQuantity copy = quantity.createCopy();
         micros.rounding.apply(copy);
         micros.modOuter = data.get(copy.getStandardPlural(rules));
         return micros;
     }
-
-    public static class ImmutableLongNameHandler implements QuantityChain {
-        final Map<StandardPlural, Modifier> data;
-        final PluralRules rules;
-        final QuantityChain parent;
-
-        public ImmutableLongNameHandler(Map<StandardPlural, Modifier> data, PluralRules rules, QuantityChain parent) {
-            this.data = data;
-            this.rules = rules;
-            this.parent = parent;
-        }
-
-        @Override
-        public MicroProps withQuantity(FormatQuantity quantity) {
-            MicroProps micros = parent.withQuantity(quantity);
-            // TODO: Avoid the copy here?
-            FormatQuantity copy = quantity.createCopy();
-            micros.rounding.apply(copy);
-            micros.modOuter = data.get(copy.getStandardPlural(rules));
-            return micros;
-        }
-    }
 }
similarity index 90%
rename from icu4j/main/classes/core/src/newapi/MurkyModifier.java
rename to icu4j/main/classes/core/src/newapi/MutablePatternModifier.java
index f71e15ffbcf8744555777457281d512701f69c6c..f730b0bdeb2cbbda8ae18a3f8e10da1e7553e5e9 100644 (file)
@@ -6,20 +6,20 @@ import com.ibm.icu.impl.StandardPlural;
 import com.ibm.icu.impl.number.AffixPatternUtils;
 import com.ibm.icu.impl.number.AffixPatternUtils.SymbolProvider;
 import com.ibm.icu.impl.number.FormatQuantity;
-import com.ibm.icu.impl.number.LdmlPatternInfo;
+import com.ibm.icu.impl.number.PatternParser;
 import com.ibm.icu.impl.number.Modifier;
 import com.ibm.icu.impl.number.NumberStringBuilder;
 import com.ibm.icu.impl.number.modifiers.ConstantMultiFieldModifier;
 import com.ibm.icu.impl.number.modifiers.CurrencySpacingEnabledModifier;
 import com.ibm.icu.text.DecimalFormatSymbols;
-import com.ibm.icu.text.MeasureFormat.FormatWidth;
 import com.ibm.icu.text.PluralRules;
 import com.ibm.icu.util.Currency;
 
 import newapi.NumberFormatter.SignDisplay;
+import newapi.NumberFormatter.UnitWidth;
 import newapi.impl.AffixPatternProvider;
 import newapi.impl.MicroProps;
-import newapi.impl.QuantityChain;
+import newapi.impl.MicroPropsGenerator;
 
 /**
  * This class is a {@link Modifier} that wraps a decimal format pattern. It applies the pattern's affixes in
@@ -37,11 +37,12 @@ import newapi.impl.QuantityChain;
  * <p>
  * This is a MUTABLE, NON-THREAD-SAFE class designed for performance. Do NOT save references to this or attempt to use
  * it from multiple threads! Instead, you can obtain a safe, immutable decimal format pattern modifier by calling
- * {@link MurkyModifier#createImmutable}, in effect treating this instance as a builder for the immutable variant.
+ * {@link MutablePatternModifier#createImmutable}, in effect treating this instance as a builder for the immutable
+ * variant.
  *
  * FIXME: Make this package-private
  */
-public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, QuantityChain {
+public class MutablePatternModifier implements Modifier, SymbolProvider, CharSequence, MicroPropsGenerator {
 
     // Modifier details
     final boolean isStrong;
@@ -53,7 +54,7 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
 
     // Symbol details
     DecimalFormatSymbols symbols;
-    FormatWidth unitWidth;
+    UnitWidth unitWidth;
     String currency1;
     String currency2;
     String[] currency3;
@@ -64,7 +65,7 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
     StandardPlural plural;
 
     // QuantityChain details
-    QuantityChain parent;
+    MicroPropsGenerator parent;
 
     // Transient CharSequence fields
     boolean inCharSequenceMode;
@@ -79,13 +80,13 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
      *            {@link Modifier#isStrong()}. Most of the time, decimal format pattern modifiers should be considered
      *            as non-strong.
      */
-    public MurkyModifier(boolean isStrong) {
+    public MutablePatternModifier(boolean isStrong) {
         this.isStrong = isStrong;
     }
 
     /**
      * Sets a reference to the parsed decimal format pattern, usually obtained from
-     * {@link LdmlPatternInfo#parse(String)}, but any implementation of {@link AffixPatternProvider} is accepted.
+     * {@link PatternParser#parse(String)}, but any implementation of {@link AffixPatternProvider} is accepted.
      */
     public void setPatternInfo(AffixPatternProvider patternInfo) {
         this.patternInfo = patternInfo;
@@ -118,7 +119,7 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
      *            Required if the triple currency sign, "¤¤¤", appears in the pattern, which can be determined from the
      *            convenience method {@link #needsPlurals()}.
      */
-    public void setSymbols(DecimalFormatSymbols symbols, Currency currency, FormatWidth unitWidth, PluralRules rules) {
+    public void setSymbols(DecimalFormatSymbols symbols, Currency currency, UnitWidth unitWidth, PluralRules rules) {
         assert (rules != null) == needsPlurals();
         this.symbols = symbols;
         this.unitWidth = unitWidth;
@@ -182,7 +183,7 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
      *            The QuantityChain to which to chain this immutable.
      * @return An immutable that supports both positive and negative numbers.
      */
-    public ImmutableMurkyModifier createImmutableAndChain(QuantityChain parent) {
+    public ImmutableMurkyModifier createImmutableAndChain(MicroPropsGenerator parent) {
         NumberStringBuilder a = new NumberStringBuilder();
         NumberStringBuilder b = new NumberStringBuilder();
         if (needsPlurals()) {
@@ -217,25 +218,25 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
         }
     }
 
-    public static interface ImmutableMurkyModifier extends QuantityChain {
+    public static interface ImmutableMurkyModifier extends MicroPropsGenerator {
         public void applyToMicros(MicroProps micros, FormatQuantity quantity);
     }
 
     public static class ImmutableMurkyModifierWithoutPlurals implements ImmutableMurkyModifier {
         final Modifier positive;
         final Modifier negative;
-        final QuantityChain parent;
+        final MicroPropsGenerator parent;
 
-        public ImmutableMurkyModifierWithoutPlurals(Modifier positive, Modifier negative, QuantityChain parent) {
+        public ImmutableMurkyModifierWithoutPlurals(Modifier positive, Modifier negative, MicroPropsGenerator parent) {
             this.positive = positive;
             this.negative = negative;
             this.parent = parent;
         }
 
         @Override
-        public MicroProps withQuantity(FormatQuantity quantity) {
+        public MicroProps processQuantity(FormatQuantity quantity) {
             assert parent != null;
-            MicroProps micros = parent.withQuantity(quantity);
+            MicroProps micros = parent.processQuantity(quantity);
             applyToMicros(micros, quantity);
             return micros;
         }
@@ -253,9 +254,9 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
     public static class ImmutableMurkyModifierWithPlurals implements ImmutableMurkyModifier {
         final Modifier[] mods;
         final PluralRules rules;
-        final QuantityChain parent;
+        final MicroPropsGenerator parent;
 
-        public ImmutableMurkyModifierWithPlurals(Modifier[] mods, PluralRules rules, QuantityChain parent) {
+        public ImmutableMurkyModifierWithPlurals(Modifier[] mods, PluralRules rules, MicroPropsGenerator parent) {
             assert mods.length == getModsLength();
             assert rules != null;
             this.mods = mods;
@@ -272,9 +273,9 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
         }
 
         @Override
-        public MicroProps withQuantity(FormatQuantity quantity) {
+        public MicroProps processQuantity(FormatQuantity quantity) {
             assert parent != null;
-            MicroProps micros = parent.withQuantity(quantity);
+            MicroProps micros = parent.processQuantity(quantity);
             applyToMicros(micros, quantity);
             return micros;
         }
@@ -290,14 +291,14 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
         }
     }
 
-    public QuantityChain addToChain(QuantityChain parent) {
+    public MicroPropsGenerator addToChain(MicroPropsGenerator parent) {
         this.parent = parent;
         return this;
     }
 
     @Override
-    public MicroProps withQuantity(FormatQuantity fq) {
-        MicroProps micros = parent.withQuantity(fq);
+    public MicroProps processQuantity(FormatQuantity fq) {
+        MicroProps micros = parent.processQuantity(fq);
         if (needsPlurals()) {
             // TODO: Fix this. Avoid the copy.
             FormatQuantity copy = fq.createCopy();
@@ -321,7 +322,8 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
 
     @Override
     public int getPrefixLength() {
-        return insertPrefix(null, 0);
+        NumberStringBuilder dummy = new NumberStringBuilder();
+        return insertPrefix(dummy, 0);
     }
 
     @Override
@@ -356,7 +358,7 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
             return symbols.getPerMillString();
         case AffixPatternUtils.TYPE_CURRENCY_SINGLE:
             // FormatWidth ISO overrides the singular currency symbol
-            if (unitWidth == FormatWidth.SHORT) {
+            if (unitWidth == UnitWidth.ISO_CODE) {
                 return currency2;
             } else {
                 return currency1;
@@ -387,7 +389,8 @@ public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, Qu
         inCharSequenceMode = true;
 
         // Should the output render '+' where '-' would normally appear in the pattern?
-        plusReplacesMinusSign = !isNegative && signDisplay == SignDisplay.ALWAYS
+        plusReplacesMinusSign = !isNegative
+                && (signDisplay == SignDisplay.ALWAYS || signDisplay == SignDisplay.ACCOUNTING_ALWAYS)
                 && patternInfo.positiveHasPlusSign() == false;
 
         // Should we use the negative affix pattern? (If not, we will use the positive one)
index c3abf489e40481adbf2f7d99a9c7eb8e386bc3e7..7cab5dffeb35730cfbcc0fb4e8286d0fd756b259 100644 (file)
@@ -4,48 +4,43 @@ package newapi;
 
 import java.util.Locale;
 
-import com.ibm.icu.util.MeasureUnit;
 import com.ibm.icu.util.ULocale;
 
 public final class NumberFormatter {
 
-  private static final UnlocalizedNumberFormatter BASE = new UnlocalizedNumberFormatter();
-
-  // This could possibly be combined into MeasureFormat.FormatWidth
-  public static enum CurrencyDisplay {
-    SYMBOL, // ¤
-    ISO_4217, // ¤¤
-    DISPLAY_NAME, // ¤¤¤
-    SYMBOL_NARROW, // ¤¤¤¤
-    HIDDEN, // uses currency rounding and formatting but omits the currency symbol
-    // TODO: For hidden, what to do if currency symbol appears in the middle, as in Portugal ?
-  }
-
-  public static enum DecimalMarkDisplay {
-    AUTO,
-    ALWAYS,
-  }
-
-  public static enum SignDisplay {
-    AUTO,
-    ALWAYS,
-    NEVER,
-  }
-
-  public static UnlocalizedNumberFormatter fromSkeleton(String skeleton) {
-    // FIXME
-    throw new UnsupportedOperationException();
-  }
-
-  public static UnlocalizedNumberFormatter with() {
-    return BASE;
-  }
-
-  public static LocalizedNumberFormatter withLocale(Locale locale) {
-    return BASE.locale(locale);
-  }
-
-  public static LocalizedNumberFormatter withLocale(ULocale locale) {
-    return BASE.locale(locale);
-  }
+    private static final UnlocalizedNumberFormatter BASE = new UnlocalizedNumberFormatter();
+
+    public static enum UnitWidth {
+        NARROW, // ¤¤¤¤¤ or narrow measure unit
+        SHORT, // ¤ or short measure unit (DEFAULT)
+        ISO_CODE, // ¤¤; undefined for measure unit
+        FULL_NAME, // ¤¤¤ or wide unit
+        HIDDEN, // no unit is displayed, but other unit effects are obeyed (like currency rounding)
+        // TODO: For hidden, what to do if currency symbol appears in the middle, as in Portugal ?
+    }
+
+    public static enum DecimalMarkDisplay {
+        AUTO, ALWAYS,
+    }
+
+    public static enum SignDisplay {
+        AUTO, ALWAYS, NEVER, ACCOUNTING, ACCOUNTING_ALWAYS,
+    }
+
+    public static UnlocalizedNumberFormatter fromSkeleton(String skeleton) {
+        // FIXME
+        throw new UnsupportedOperationException();
+    }
+
+    public static UnlocalizedNumberFormatter with() {
+        return BASE;
+    }
+
+    public static LocalizedNumberFormatter withLocale(Locale locale) {
+        return BASE.locale(locale);
+    }
+
+    public static LocalizedNumberFormatter withLocale(ULocale locale) {
+        return BASE.locale(locale);
+    }
 }
index 112a3092935c8eb7a9ddda9824b8392404f799ce..c370b2436b657fe57b470cb55c25bcdf158c118c 100644 (file)
@@ -5,14 +5,22 @@ package newapi;
 import com.ibm.icu.text.DecimalFormatSymbols;
 import com.ibm.icu.text.MeasureFormat.FormatWidth;
 import com.ibm.icu.text.NumberingSystem;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Measure;
 import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.NoUnit;
 import com.ibm.icu.util.ULocale;
 
 import newapi.NumberFormatter.DecimalMarkDisplay;
 import newapi.NumberFormatter.SignDisplay;
+import newapi.NumberFormatter.UnitWidth;
 import newapi.impl.MacroProps;
 import newapi.impl.Padder;
 
+/**
+ * An abstract base class for specifying settings related to number formatting. This class is implemented by
+ * {@link UnlocalizedNumberFormatter} and {@link LocalizedNumberFormatter}.
+ */
 public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<?>> {
 
     static final int KEY_MACROS = 0;
@@ -41,35 +49,292 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
         this.value = value;
     }
 
+    /**
+     * Specifies the notation style (simple, scientific, or compact) for rendering numbers.
+     *
+     * <ul>
+     * <li>Simple notation: "12,300"
+     * <li>Scientific notation: "1.23E4"
+     * <li>Compact notation: "12K"
+     * </ul>
+     *
+     * <p>
+     * All notation styles will be properly localized with locale data, and all notation styles are compatible with
+     * units, rounding strategies, and other number formatter settings.
+     *
+     * <p>
+     * Pass this method the return value of a {@link Notation} factory method. For example:
+     *
+     * <pre>
+     * NumberFormatter.with().notation(Notation.compactShort())
+     * </pre>
+     *
+     * The default is to use simple notation.
+     *
+     * @param notation
+     *            The notation strategy to use.
+     * @return The fluent chain.
+     * @see Notation
+     * @provisional This API might change or be removed in a future release.
+     * @draft ICU 60
+     */
     public T notation(Notation notation) {
         return create(KEY_NOTATION, notation);
     }
 
+    /**
+     * Specifies the unit (unit of measure, currency, or percent) to associate with rendered numbers.
+     *
+     * <ul>
+     * <li>Unit of measure: "12.3 meters"
+     * <li>Currency: "$12.30"
+     * <li>Percent: "12.3%"
+     * </ul>
+     *
+     * <p>
+     * <strong>Note:</strong> The unit can also be specified by passing a {@link Measure} to
+     * {@link LocalizedNumberFormatter#format(Measure)}. Units specified via the format method take precedence over
+     * units specified here.
+     *
+     * <p>
+     * All units will be properly localized with locale data, and all units are compatible with notation styles,
+     * rounding strategies, and other number formatter settings.
+     *
+     * <p>
+     * Pass this method any instance of {@link MeasureUnit}. For units of measure:
+     *
+     * <pre>
+     * NumberFormatter.with().unit(MeasureUnit.METER)
+     * </pre>
+     *
+     * Currency:
+     *
+     * <pre>
+     * NumberFormatter.with().unit(Currency.getInstance("USD"))
+     * </pre>
+     *
+     * Percent:
+     *
+     * <pre>
+     * NumberFormatter.with().unit(NoUnit.PERCENT)
+     * </pre>
+     *
+     * The default is to render without units (equivalent to {@link NoUnit#BASE}).
+     *
+     * @param unit
+     *            The unit to render.
+     * @return The fluent chain.
+     * @see MeasureUnit
+     * @see Currency
+     * @see NoUnit
+     * @provisional This API might change or be removed in a future release.
+     * @draft ICU 60
+     */
     public T unit(MeasureUnit unit) {
         return create(KEY_UNIT, unit);
     }
 
+    /**
+     * Specifies the rounding strategy to use when formatting numbers.
+     *
+     * <ul>
+     * <li>Round to 3 decimal places: "3.142"
+     * <li>Round to 3 significant figures: "3.14"
+     * <li>Round to the closest nickel: "3.15"
+     * <li>Do not perform rounding: "3.1415926..."
+     * </ul>
+     *
+     * <p>
+     * Pass this method the return value of one of the factory methods on {@link Rounder}. For example:
+     *
+     * <pre>
+     * NumberFormatter.with().rounding(Rounder.fixedFraction(2))
+     * </pre>
+     *
+     * The default is to not perform rounding.
+     *
+     * @param rounder
+     *            The rounding strategy to use.
+     * @return The fluent chain.
+     * @see Rounder
+     * @provisional This API might change or be removed in a future release.
+     * @draft ICU 60
+     */
     public T rounding(Rounder rounder) {
         return create(KEY_ROUNDER, rounder);
     }
 
+    /**
+     * Specifies the grouping strategy to use when formatting numbers.
+     *
+     * <ul>
+     * <li>Default grouping: "12,300" and "1,230"
+     * <li>Grouping with at least 2 digits: "12,300" and "1230"
+     * <li>No grouping: "12300" and "1230"
+     * </ul>
+     *
+     * <p>
+     * The exact grouping widths will be chosen based on the locale.
+     *
+     * <p>
+     * Pass this method the return value of one of the factory methods on {@link Grouper}. For example:
+     *
+     * <pre>
+     * NumberFormatter.with().grouping(Grouper.min2())
+     * </pre>
+     *
+     * The default is to perform grouping without concern for the minimum grouping digits.
+     *
+     * @param grouper
+     *            The grouping strategy to use.
+     * @return The fluent chain.
+     * @see Grouper
+     * @see Notation
+     * @provisional This API might change or be removed in a future release.
+     * @draft ICU 60
+     */
     public T grouping(Grouper grouper) {
         return create(KEY_GROUPER, grouper);
     }
 
+    /**
+     * Specifies the minimum and maximum number of digits to render before the decimal mark.
+     *
+     * <ul>
+     * <li>Zero minimum integer digits: ".08"
+     * <li>One minimum integer digit: "0.08"
+     * <li>Two minimum integer digits: "00.08"
+     * </ul>
+     *
+     * <p>
+     * Pass this method the return value of {@link IntegerWidth#zeroFillTo(int)}. For example:
+     *
+     * <pre>
+     * NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(2))
+     * </pre>
+     *
+     * The default is to have one minimum integer digit.
+     *
+     * @param style
+     *            The integer width to use.
+     * @return The fluent chain.
+     * @see IntegerWidth
+     * @provisional This API might change or be removed in a future release.
+     * @draft ICU 60
+     */
     public T integerWidth(IntegerWidth style) {
         return create(KEY_INTEGER, style);
     }
 
+    /**
+     * Specifies the symbols (decimal separator, grouping separator, percent sign, numerals, etc.) to use when rendering
+     * numbers.
+     *
+     * <ul>
+     * <li><em>en_US</em> symbols: "12,345.67"
+     * <li><em>fr_FR</em> symbols: "12&nbsp;345,67"
+     * <li><em>de_CH</em> symbols: "12’345.67"
+     * <li><em>my_MY</em> symbols: "၁၂,၃၄၅.၆၇"
+     * </ul>
+     *
+     * <p>
+     * Pass this method an instance of {@link DecimalFormatSymbols}. For example:
+     *
+     * <pre>
+     * NumberFormatter.with().symbols(DecimalFormatSymbols.getInstance(new ULocale("de_CH")))
+     * </pre>
+     *
+     * <p>
+     * <strong>Note:</strong> DecimalFormatSymbols automatically chooses the best numbering system based on the locale.
+     * In the examples above, the first three are using the Latin numbering system, and the fourth is using the Myanmar
+     * numbering system.
+     *
+     * <p>
+     * <strong>Note:</strong> The instance of DecimalFormatSymbols will be copied: changes made to the symbols object
+     * after passing it into the fluent chain will not be seen.
+     *
+     * <p>
+     * <strong>Note:</strong> Calling this method will override the NumberingSystem previously specified in
+     * {@link #symbols(NumberingSystem)}.
+     *
+     * <p>
+     * The default is to choose the symbols based on the locale specified in the fluent chain.
+     *
+     * @param symbols
+     *            The DecimalFormatSymbols to use.
+     * @return The fluent chain.
+     * @see DecimalFormatSymbols
+     * @provisional This API might change or be removed in a future release.
+     * @draft ICU 60
+     */
     public T symbols(DecimalFormatSymbols symbols) {
+        symbols = (DecimalFormatSymbols) symbols.clone();
         return create(KEY_SYMBOLS, symbols);
     }
 
+    /**
+     * Specifies that the given numbering system should be used when fetching symbols.
+     *
+     * <ul>
+     * <li>Latin numbering system: "12,345"
+     * <li>Myanmar numbering system: "၁၂,၃၄၅"
+     * <li>Math Sans Bold numbering system: "𝟭𝟮,𝟯𝟰𝟱"
+     * </ul>
+     *
+     * <p>
+     * Pass this method an instance of {@link NumberingSystem}. For example, to force the locale to always use the Latin
+     * alphabet numbering system (ASCII digits):
+     *
+     * <pre>
+     * NumberFormatter.with().symbols(NumberingSystem.LATIN)
+     * </pre>
+     *
+     * <p>
+     * <strong>Note:</strong> Calling this method will override the DecimalFormatSymbols previously specified in
+     * {@link #symbols(DecimalFormatSymbols)}.
+     *
+     * <p>
+     * The default is to choose the best numbering system for the locale.
+     *
+     * @param ns
+     *            The NumberingSystem to use.
+     * @return The fluent chain.
+     * @see NumberingSystem
+     * @provisional This API might change or be removed in a future release.
+     * @draft ICU 60
+     */
     public T symbols(NumberingSystem ns) {
         return create(KEY_SYMBOLS, ns);
     }
 
-    public T unitWidth(FormatWidth style) {
+    /**
+     * Sets the width of the unit (measure unit or currency).
+     *
+     * <ul>
+     * <li>Narrow: "$12.00", "12 m"
+     * <li>Short: "12.00 USD", "12 m"
+     * <li>Wide: "12.00 US dollars", "12 meters"
+     * <li>Hidden: "12.00", "12"
+     * </ul>
+     *
+     * <p>
+     * Pass an element from the {@link FormatWidth} enum to this setter. For example:
+     *
+     * <pre>
+     * NumberFormatter.with().unitWidth(FormatWidth.SHORT)
+     * </pre>
+     *
+     * <p>
+     * The default is the narrow width.
+     *
+     * @param style
+     *            The with to use when rendering numbers.
+     * @return The fluent chain
+     * @see FormatWidth
+     * @provisional This API might change or be removed in a future release.
+     * @draft ICU 60
+     */
+    public T unitWidth(UnitWidth style) {
         return create(KEY_UNIT_WIDTH, style);
     }
 
@@ -161,7 +426,7 @@ public abstract class NumberFormatterSettings<T extends NumberFormatterSettings<
                 break;
             case KEY_UNIT_WIDTH:
                 if (macros.unitWidth == null) {
-                    macros.unitWidth = (FormatWidth) current.value;
+                    macros.unitWidth = (UnitWidth) current.value;
                 }
                 break;
             case KEY_SIGN:
index 274667ba3b7d889922ab0e6e5ee5fd34659dee99..c273cac446dc9dc22ca1d9e1571452bbc0167b78 100644 (file)
@@ -7,9 +7,9 @@ import java.math.MathContext;
 
 import com.ibm.icu.impl.StandardPlural;
 import com.ibm.icu.impl.number.AffixPatternUtils;
-import com.ibm.icu.impl.number.LdmlPatternInfo;
-import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult;
-import com.ibm.icu.impl.number.PatternString;
+import com.ibm.icu.impl.number.PatternAndPropertyUtils;
+import com.ibm.icu.impl.number.PatternParser;
+import com.ibm.icu.impl.number.PatternParser.ParsedPatternInfo;
 import com.ibm.icu.impl.number.Properties;
 import com.ibm.icu.impl.number.RoundingUtils;
 import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
@@ -44,7 +44,7 @@ public final class NumberPropertyMapper {
      * public API if there is demand.
      */
     public static UnlocalizedNumberFormatter create(String pattern, DecimalFormatSymbols symbols) {
-        Properties properties = PatternString.parseToProperties(pattern);
+        Properties properties = PatternAndPropertyUtils.parseToProperties(pattern);
         return create(properties, symbols);
     }
 
@@ -407,9 +407,9 @@ public final class NumberPropertyMapper {
         private final AffixPatternProvider[] affixesByPlural;
 
         public CurrencyPluralInfoAffixProvider(CurrencyPluralInfo cpi) {
-            affixesByPlural = new PatternParseResult[StandardPlural.COUNT];
+            affixesByPlural = new ParsedPatternInfo[StandardPlural.COUNT];
             for (StandardPlural plural : StandardPlural.VALUES) {
-                affixesByPlural[plural.ordinal()] = LdmlPatternInfo
+                affixesByPlural[plural.ordinal()] = PatternParser
                         .parse(cpi.getCurrencyPluralPattern(plural.getKeyword()));
             }
         }
index 5a6e74d6b2fcb110e6b136d026d67f62d570ccfb..37ad2e1c1f076af83770e36d99aeb094441993b9 100644 (file)
@@ -33,65 +33,65 @@ public abstract class Rounder implements Cloneable {
         return constructFraction(0, 0);
     }
 
-    public static FractionRounder fixedFraction(int minMaxFrac) {
-        if (minMaxFrac >= 0 && minMaxFrac <= MAX_VALUE) {
-            return constructFraction(minMaxFrac, minMaxFrac);
+    public static FractionRounder fixedFraction(int minMaxFractionDigits) {
+        if (minMaxFractionDigits >= 0 && minMaxFractionDigits <= MAX_VALUE) {
+            return constructFraction(minMaxFractionDigits, minMaxFractionDigits);
         } else {
             throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE);
         }
     }
 
-    public static FractionRounder minFraction(int minFrac) {
-        if (minFrac >= 0 && minFrac < MAX_VALUE) {
-            return constructFraction(minFrac, -1);
+    public static FractionRounder minFraction(int minFractionDigits) {
+        if (minFractionDigits >= 0 && minFractionDigits < MAX_VALUE) {
+            return constructFraction(minFractionDigits, -1);
         } else {
             throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE);
         }
     }
 
-    public static FractionRounder maxFraction(int maxFrac) {
-        if (maxFrac >= 0 && maxFrac < MAX_VALUE) {
-            return constructFraction(0, maxFrac);
+    public static FractionRounder maxFraction(int maxFractionDigits) {
+        if (maxFractionDigits >= 0 && maxFractionDigits < MAX_VALUE) {
+            return constructFraction(0, maxFractionDigits);
         } else {
             throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE);
         }
     }
 
-    public static FractionRounder minMaxFraction(int minFrac, int maxFrac) {
-        if (minFrac >= 0 && maxFrac <= MAX_VALUE && minFrac <= maxFrac) {
-            return constructFraction(minFrac, maxFrac);
+    public static FractionRounder minMaxFraction(int minFractionDigits, int maxFractionDigits) {
+        if (minFractionDigits >= 0 && maxFractionDigits <= MAX_VALUE && minFractionDigits <= maxFractionDigits) {
+            return constructFraction(minFractionDigits, maxFractionDigits);
         } else {
             throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE);
         }
     }
 
-    public static Rounder fixedFigures(int minMaxSig) {
-        if (minMaxSig > 0 && minMaxSig <= MAX_VALUE) {
-            return constructSignificant(minMaxSig, minMaxSig);
+    public static Rounder fixedDigits(int minMaxSignificantDigits) {
+        if (minMaxSignificantDigits > 0 && minMaxSignificantDigits <= MAX_VALUE) {
+            return constructSignificant(minMaxSignificantDigits, minMaxSignificantDigits);
         } else {
             throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
         }
     }
 
-    public static Rounder minFigures(int minSig) {
-        if (minSig > 0 && minSig <= MAX_VALUE) {
-            return constructSignificant(minSig, -1);
+    public static Rounder minDigits(int minSignificantDigits) {
+        if (minSignificantDigits > 0 && minSignificantDigits <= MAX_VALUE) {
+            return constructSignificant(minSignificantDigits, -1);
         } else {
             throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
         }
     }
 
-    public static Rounder maxFigures(int maxSig) {
-        if (maxSig > 0 && maxSig <= MAX_VALUE) {
-            return constructSignificant(0, maxSig);
+    public static Rounder maxDigits(int maxSignificantDigits) {
+        if (maxSignificantDigits > 0 && maxSignificantDigits <= MAX_VALUE) {
+            return constructSignificant(0, maxSignificantDigits);
         } else {
             throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
         }
     }
 
-    public static Rounder minMaxFigures(int minSig, int maxSig) {
-        if (minSig > 0 && maxSig <= MAX_VALUE && minSig <= maxSig) {
-            return constructSignificant(minSig, maxSig);
+    public static Rounder minMaxDigits(int minSignificantDigits, int maxSignificantDigits) {
+        if (minSignificantDigits > 0 && maxSignificantDigits <= MAX_VALUE && minSignificantDigits <= maxSignificantDigits) {
+            return constructSignificant(minSignificantDigits, maxSignificantDigits);
         } else {
             throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
         }
index 3c2214c98b85bb6859d63a59d7c5ed43b0229eef..3ac73e293ea0156549ef0c49af9bb3ced926c7d5 100644 (file)
@@ -12,7 +12,7 @@ import newapi.NumberFormatter.SignDisplay;
 import newapi.Rounder.SignificantRounderImpl;
 import newapi.impl.MicroProps;
 import newapi.impl.MultiplierProducer;
-import newapi.impl.QuantityChain;
+import newapi.impl.MicroPropsGenerator;
 
 @SuppressWarnings("unused")
 public class ScientificNotation extends Notation implements Cloneable {
@@ -56,19 +56,19 @@ public class ScientificNotation extends Notation implements Cloneable {
         }
     }
 
-    /* package-private */ QuantityChain withLocaleData(DecimalFormatSymbols symbols, boolean build,
-            QuantityChain parent) {
+    /* package-private */ MicroPropsGenerator withLocaleData(DecimalFormatSymbols symbols, boolean build,
+            MicroPropsGenerator parent) {
         return new MurkyScientificHandler(symbols, build, parent);
     }
 
-    private class MurkyScientificHandler implements QuantityChain, MultiplierProducer, Modifier {
+    private class MurkyScientificHandler implements MicroPropsGenerator, MultiplierProducer, Modifier {
 
         final DecimalFormatSymbols symbols;
         final ImmutableScientificModifier[] precomputedMods;
-        final QuantityChain parent;
+        final MicroPropsGenerator parent;
         /* unsafe */ int exponent;
 
-        private MurkyScientificHandler(DecimalFormatSymbols symbols, boolean safe, QuantityChain parent) {
+        private MurkyScientificHandler(DecimalFormatSymbols symbols, boolean safe, MicroPropsGenerator parent) {
             this.symbols = symbols;
             this.parent = parent;
 
@@ -84,8 +84,8 @@ public class ScientificNotation extends Notation implements Cloneable {
         }
 
         @Override
-        public MicroProps withQuantity(FormatQuantity quantity) {
-            MicroProps micros = parent.withQuantity(quantity);
+        public MicroProps processQuantity(FormatQuantity quantity) {
+            MicroProps micros = parent.processQuantity(quantity);
             assert micros.rounding != null;
 
             // Treat zero as if it had magnitude 0
index bc77e212793c4326a041fcf221b164b89336f8f2..b44385b16b2f60b04fab267737e4f0c31e0ee6d9 100644 (file)
@@ -8,15 +8,15 @@ import java.math.RoundingMode;
 
 import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
 import com.ibm.icu.text.DecimalFormatSymbols;
-import com.ibm.icu.text.MeasureFormat.FormatWidth;
 import com.ibm.icu.text.NumberingSystem;
 import com.ibm.icu.util.Currency;
 import com.ibm.icu.util.Currency.CurrencyUsage;
-import com.ibm.icu.util.Dimensionless;
 import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.NoUnit;
 
 import newapi.NumberFormatter.DecimalMarkDisplay;
 import newapi.NumberFormatter.SignDisplay;
+import newapi.NumberFormatter.UnitWidth;
 import newapi.Rounder.CurrencyRounderImpl;
 import newapi.Rounder.FracSigRounderImpl;
 import newapi.Rounder.FractionRounderImpl;
@@ -228,7 +228,7 @@ final class SkeletonBuilder {
     }
 
     private static void unitToSkeleton(MeasureUnit value, StringBuilder sb) {
-        if (value.getType().equals("dimensionless")) {
+        if (value.getType().equals("none")) {
             if (value.getSubtype().equals("percent")) {
                 sb.append('%');
             } else if (value.getSubtype().equals("permille")) {
@@ -255,12 +255,12 @@ final class SkeletonBuilder {
         if (c0 == '%') {
             char c = safeCharAt(skeleton, offset++);
             if (c == '%') {
-                result = Dimensionless.PERCENT;
+                result = NoUnit.PERCENT;
             } else {
-                result = Dimensionless.PERMILLE;
+                result = NoUnit.PERMILLE;
             }
         } else if (c0 == 'B') {
-            result = Dimensionless.BASE;
+            result = NoUnit.BASE;
         } else if (c0 == '$') {
             String currencyCode = skeleton.substring(offset, offset + 3);
             offset += 3;
@@ -360,10 +360,10 @@ final class SkeletonBuilder {
             char c1 = skeleton.charAt(offset++);
             if (c1 == '<') {
                 char c2 = skeleton.charAt(offset++);
-                result = temp.withMaxFigures(c2 - '0');
+                result = temp.withMaxDigits(c2 - '0');
             } else if (c1 == '>') {
                 char c2 = skeleton.charAt(offset++);
-                result = temp.withMinFigures(c2 - '0');
+                result = temp.withMinDigits(c2 - '0');
             } else {
                 result = temp;
             }
@@ -518,7 +518,7 @@ final class SkeletonBuilder {
         }
     }
 
-    private static void unitWidthToSkeleton(FormatWidth value, StringBuilder sb) {
+    private static void unitWidthToSkeleton(UnitWidth value, StringBuilder sb) {
         sb.append(value.name());
     }
 
@@ -526,7 +526,7 @@ final class SkeletonBuilder {
         int originalOffset = offset;
         StringBuilder sb = new StringBuilder();
         offset += consumeUntil(skeleton, offset, ' ', sb);
-        output.unitWidth = Enum.valueOf(FormatWidth.class, sb.toString());
+        output.unitWidth = Enum.valueOf(UnitWidth.class, sb.toString());
         return offset - originalOffset;
     }
 
index 1453d7fc3d576b4cb192e2205367197d2529763c..1034e7b6ba73018c9e2035f314c7fc5f19d082b9 100644 (file)
 package newapi;
 
 import com.ibm.icu.impl.number.FormatQuantity;
-import com.ibm.icu.impl.number.LdmlPatternInfo;
-import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult;
+import com.ibm.icu.impl.number.PatternParser;
+import com.ibm.icu.impl.number.PatternParser.ParsedPatternInfo;
 import com.ibm.icu.impl.number.NumberStringBuilder;
 import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier;
 import com.ibm.icu.text.CompactDecimalFormat.CompactType;
 import com.ibm.icu.text.DecimalFormatSymbols;
-import com.ibm.icu.text.MeasureFormat.FormatWidth;
 import com.ibm.icu.text.NumberFormat;
 import com.ibm.icu.text.NumberingSystem;
 import com.ibm.icu.text.PluralRules;
 import com.ibm.icu.util.Currency;
 import com.ibm.icu.util.Currency.CurrencyUsage;
-import com.ibm.icu.util.Dimensionless;
+import com.ibm.icu.util.NoUnit;
 import com.ibm.icu.util.ULocale;
 
 import newapi.NumberFormatter.DecimalMarkDisplay;
 import newapi.NumberFormatter.SignDisplay;
+import newapi.NumberFormatter.UnitWidth;
 import newapi.impl.MacroProps;
 import newapi.impl.MicroProps;
+import newapi.impl.MicroPropsGenerator;
 import newapi.impl.Padder;
-import newapi.impl.QuantityChain;
 
 public class Worker1 {
 
     public static Worker1 fromMacros(MacroProps macros) {
-        return new Worker1(make(macros, true));
+        // Build a "safe" MicroPropsGenerator, which is thread-safe and can be used repeatedly.
+        MicroPropsGenerator microPropsGenerator = macrosToMicroGenerator(macros, true);
+        return new Worker1(microPropsGenerator);
     }
 
     public static MicroProps applyStatic(MacroProps macros, FormatQuantity inValue, NumberStringBuilder outString) {
-        MicroProps micros = make(macros, false).withQuantity(inValue);
-        applyStatic(micros, inValue, outString);
+        // Build an "unsafe" MicroPropsGenerator, which is cheaper but can be used only once.
+        MicroPropsGenerator microPropsGenerator = macrosToMicroGenerator(macros, false);
+        MicroProps micros = microPropsGenerator.processQuantity(inValue);
+        microsToString(micros, inValue, outString);
         return micros;
     }
 
     private static final Currency DEFAULT_CURRENCY = Currency.getInstance("XXX");
 
-    final QuantityChain microsGenerator;
+    final MicroPropsGenerator microPropsGenerator;
 
-    private Worker1(QuantityChain microsGenerator) {
-        this.microsGenerator = microsGenerator;
+    private Worker1(MicroPropsGenerator microsGenerator) {
+        this.microPropsGenerator = microsGenerator;
     }
 
     public MicroProps apply(FormatQuantity inValue, NumberStringBuilder outString) {
-        MicroProps micros = microsGenerator.withQuantity(inValue);
-        applyStatic(micros, inValue, outString);
+        MicroProps micros = microPropsGenerator.processQuantity(inValue);
+        microsToString(micros, inValue, outString);
         return micros;
     }
 
     //////////
 
-    private static QuantityChain make(MacroProps input, boolean build) {
+    /**
+     * Synthesizes the MacroProps into a MicroPropsGenerator. All information, including the locale, is encoded into the
+     * MicroPropsGenerator, except for the quantity itself, which is left abstract and must be provided to the returned
+     * MicroPropsGenerator instance.
+     *
+     * @see MicroPropsGenerator
+     * @param macros
+     *            The {@link MacroProps} to consume. This method does not mutate the MacroProps instance.
+     * @param safe
+     *            If true, the returned MicroPropsGenerator will be thread-safe. If false, the returned value will
+     *            <em>not</em> be thread-safe, intended for a single "one-shot" use only. Building the thread-safe
+     *            object is more expensive.
+     * @return
+     */
+    private static MicroPropsGenerator macrosToMicroGenerator(MacroProps macros, boolean safe) {
 
         String innerPattern = null;
-        MurkyLongNameHandler longNames = null;
+        LongNameHandler longNames = null;
         Rounder defaultRounding = Rounder.none();
         Currency currency = DEFAULT_CURRENCY;
-        FormatWidth unitWidth = null;
+        UnitWidth unitWidth = null;
         boolean perMille = false;
-        PluralRules rules = input.rules;
+        PluralRules rules = macros.rules;
 
-        MicroProps micros = new MicroProps(build);
-        QuantityChain chain = micros;
+        MicroProps micros = new MicroProps(safe);
+        MicroPropsGenerator chain = micros;
 
         // Copy over the simple settings
-        micros.sign = input.sign == null ? SignDisplay.AUTO : input.sign;
-        micros.decimal = input.decimal == null ? DecimalMarkDisplay.AUTO : input.decimal;
+        micros.sign = macros.sign == null ? SignDisplay.AUTO : macros.sign;
+        micros.decimal = macros.decimal == null ? DecimalMarkDisplay.AUTO : macros.decimal;
         micros.multiplier = 0;
-        micros.integerWidth = input.integerWidth == null ? IntegerWidth.zeroFillTo(1) : input.integerWidth;
+        micros.integerWidth = macros.integerWidth == null ? IntegerWidth.zeroFillTo(1) : macros.integerWidth;
 
-        if (input.unit == null || input.unit == Dimensionless.BASE) {
+        if (macros.unit == null || macros.unit == NoUnit.BASE) {
             // No units; default format
-            innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.NUMBERSTYLE);
-        } else if (input.unit == Dimensionless.PERCENT) {
+            innerPattern = NumberFormat.getPatternForStyle(macros.loc, NumberFormat.NUMBERSTYLE);
+        } else if (macros.unit == NoUnit.PERCENT) {
             // Percent
-            innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.PERCENTSTYLE);
+            innerPattern = NumberFormat.getPatternForStyle(macros.loc, NumberFormat.PERCENTSTYLE);
             micros.multiplier += 2;
-        } else if (input.unit == Dimensionless.PERMILLE) {
+        } else if (macros.unit == NoUnit.PERMILLE) {
             // Permille
-            innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.PERCENTSTYLE);
+            innerPattern = NumberFormat.getPatternForStyle(macros.loc, NumberFormat.PERCENTSTYLE);
             micros.multiplier += 3;
             perMille = true;
-        } else if (input.unit instanceof Currency && input.unitWidth != FormatWidth.WIDE) {
+        } else if (macros.unit instanceof Currency && macros.unitWidth != UnitWidth.FULL_NAME) {
             // Narrow, short, or ISO currency.
-            // TODO: Accounting style?
-            innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.CURRENCYSTYLE);
+            // TODO: Although ACCOUNTING and ACCOUNTING_ALWAYS are only supported in currencies right now,
+            // the API contract allows us to add support to other units.
+            if (macros.sign == SignDisplay.ACCOUNTING || macros.sign == SignDisplay.ACCOUNTING_ALWAYS) {
+                innerPattern = NumberFormat.getPatternForStyle(macros.loc, NumberFormat.ACCOUNTINGCURRENCYSTYLE);
+            } else {
+                innerPattern = NumberFormat.getPatternForStyle(macros.loc, NumberFormat.CURRENCYSTYLE);
+            }
             defaultRounding = Rounder.currency(CurrencyUsage.STANDARD);
-            currency = (Currency) input.unit;
+            currency = (Currency) macros.unit;
             micros.useCurrency = true;
-            unitWidth = (input.unitWidth == null) ? FormatWidth.NARROW : input.unitWidth;
-        } else if (input.unit instanceof Currency) {
+            unitWidth = (macros.unitWidth == null) ? UnitWidth.SHORT : macros.unitWidth;
+        } else if (macros.unit instanceof Currency) {
             // Currency long name
-            innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.NUMBERSTYLE);
-            longNames = MurkyLongNameHandler.getCurrencyLongNameModifiers(input.loc, (Currency) input.unit);
+            innerPattern = NumberFormat.getPatternForStyle(macros.loc, NumberFormat.NUMBERSTYLE);
+            longNames = LongNameHandler.getCurrencyLongNameModifiers(macros.loc, (Currency) macros.unit);
             defaultRounding = Rounder.currency(CurrencyUsage.STANDARD);
-            currency = (Currency) input.unit;
+            currency = (Currency) macros.unit;
             micros.useCurrency = true;
-            unitWidth = input.unitWidth = FormatWidth.WIDE;
+            unitWidth = UnitWidth.FULL_NAME;
         } else {
             // MeasureUnit
-            innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.NUMBERSTYLE);
-            unitWidth = (input.unitWidth == null) ? FormatWidth.SHORT : input.unitWidth;
-            longNames = MurkyLongNameHandler.getMeasureUnitModifiers(input.loc, input.unit, unitWidth);
+            innerPattern = NumberFormat.getPatternForStyle(macros.loc, NumberFormat.NUMBERSTYLE);
+            unitWidth = (macros.unitWidth == null) ? UnitWidth.SHORT : macros.unitWidth;
+            longNames = LongNameHandler.getMeasureUnitModifiers(macros.loc, macros.unit, unitWidth);
         }
 
         // Parse the pattern, which is used for grouping and affixes only.
-        PatternParseResult patternInfo = LdmlPatternInfo.parse(innerPattern);
+        ParsedPatternInfo patternInfo = PatternParser.parse(innerPattern);
 
         // Symbols
-        if (input.symbols == null) {
-            micros.symbols = DecimalFormatSymbols.getInstance(input.loc);
-        } else if (input.symbols instanceof DecimalFormatSymbols) {
-            micros.symbols = (DecimalFormatSymbols) input.symbols;
-        } else if (input.symbols instanceof NumberingSystem) {
+        if (macros.symbols == null) {
+            micros.symbols = DecimalFormatSymbols.getInstance(macros.loc);
+        } else if (macros.symbols instanceof DecimalFormatSymbols) {
+            micros.symbols = (DecimalFormatSymbols) macros.symbols;
+        } else if (macros.symbols instanceof NumberingSystem) {
             // TODO: Do this more efficiently. Will require modifying DecimalFormatSymbols.
-            NumberingSystem ns = (NumberingSystem) input.symbols;
-            ULocale temp = input.loc.setKeywordValue("numbers", ns.getName());
+            NumberingSystem ns = (NumberingSystem) macros.symbols;
+            ULocale temp = macros.loc.setKeywordValue("numbers", ns.getName());
             micros.symbols = DecimalFormatSymbols.getInstance(temp);
         } else {
             throw new AssertionError();
@@ -130,23 +153,23 @@ public class Worker1 {
         // Multiplier (compatibility mode value).
         // An int magnitude multiplier is used when not in compatibility mode to
         // reduce object creations.
-        if (input.multiplier != null) {
-            chain = input.multiplier.copyAndChain(chain);
+        if (macros.multiplier != null) {
+            chain = macros.multiplier.copyAndChain(chain);
         }
 
         // Rounding strategy
-        if (input.rounder != null) {
-            micros.rounding = Rounder.normalizeType(input.rounder, currency);
-        } else if (input.notation instanceof CompactNotation) {
+        if (macros.rounder != null) {
+            micros.rounding = Rounder.normalizeType(macros.rounder, currency);
+        } else if (macros.notation instanceof CompactNotation) {
             micros.rounding = Rounder.COMPACT_STRATEGY;
         } else {
             micros.rounding = Rounder.normalizeType(defaultRounding, currency);
         }
 
         // Grouping strategy
-        if (input.grouper != null) {
-            micros.grouping = Grouper.normalizeType(input.grouper, patternInfo);
-        } else if (input.notation instanceof CompactNotation) {
+        if (macros.grouper != null) {
+            micros.grouping = Grouper.normalizeType(macros.grouper, patternInfo);
+        } else if (macros.notation instanceof CompactNotation) {
             // Compact notation uses minGrouping by default since ICU 59
             micros.grouping = Grouper.normalizeType(Grouper.min2(), patternInfo);
         } else {
@@ -154,8 +177,8 @@ public class Worker1 {
         }
 
         // Inner modifier (scientific notation)
-        if (input.notation instanceof ScientificNotation) {
-            chain = ((ScientificNotation) input.notation).withLocaleData(micros.symbols, build, chain);
+        if (macros.notation instanceof ScientificNotation) {
+            chain = ((ScientificNotation) macros.notation).withLocaleData(micros.symbols, safe, chain);
         } else {
             // No inner modifier required
             micros.modInner = ConstantAffixModifier.EMPTY;
@@ -163,39 +186,39 @@ public class Worker1 {
 
         // Middle modifier (patterns, positive/negative, currency symbols, percent)
         // The default middle modifier is weak (thus the false argument).
-        MurkyModifier murkyMod = new MurkyModifier(false);
-        murkyMod.setPatternInfo((input.affixProvider != null) ? input.affixProvider : patternInfo);
-        murkyMod.setPatternAttributes(micros.sign, perMille);
-        if (murkyMod.needsPlurals()) {
+        MutablePatternModifier patternMod = new MutablePatternModifier(false);
+        patternMod.setPatternInfo((macros.affixProvider != null) ? macros.affixProvider : patternInfo);
+        patternMod.setPatternAttributes(micros.sign, perMille);
+        if (patternMod.needsPlurals()) {
             if (rules == null) {
                 // Lazily create PluralRules
-                rules = PluralRules.forLocale(input.loc);
+                rules = PluralRules.forLocale(macros.loc);
             }
-            murkyMod.setSymbols(micros.symbols, currency, unitWidth, rules);
+            patternMod.setSymbols(micros.symbols, currency, unitWidth, rules);
         } else {
-            murkyMod.setSymbols(micros.symbols, currency, unitWidth, null);
+            patternMod.setSymbols(micros.symbols, currency, unitWidth, null);
         }
-        if (build) {
-            chain = murkyMod.createImmutableAndChain(chain);
+        if (safe) {
+            chain = patternMod.createImmutableAndChain(chain);
         } else {
-            chain = murkyMod.addToChain(chain);
+            chain = patternMod.addToChain(chain);
         }
 
         // Outer modifier (CLDR units and currency long names)
         if (longNames != null) {
             if (rules == null) {
                 // Lazily create PluralRules
-                rules = PluralRules.forLocale(input.loc);
+                rules = PluralRules.forLocale(macros.loc);
             }
-            chain = longNames.withLocaleData(rules, build, chain);
+            chain = longNames.withLocaleData(rules, safe, chain);
         } else {
             // No outer modifier required
             micros.modOuter = ConstantAffixModifier.EMPTY;
         }
 
         // Padding strategy
-        if (input.padder != null) {
-            micros.padding = input.padder;
+        if (macros.padder != null) {
+            micros.padding = macros.padder;
         } else {
             micros.padding = Padder.none();
         }
@@ -203,14 +226,14 @@ public class Worker1 {
         // Compact notation
         // NOTE: Compact notation can (but might not) override the middle modifier and rounding.
         // It therefore needs to go at the end of the chain.
-        if (input.notation instanceof CompactNotation) {
+        if (macros.notation instanceof CompactNotation) {
             if (rules == null) {
                 // Lazily create PluralRules
-                rules = PluralRules.forLocale(input.loc);
+                rules = PluralRules.forLocale(macros.loc);
             }
-            CompactType compactType = (input.unit instanceof Currency) ? CompactType.CURRENCY : CompactType.DECIMAL;
-            chain = ((CompactNotation) input.notation).withLocaleData(input.loc, compactType, rules,
-                    build ? murkyMod : null, chain);
+            CompactType compactType = (macros.unit instanceof Currency) ? CompactType.CURRENCY : CompactType.DECIMAL;
+            chain = ((CompactNotation) macros.notation).withLocaleData(macros.loc, compactType, rules,
+                    safe ? patternMod : null, chain);
         }
 
         return chain;
@@ -218,58 +241,67 @@ public class Worker1 {
 
     //////////
 
-    private static int applyStatic(MicroProps micros, FormatQuantity inValue, NumberStringBuilder outString) {
-        inValue.adjustMagnitude(micros.multiplier);
-        micros.rounding.apply(inValue);
+    /**
+     * Synthesizes the output string from a MicroProps and FormatQuantity.
+     *
+     * @param micros
+     *            The MicroProps after the quantity has been consumed. Will not be mutated.
+     * @param quantity
+     *            The FormatQuantity to be rendered. May be mutated.
+     * @param string
+     *            The output string. Will be mutated.
+     */
+    private static void microsToString(MicroProps micros, FormatQuantity quantity, NumberStringBuilder string) {
+        quantity.adjustMagnitude(micros.multiplier);
+        micros.rounding.apply(quantity);
         if (micros.integerWidth.maxInt == -1) {
-            inValue.setIntegerLength(micros.integerWidth.minInt, Integer.MAX_VALUE);
+            quantity.setIntegerLength(micros.integerWidth.minInt, Integer.MAX_VALUE);
         } else {
-            inValue.setIntegerLength(micros.integerWidth.minInt, micros.integerWidth.maxInt);
+            quantity.setIntegerLength(micros.integerWidth.minInt, micros.integerWidth.maxInt);
         }
-        int length = writeNumber(micros, inValue, outString);
+        int length = writeNumber(micros, quantity, string);
         // NOTE: When range formatting is added, these modifiers can bubble up.
         // For now, apply them all here at once.
-        length += micros.padding.applyModsAndMaybePad(micros, outString, 0, length);
-        return length;
+        length += micros.padding.applyModsAndMaybePad(micros, string, 0, length);
     }
 
-    private static int writeNumber(MicroProps micros, FormatQuantity input, NumberStringBuilder string) {
+    private static int writeNumber(MicroProps micros, FormatQuantity quantity, NumberStringBuilder string) {
         int length = 0;
-        if (input.isInfinite()) {
+        if (quantity.isInfinite()) {
             length += string.insert(length, micros.symbols.getInfinity(), NumberFormat.Field.INTEGER);
 
-        } else if (input.isNaN()) {
+        } else if (quantity.isNaN()) {
             length += string.insert(length, micros.symbols.getNaN(), NumberFormat.Field.INTEGER);
 
         } else {
             // Add the integer digits
-            length += writeIntegerDigits(micros, input, string);
+            length += writeIntegerDigits(micros, quantity, string);
 
             // Add the decimal point
-            if (input.getLowerDisplayMagnitude() < 0 || micros.decimal == DecimalMarkDisplay.ALWAYS) {
+            if (quantity.getLowerDisplayMagnitude() < 0 || micros.decimal == DecimalMarkDisplay.ALWAYS) {
                 length += string.insert(length, micros.useCurrency ? micros.symbols.getMonetaryDecimalSeparatorString()
                         : micros.symbols.getDecimalSeparatorString(), NumberFormat.Field.DECIMAL_SEPARATOR);
             }
 
             // Add the fraction digits
-            length += writeFractionDigits(micros, input, string);
+            length += writeFractionDigits(micros, quantity, string);
         }
 
         return length;
     }
 
-    private static int writeIntegerDigits(MicroProps micros, FormatQuantity input, NumberStringBuilder string) {
+    private static int writeIntegerDigits(MicroProps micros, FormatQuantity quantity, NumberStringBuilder string) {
         int length = 0;
-        int integerCount = input.getUpperDisplayMagnitude() + 1;
+        int integerCount = quantity.getUpperDisplayMagnitude() + 1;
         for (int i = 0; i < integerCount; i++) {
             // Add grouping separator
-            if (micros.grouping.groupAtPosition(i, input)) {
+            if (micros.grouping.groupAtPosition(i, quantity)) {
                 length += string.insert(0, micros.useCurrency ? micros.symbols.getMonetaryGroupingSeparatorString()
                         : micros.symbols.getGroupingSeparatorString(), NumberFormat.Field.GROUPING_SEPARATOR);
             }
 
             // Get and append the next digit value
-            byte nextDigit = input.getDigit(i);
+            byte nextDigit = quantity.getDigit(i);
             if (micros.symbols.getCodePointZero() != -1) {
                 length += string.insertCodePoint(0, micros.symbols.getCodePointZero() + nextDigit,
                         NumberFormat.Field.INTEGER);
@@ -281,12 +313,12 @@ public class Worker1 {
         return length;
     }
 
-    private static int writeFractionDigits(MicroProps micros, FormatQuantity input, NumberStringBuilder string) {
+    private static int writeFractionDigits(MicroProps micros, FormatQuantity quantity, NumberStringBuilder string) {
         int length = 0;
-        int fractionCount = -input.getLowerDisplayMagnitude();
+        int fractionCount = -quantity.getLowerDisplayMagnitude();
         for (int i = 0; i < fractionCount; i++) {
             // Get and append the next digit value
-            byte nextDigit = input.getDigit(-i - 1);
+            byte nextDigit = quantity.getDigit(-i - 1);
             if (micros.symbols.getCodePointZero() != -1) {
                 length += string.appendCodePoint(micros.symbols.getCodePointZero() + nextDigit,
                         NumberFormat.Field.FRACTION);
index e135116b0f71629b703556bca70501bf889df68c..8146daffdf5ba5c3b43442ecf162603a2a81c71d 100644 (file)
@@ -4,7 +4,6 @@ package newapi.impl;
 
 import java.util.Objects;
 
-import com.ibm.icu.text.MeasureFormat.FormatWidth;
 import com.ibm.icu.text.PluralRules;
 import com.ibm.icu.util.MeasureUnit;
 import com.ibm.icu.util.ULocale;
@@ -12,10 +11,10 @@ import com.ibm.icu.util.ULocale;
 import newapi.Grouper;
 import newapi.IntegerWidth;
 import newapi.Notation;
-import newapi.NumberFormatter;
-import newapi.Rounder;
 import newapi.NumberFormatter.DecimalMarkDisplay;
 import newapi.NumberFormatter.SignDisplay;
+import newapi.NumberFormatter.UnitWidth;
+import newapi.Rounder;
 
 public class MacroProps implements Cloneable {
   public Notation notation;
@@ -25,7 +24,7 @@ public class MacroProps implements Cloneable {
   public Padder padder;
   public IntegerWidth integerWidth;
   public Object symbols;
-  public FormatWidth unitWidth;
+  public UnitWidth unitWidth;
   public SignDisplay sign;
   public DecimalMarkDisplay decimal;
   public AffixPatternProvider affixProvider; // not in API; for JDK compatibility mode only
index d48c129bde7dd8edd25e79f6326e9ed00f74f9bd..c45220478be64285c3a2eb0a8dce7b38572acc91 100644 (file)
@@ -9,11 +9,12 @@ 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.text.MeasureFormat.FormatWidth;
 import com.ibm.icu.util.MeasureUnit;
 import com.ibm.icu.util.ULocale;
 import com.ibm.icu.util.UResourceBundle;
 
+import newapi.NumberFormatter.UnitWidth;
+
 public class MeasureData {
 
   private static final class ShanesMeasureUnitSink extends UResource.Sink {
@@ -42,14 +43,14 @@ public class MeasureData {
   }
 
   public static Map<StandardPlural, String> getMeasureData(
-      ULocale locale, MeasureUnit unit, FormatWidth width) {
+      ULocale locale, MeasureUnit unit, UnitWidth width) {
     ICUResourceBundle resource =
         (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_UNIT_BASE_NAME, locale);
     StringBuilder key = new StringBuilder();
     key.append("units");
-    if (width == FormatWidth.NARROW) {
+    if (width == UnitWidth.NARROW) {
       key.append("Narrow");
-    } else if (width == FormatWidth.SHORT) {
+    } else if (width == UnitWidth.SHORT) {
       key.append("Short");
     }
     key.append("/");
index dc0aa88bdb22babfb196c35c6b76fdb6e2635975..3c9d9262cc9d716efcd8cdb5a8434a0277410690 100644 (file)
@@ -13,7 +13,7 @@ import newapi.Rounder;
 import newapi.NumberFormatter.DecimalMarkDisplay;
 import newapi.NumberFormatter.SignDisplay;
 
-public class MicroProps implements Cloneable, QuantityChain {
+public class MicroProps implements Cloneable, MicroPropsGenerator {
   // Populated globally:
   public SignDisplay sign;
   public DecimalFormatSymbols symbols;
@@ -41,7 +41,7 @@ public class MicroProps implements Cloneable, QuantityChain {
   }
 
   @Override
-  public MicroProps withQuantity(FormatQuantity quantity) {
+  public MicroProps processQuantity(FormatQuantity quantity) {
     if (immutable) {
       return (MicroProps) this.clone();
     } else {
diff --git a/icu4j/main/classes/core/src/newapi/impl/MicroPropsGenerator.java b/icu4j/main/classes/core/src/newapi/impl/MicroPropsGenerator.java
new file mode 100644 (file)
index 0000000..41822b0
--- /dev/null
@@ -0,0 +1,54 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+
+/**
+ * This interface is used when all number formatting settings, including the locale, are known, except for the quantity
+ * itself. The {@link #processQuantity} method performs the final step in the number processing pipeline: it uses the
+ * quantity to generate a finalized {@link MicroProps}, which can be used to render the number to output.
+ *
+ * <p>
+ * In other words, this interface is used for the parts of number processing that are <em>quantity-dependent</em>.
+ *
+ * <p>
+ * In order to allow for multiple different objects to all mutate the same MicroProps, a "chain" of MicroPropsGenerators
+ * are linked together, and each one is responsible for manipulating a certain quantity-dependent part of the
+ * MicroProps. At the top of the linked list is a base instance of {@link MicroProps} with properties that are not
+ * quantity-dependent. Each element in the linked list calls {@link #processQuantity} on its "parent", then does its
+ * work, and then returns the result.
+ *
+ * <p>
+ * A class implementing MicroPropsGenerator looks something like this:
+ *
+ * <pre>
+ * class Foo implements MicroPropsGenerator {
+ *     private final MicroPropsGenerator parent;
+ *
+ *     public Foo(MicroPropsGenerator parent) {
+ *         this.parent = parent;
+ *     }
+ *
+ *     &#64;Override
+ *     public MicroProps processQuantity(FormatQuantity quantity) {
+ *         MicroProps micros = this.parent.processQuantity(quantity);
+ *         // Perform manipulations on micros and/or quantity
+ *         return micros;
+ *     }
+ * }
+ * </pre>
+ *
+ * @author sffc
+ *
+ */
+public interface MicroPropsGenerator {
+    /**
+     * Considers the given {@link FormatQuantity}, optionally mutates it, and returns a {@link MicroProps}.
+     *
+     * @param quantity
+     *            The quantity for consideration and optional mutation.
+     * @return A MicroProps instance resolved for the quantity.
+     */
+    public MicroProps processQuantity(FormatQuantity quantity);
+}
\ No newline at end of file
diff --git a/icu4j/main/classes/core/src/newapi/impl/MicroPropsMutator.java b/icu4j/main/classes/core/src/newapi/impl/MicroPropsMutator.java
new file mode 100644 (file)
index 0000000..3c01a3b
--- /dev/null
@@ -0,0 +1,13 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+/**
+ * @author sffc
+ *
+ */
+public interface MicroPropsMutator<T> {
+
+    public void mutateMicros(MicroProps micros, T value);
+
+}
index 1c737717c6487d6755a9cfb60f07b644197ab431..40dcffa292a68b52abd16bbeaae2be79378e2780 100644 (file)
@@ -6,10 +6,10 @@ import java.math.BigDecimal;
 
 import com.ibm.icu.impl.number.FormatQuantity;
 
-public class MultiplierImpl implements QuantityChain, Cloneable {
+public class MultiplierImpl implements MicroPropsGenerator, Cloneable {
   final int magnitudeMultiplier;
   final BigDecimal bigDecimalMultiplier;
-  final QuantityChain parent;
+  final MicroPropsGenerator parent;
 
   public MultiplierImpl(int magnitudeMultiplier) {
     this.magnitudeMultiplier = magnitudeMultiplier;
@@ -23,19 +23,19 @@ public class MultiplierImpl implements QuantityChain, Cloneable {
     parent = null;
   }
 
-  private MultiplierImpl(MultiplierImpl base, QuantityChain parent) {
+  private MultiplierImpl(MultiplierImpl base, MicroPropsGenerator parent) {
     this.magnitudeMultiplier = base.magnitudeMultiplier;
     this.bigDecimalMultiplier = base.bigDecimalMultiplier;
     this.parent = parent;
   }
 
-  public QuantityChain copyAndChain(QuantityChain parent) {
+  public MicroPropsGenerator copyAndChain(MicroPropsGenerator parent) {
     return new MultiplierImpl(this, parent);
   }
 
   @Override
-  public MicroProps withQuantity(FormatQuantity quantity) {
-    MicroProps micros = parent.withQuantity(quantity);
+  public MicroProps processQuantity(FormatQuantity quantity) {
+    MicroProps micros = parent.processQuantity(quantity);
     quantity.adjustMagnitude(magnitudeMultiplier);
     if (bigDecimalMultiplier != null) {
       quantity.multiplyBy(bigDecimalMultiplier);
index faacd5059264cb8f61c2b29bdf66b5f91d9e5b3d..91b2099903b558dfd28d6295c6a6c17d66d75356 100644 (file)
@@ -3,9 +3,46 @@
 package newapi.impl;
 
 import com.ibm.icu.impl.number.NumberStringBuilder;
-import com.ibm.icu.impl.number.ThingsNeedingNewHome.PadPosition;
 
 public class Padder {
+    public static final String FALLBACK_PADDING_STRING = "\u0020"; // i.e. a space
+
+    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
+        }
+      }
+    }
 
     private static final Padder NONE = new Padder(null, -1, null);
 
diff --git a/icu4j/main/classes/core/src/newapi/impl/QuantityChain.java b/icu4j/main/classes/core/src/newapi/impl/QuantityChain.java
deleted file mode 100644 (file)
index 4f7a796..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-// © 2017 and later: Unicode, Inc. and others.
-// License & terms of use: http://www.unicode.org/copyright.html#License
-package newapi.impl;
-
-import com.ibm.icu.impl.number.FormatQuantity;
-
-public interface QuantityChain {
-  //QuantityChain addToChain(QuantityChain parent);
-  MicroProps withQuantity(FormatQuantity quantity);
-}
\ No newline at end of file
index 3f8dcea922a531bacf93d08770c6e3ed059a67cf..8deaa788a262b50ec5b6b832f3e98bc719eb678b 100644 (file)
@@ -5,21 +5,21 @@ package newapi.impl;
 import java.math.RoundingMode;
 
 import com.ibm.icu.text.DecimalFormatSymbols;
-import com.ibm.icu.text.MeasureFormat.FormatWidth;
 import com.ibm.icu.text.NumberingSystem;
 import com.ibm.icu.util.Currency;
 import com.ibm.icu.util.Currency.CurrencyUsage;
-import com.ibm.icu.util.Dimensionless;
 import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.NoUnit;
 import com.ibm.icu.util.ULocale;
 
 import newapi.Grouper;
 import newapi.Notation;
 import newapi.NumberFormatter;
-import newapi.Rounder;
-import newapi.UnlocalizedNumberFormatter;
 import newapi.NumberFormatter.DecimalMarkDisplay;
 import newapi.NumberFormatter.SignDisplay;
+import newapi.NumberFormatter.UnitWidth;
+import newapi.Rounder;
+import newapi.UnlocalizedNumberFormatter;
 
 public class demo {
   public static void main(String[] args) {
@@ -31,9 +31,9 @@ public class demo {
             .notation(Notation.engineering().withMinExponentDigits(2))
             .notation(Notation.simple())
             .unit(Currency.getInstance("GBP"))
-            .unit(Dimensionless.PERCENT)
+            .unit(NoUnit.PERCENT)
             .unit(MeasureUnit.CUBIC_METER)
-            .unitWidth(FormatWidth.SHORT)
+            .unitWidth(UnitWidth.SHORT)
             // .rounding(Rounding.fixedSignificantDigits(3))
 //            .rounding(
 //                (BigDecimal input) -> {
index 12f0b43b300118a1cab67c2feac19ec1110f8685..099a2e16b163a21f4afe3e4be1752305bccc167e 100644 (file)
@@ -17,6 +17,7 @@ import java.io.Serializable;
 import java.lang.reflect.Field;
 import java.text.FieldPosition;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -41,6 +42,7 @@ import com.ibm.icu.text.NumberFormat;
 import com.ibm.icu.util.Currency;
 import com.ibm.icu.util.Measure;
 import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.NoUnit;
 import com.ibm.icu.util.TimeUnit;
 import com.ibm.icu.util.TimeUnitAmount;
 import com.ibm.icu.util.ULocale;
@@ -241,7 +243,6 @@ public class MeasureUnitTest extends TestFmwk {
         }
     }
 
-/*
     @Test
     public void testZZZ() {
         // various generateXXX calls go here, see
@@ -254,7 +255,6 @@ public class MeasureUnitTest extends TestFmwk {
         //generateCXXBackwardCompatibilityTest("59"); // for measfmttest.cpp, create TestCompatible59
         //updateJAVAVersions("59"); // for MeasureUnitTest.java, JAVA_VERSIONS
     }
-*/
 
     @Test
     public void TestCompatible53() {
@@ -2000,7 +2000,7 @@ public class MeasureUnitTest extends TestFmwk {
             if (type.equals("currency")
                     || type.equals("compound")
                     || type.equals("coordinate")
-                    || type.equals("dimensionless")) {
+                    || type.equals("none")) {
                 continue;
             }
             for (MeasureUnit unit : MeasureUnit.getAvailable(type)) {
@@ -2147,6 +2147,9 @@ public class MeasureUnitTest extends TestFmwk {
         System.out.println("");
         TreeMap<String, List<MeasureUnit>> allUnits = getAllUnits();
 
+        // Hack: for C++, add NoUnits here, but ignore them when printing the create methods.
+        allUnits.put("none", Arrays.asList(new MeasureUnit[]{NoUnit.BASE, NoUnit.PERCENT, NoUnit.PERMILLE}));
+
         System.out.println("static const int32_t gOffsets[] = {");
         int index = 0;
         for (Map.Entry<String, List<MeasureUnit>> entry : allUnits.entrySet()) {
@@ -2246,7 +2249,7 @@ public class MeasureUnitTest extends TestFmwk {
         for (Map.Entry<String, List<MeasureUnit>> entry : allUnits.entrySet()) {
 
             String type = entry.getKey();
-            if (type.equals("currency")) {
+            if (type.equals("currency") || type.equals("none")) {
                 continue;
             }
             for (MeasureUnit unit : entry.getValue()) {
index 3374fbf498eb5f5cedf42912d8233b6f9f7b37a0..57c559d33735af906220bc3a46f8c1382377361b 100644 (file)
@@ -10,9 +10,8 @@ import org.junit.Test;
 
 import com.ibm.icu.dev.test.TestUtil;
 import com.ibm.icu.impl.number.Parse.ParseMode;
-import com.ibm.icu.impl.number.PatternString;
+import com.ibm.icu.impl.number.PatternAndPropertyUtils;
 import com.ibm.icu.impl.number.Properties;
-import com.ibm.icu.impl.number.ThingsNeedingNewHome.PadPosition;
 import com.ibm.icu.text.DecimalFormatSymbols;
 import com.ibm.icu.text.DecimalFormat_ICU58;
 import com.ibm.icu.util.CurrencyAmount;
@@ -20,6 +19,7 @@ import com.ibm.icu.util.ULocale;
 
 import newapi.LocalizedNumberFormatter;
 import newapi.NumberPropertyMapper;
+import newapi.impl.Padder.PadPosition;
 
 public class NumberFormatDataDrivenTest {
 
@@ -509,8 +509,8 @@ public class NumberFormatDataDrivenTest {
     }
     if (tuple.localizedPattern != null) {
       DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(tuple.locale);
-      String converted = PatternString.convertLocalized(tuple.localizedPattern, symbols, false);
-      PatternString.parseToExistingProperties(converted, properties);
+      String converted = PatternAndPropertyUtils.convertLocalized(tuple.localizedPattern, symbols, false);
+      PatternAndPropertyUtils.parseToExistingProperties(converted, properties);
     }
     if (tuple.lenient != null) {
       properties.setParseMode(tuple.lenient == 0 ? ParseMode.STRICT : ParseMode.LENIENT);
@@ -548,11 +548,11 @@ public class NumberFormatDataDrivenTest {
           String pattern = (tuple.pattern == null) ? "0" : tuple.pattern;
           ULocale locale = (tuple.locale == null) ? ULocale.ENGLISH : tuple.locale;
           Properties properties =
-              PatternString.parseToProperties(
+              PatternAndPropertyUtils.parseToProperties(
                   pattern,
                   tuple.currency != null
-                      ? PatternString.IGNORE_ROUNDING_ALWAYS
-                      : PatternString.IGNORE_ROUNDING_NEVER);
+                      ? PatternAndPropertyUtils.IGNORE_ROUNDING_ALWAYS
+                      : PatternAndPropertyUtils.IGNORE_ROUNDING_NEVER);
           propertiesFromTuple(tuple, properties);
           DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale);
           LocalizedNumberFormatter fmt = NumberPropertyMapper.create(properties, symbols).locale(locale);
index 697002ad2e25759e82785d0579aeb691314c2abd..20cb8f1ea047f8de7966e9f15c125c156a4de94e 100644 (file)
@@ -5188,7 +5188,7 @@ public class NumberFormatTest extends TestFmwk {
     }
 
     @Test
-    public void Test13113() {
+    public void Test13113_MalformedPatterns() {
         String[][] cases = {
                 {"'", "quoted literal"},
                 {"ab#c'd", "quoted literal"},
@@ -5197,7 +5197,8 @@ public class NumberFormatTest extends TestFmwk {
                 {".#0", "0 cannot follow #"},
                 {"@0", "Cannot mix @ and 0"},
                 {"0@", "Cannot mix 0 and @"},
-                {"#x#", "unquoted special character"}
+                {"#x#", "unquoted special character"},
+                {"@#@", "# inside of a run of @"},
         };
         for (String[] cas : cases) {
             try {
index c0552fda28b2b6ba28c10aec57f1a94f1d2463cd..2bb88e1f47bcadce5db28ec4c5b1e6993e32c528 100644 (file)
@@ -15,6 +15,42 @@ import com.ibm.icu.util.ULocale;
 
 public class AffixPatternUtilsTest {
 
+    private static final SymbolProvider DEFAULT_SYMBOL_PROVIDER =
+        new SymbolProvider() {
+          // ar_SA has an interesting percent sign and various Arabic letter marks
+          private final DecimalFormatSymbols SYMBOLS =
+              DecimalFormatSymbols.getInstance(new ULocale("ar_SA"));
+
+          @Override
+          public CharSequence getSymbol(int type) {
+            switch (type) {
+              case AffixPatternUtils.TYPE_MINUS_SIGN:
+                return "−";
+              case AffixPatternUtils.TYPE_PLUS_SIGN:
+                return SYMBOLS.getPlusSignString();
+              case AffixPatternUtils.TYPE_PERCENT:
+                return SYMBOLS.getPercentString();
+              case AffixPatternUtils.TYPE_PERMILLE:
+                return SYMBOLS.getPerMillString();
+              case AffixPatternUtils.TYPE_CURRENCY_SINGLE:
+                return "$";
+              case AffixPatternUtils.TYPE_CURRENCY_DOUBLE:
+                return "XXX";
+              case AffixPatternUtils.TYPE_CURRENCY_TRIPLE:
+                return "long name";
+              case AffixPatternUtils.TYPE_CURRENCY_QUAD:
+                return "\uFFFD";
+              case AffixPatternUtils.TYPE_CURRENCY_QUINT:
+                // TODO: Add support for narrow currency symbols here.
+                return "\uFFFD";
+              case AffixPatternUtils.TYPE_CURRENCY_OVERFLOW:
+                return "\uFFFD";
+              default:
+                throw new AssertionError();
+            }
+          }
+        };
+
   @Test
   public void testEscape() {
     Object[][] cases = {
@@ -183,42 +219,6 @@ public class AffixPatternUtilsTest {
     assertEquals("Symbol provider into middle", "abcd123efg", sb.toString());
   }
 
-  private static final SymbolProvider DEFAULT_SYMBOL_PROVIDER =
-      new SymbolProvider() {
-        // ar_SA has an interesting percent sign and various Arabic letter marks
-        private final DecimalFormatSymbols SYMBOLS =
-            DecimalFormatSymbols.getInstance(new ULocale("ar_SA"));
-
-        @Override
-        public CharSequence getSymbol(int type) {
-          switch (type) {
-            case AffixPatternUtils.TYPE_MINUS_SIGN:
-              return "−";
-            case AffixPatternUtils.TYPE_PLUS_SIGN:
-              return SYMBOLS.getPlusSignString();
-            case AffixPatternUtils.TYPE_PERCENT:
-              return SYMBOLS.getPercentString();
-            case AffixPatternUtils.TYPE_PERMILLE:
-              return SYMBOLS.getPerMillString();
-            case AffixPatternUtils.TYPE_CURRENCY_SINGLE:
-              return "$";
-            case AffixPatternUtils.TYPE_CURRENCY_DOUBLE:
-              return "XXX";
-            case AffixPatternUtils.TYPE_CURRENCY_TRIPLE:
-              return "long name";
-            case AffixPatternUtils.TYPE_CURRENCY_QUAD:
-              return "\uFFFD";
-            case AffixPatternUtils.TYPE_CURRENCY_QUINT:
-              // TODO: Add support for narrow currency symbols here.
-              return "\uFFFD";
-            case AffixPatternUtils.TYPE_CURRENCY_OVERFLOW:
-              return "\uFFFD";
-            default:
-              throw new AssertionError();
-          }
-        }
-      };
-
   private static String unescapeWithDefaults(String input) {
     NumberStringBuilder nsb = new NumberStringBuilder();
     AffixPatternUtils.unescape(input, nsb, 0, DEFAULT_SYMBOL_PROVIDER);
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/ModifierTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/ModifierTest.java
new file mode 100644 (file)
index 0000000..54b0e53
--- /dev/null
@@ -0,0 +1,152 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package com.ibm.icu.dev.test.number;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+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.modifiers.ConstantAffixModifier;
+import com.ibm.icu.impl.number.modifiers.ConstantMultiFieldModifier;
+import com.ibm.icu.impl.number.modifiers.CurrencySpacingEnabledModifier;
+import com.ibm.icu.impl.number.modifiers.SimpleModifier;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.NumberFormat;
+import com.ibm.icu.util.ULocale;
+
+public class ModifierTest {
+    @Test
+    public void testConstantAffixModifier() {
+        assertModifierEquals(ConstantAffixModifier.EMPTY, 0, false, "|", "n");
+
+        Modifier mod1 = new ConstantAffixModifier("a", "b", NumberFormat.Field.PERCENT, true);
+        assertModifierEquals(mod1, 1, true, "a|b", "%n%");
+    }
+
+    @Test
+    public void testConstantMultiFieldModifier() {
+        NumberStringBuilder prefix = new NumberStringBuilder();
+        NumberStringBuilder suffix = new NumberStringBuilder();
+        Modifier mod1 = new ConstantMultiFieldModifier(prefix, suffix, true);
+        assertModifierEquals(mod1, 0, true, "|", "n");
+
+        prefix.append("a", NumberFormat.Field.PERCENT);
+        suffix.append("b", NumberFormat.Field.CURRENCY);
+        Modifier mod2 = new ConstantMultiFieldModifier(prefix, suffix, true);
+        assertModifierEquals(mod2, 1, true, "a|b", "%n$");
+
+        // Make sure the first modifier is still the same (that it stayed constant)
+        assertModifierEquals(mod1, 0, true, "|", "n");
+    }
+
+    @Test
+    public void testSimpleModifier() {
+        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 } };
+        int[] prefixLens = { 0, 1, 2, 0, 4 };
+        String[][] expectedCharFields = { { "|", "n" }, { "X|Y", "%n%" }, { "XX|YYY", "%%n%%%" }, { "|YY", "n%%" },
+                { "XXXX|", "%%%%n" } };
+        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++) {
+            String pattern = patterns[i];
+            String compiledPattern = SimpleFormatterImpl
+                    .compileToStringMinMaxArguments(pattern, new StringBuilder(), 1, 1);
+            Modifier mod = new SimpleModifier(compiledPattern, NumberFormat.Field.PERCENT, false);
+            assertModifierEquals(mod, prefixLens[i], false, expectedCharFields[i][0], expectedCharFields[i][1]);
+
+            // Test strange insertion positions
+            for (int j = 0; j < outputs.length; j++) {
+                NumberStringBuilder output = new NumberStringBuilder();
+                output.append((String) outputs[j][0], null);
+                mod.apply(output, (Integer) outputs[j][1], (Integer) outputs[j][2]);
+                String expected = expecteds[j][i];
+                String actual = output.toString();
+                assertEquals(expected, actual);
+            }
+        }
+    }
+
+    @Test
+    public void testCurrencySpacingEnabledModifier() {
+        DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(ULocale.ENGLISH);
+        NumberStringBuilder prefix = new NumberStringBuilder();
+        NumberStringBuilder suffix = new NumberStringBuilder();
+        Modifier mod1 = new CurrencySpacingEnabledModifier(prefix, suffix, true, symbols);
+        assertModifierEquals(mod1, 0, true, "|", "n");
+
+        prefix.append("USD", NumberFormat.Field.CURRENCY);
+        Modifier mod2 = new CurrencySpacingEnabledModifier(prefix, suffix, true, symbols);
+        assertModifierEquals(mod2, 3, true, "USD|", "$$$n");
+
+        // Test the default currency spacing rules
+        NumberStringBuilder sb = new NumberStringBuilder();
+        sb.append("123", NumberFormat.Field.INTEGER);
+        NumberStringBuilder sb1 = new NumberStringBuilder(sb);
+        assertModifierEquals(mod2, sb1, 3, true, "USD\u00A0123", "$$$niii");
+
+        // Compare with the unsafe code path
+        NumberStringBuilder sb2 = new NumberStringBuilder(sb);
+        sb2.insert(0, "USD", NumberFormat.Field.CURRENCY);
+        CurrencySpacingEnabledModifier.applyCurrencySpacing(sb2, 0, 3, 6, 0, symbols);
+        assertTrue(sb1.toDebugString() + " vs " + sb2.toDebugString(), sb1.contentEquals(sb2));
+
+        // Test custom patterns
+        // The following line means that the last char of the number should be a | (rather than a digit)
+        symbols.setPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_SURROUNDING_MATCH, true, "[|]");
+        suffix.append("XYZ", NumberFormat.Field.CURRENCY);
+        Modifier mod3 = new CurrencySpacingEnabledModifier(prefix, suffix, true, symbols);
+        assertModifierEquals(mod3, 3, true, "USD|\u00A0XYZ", "$$$nn$$$");
+    }
+
+    @Test
+    public void testCurrencySpacingPatternStability() {
+        // This test checks for stability of the currency spacing patterns in CLDR.
+        // For efficiency, ICU caches the most common currency spacing UnicodeSets.
+        // If this test starts failing, please update the method #getUnicodeSet() in
+        // BOTH CurrencySpacingEnabledModifier.java AND in C++.
+        DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(new ULocale("en-US"));
+        assertEquals(
+                "[:^S:]",
+                dfs.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_CURRENCY_MATCH, true));
+        assertEquals(
+                "[:^S:]",
+                dfs.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_CURRENCY_MATCH, false));
+        assertEquals(
+                "[:digit:]",
+                dfs.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_SURROUNDING_MATCH, true));
+        assertEquals(
+                "[:digit:]",
+                dfs.getPatternForCurrencySpacing(DecimalFormatSymbols.CURRENCY_SPC_SURROUNDING_MATCH, false));
+    }
+
+    private void assertModifierEquals(
+            Modifier mod,
+            int expectedPrefixLength,
+            boolean expectedStrong,
+            String expectedChars,
+            String expectedFields) {
+        NumberStringBuilder sb = new NumberStringBuilder();
+        sb.appendCodePoint('|', null);
+        assertModifierEquals(mod, sb, expectedPrefixLength, expectedStrong, expectedChars, expectedFields);
+    }
+
+    private void assertModifierEquals(
+            Modifier mod,
+            NumberStringBuilder sb,
+            int expectedPrefixLength,
+            boolean expectedStrong,
+            String expectedChars,
+            String expectedFields) {
+        mod.apply(sb, 0, sb.length());
+        assertEquals("Prefix length on " + sb, expectedPrefixLength, mod.getPrefixLength());
+        assertEquals("Strong on " + sb, expectedStrong, mod.isStrong());
+        assertEquals("<NumberStringBuilder [" + expectedChars + "] [" + expectedFields + "]>", sb.toDebugString());
+    }
+}
index 686bd4a4c5c854b26af2f69ccd2a4bda0c8d7061..0bc996b5a3e447ab9c70c4deef861c948dce50e3 100644 (file)
@@ -6,27 +6,27 @@ import static org.junit.Assert.assertEquals;
 
 import org.junit.Test;
 
-import com.ibm.icu.impl.number.LdmlPatternInfo;
+import com.ibm.icu.impl.number.PatternParser;
 import com.ibm.icu.impl.number.NumberStringBuilder;
 import com.ibm.icu.text.DecimalFormatSymbols;
-import com.ibm.icu.text.MeasureFormat.FormatWidth;
 import com.ibm.icu.util.Currency;
 import com.ibm.icu.util.ULocale;
 
-import newapi.MurkyModifier;
+import newapi.MutablePatternModifier;
 import newapi.NumberFormatter.SignDisplay;
+import newapi.NumberFormatter.UnitWidth;
 
 public class MurkyModifierTest {
 
   @Test
   public void basic() {
-    MurkyModifier murky = new MurkyModifier(false);
-    murky.setPatternInfo(LdmlPatternInfo.parse("a0b"));
+    MutablePatternModifier murky = new MutablePatternModifier(false);
+    murky.setPatternInfo(PatternParser.parse("a0b"));
     murky.setPatternAttributes(SignDisplay.AUTO, false);
     murky.setSymbols(
         DecimalFormatSymbols.getInstance(ULocale.ENGLISH),
         Currency.getInstance("USD"),
-        FormatWidth.SHORT,
+        UnitWidth.SHORT,
         null);
     murky.setNumberProperties(false, null);
     assertEquals("a", getPrefix(murky));
@@ -41,7 +41,7 @@ public class MurkyModifierTest {
     assertEquals("a", getPrefix(murky));
     assertEquals("b", getSuffix(murky));
 
-    murky.setPatternInfo(LdmlPatternInfo.parse("a0b;c-0d"));
+    murky.setPatternInfo(PatternParser.parse("a0b;c-0d"));
     murky.setPatternAttributes(SignDisplay.AUTO, false);
     murky.setNumberProperties(false, null);
     assertEquals("a", getPrefix(murky));
@@ -57,13 +57,13 @@ public class MurkyModifierTest {
     assertEquals("d", getSuffix(murky));
   }
 
-  private static String getPrefix(MurkyModifier murky) {
+  private static String getPrefix(MutablePatternModifier murky) {
       NumberStringBuilder nsb = new NumberStringBuilder();
       murky.apply(nsb, 0, 0);
       return nsb.subSequence(0, murky.getPrefixLength()).toString();
   }
 
-  private static String getSuffix(MurkyModifier murky) {
+  private static String getSuffix(MutablePatternModifier murky) {
       NumberStringBuilder nsb = new NumberStringBuilder();
       murky.apply(nsb, 0, 0);
       return nsb.subSequence(murky.getPrefixLength(), nsb.length()).toString();
index 5ba0e95756df03d750ed81c483a46ff2ae459520..460aae4c1cb746da45c4833ee8281844437a1463 100644 (file)
@@ -11,16 +11,14 @@ import java.util.Locale;
 import org.junit.Ignore;
 import org.junit.Test;
 
-import com.ibm.icu.impl.number.ThingsNeedingNewHome.PadPosition;
 import com.ibm.icu.text.DecimalFormatSymbols;
-import com.ibm.icu.text.MeasureFormat.FormatWidth;
 import com.ibm.icu.text.NumberingSystem;
 import com.ibm.icu.util.Currency;
 import com.ibm.icu.util.Currency.CurrencyUsage;
 import com.ibm.icu.util.CurrencyAmount;
-import com.ibm.icu.util.Dimensionless;
 import com.ibm.icu.util.Measure;
 import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.NoUnit;
 import com.ibm.icu.util.ULocale;
 
 import newapi.FormattedNumber;
@@ -31,1194 +29,1296 @@ import newapi.Notation;
 import newapi.NumberFormatter;
 import newapi.NumberFormatter.DecimalMarkDisplay;
 import newapi.NumberFormatter.SignDisplay;
-import newapi.impl.Padder;
+import newapi.NumberFormatter.UnitWidth;
 import newapi.NumberPropertyMapper;
 import newapi.Rounder;
 import newapi.UnlocalizedNumberFormatter;
+import newapi.impl.Padder;
+import newapi.impl.Padder.PadPosition;
 
 public class NumberFormatterTest {
 
-  private static final Currency USD = Currency.getInstance("USD");
-  private static final Currency GBP = Currency.getInstance("GBP");
-  private static final Currency CZK = Currency.getInstance("CZK");
-
-  @Test
-  public void notationSimple() {
-    assertFormatDescending(
-        "Basic",
-        "",
-        NumberFormatter.with(),
-        ULocale.ENGLISH,
-        "87,650",
-        "8,765",
-        "876.5",
-        "87.65",
-        "8.765",
-        "0.8765",
-        "0.08765",
-        "0.008765",
-        "0");
-
-    assertFormatSingle(
-        "Basic with Negative Sign",
-        "",
-        NumberFormatter.with(),
-        ULocale.ENGLISH,
-        -9876543.21,
-        "-9,876,543.21");
-  }
-
-  @Test
-  public void notationScientific() {
-    assertFormatDescending(
-        "Scientific",
-        "E",
-        NumberFormatter.with().notation(Notation.scientific()),
-        ULocale.ENGLISH,
-        "8.765E4",
-        "8.765E3",
-        "8.765E2",
-        "8.765E1",
-        "8.765E0",
-        "8.765E-1",
-        "8.765E-2",
-        "8.765E-3",
-        "0E0");
-
-    assertFormatDescending(
-        "Engineering",
-        "E3",
-        NumberFormatter.with().notation(Notation.engineering()),
-        ULocale.ENGLISH,
-        "87.65E3",
-        "8.765E3",
-        "876.5E0",
-        "87.65E0",
-        "8.765E0",
-        "876.5E-3",
-        "87.65E-3",
-        "8.765E-3",
-        "0E0");
-
-    assertFormatDescending(
-        "Scientific sign always shown",
-        "E+",
-        NumberFormatter.with()
-            .notation(Notation.scientific().withExponentSignDisplay(SignDisplay.ALWAYS)),
-        ULocale.ENGLISH,
-        "8.765E+4",
-        "8.765E+3",
-        "8.765E+2",
-        "8.765E+1",
-        "8.765E+0",
-        "8.765E-1",
-        "8.765E-2",
-        "8.765E-3",
-        "0E+0");
-
-    assertFormatDescending(
-        "Scientific min exponent digits",
-        "E00",
-        NumberFormatter.with().notation(Notation.scientific().withMinExponentDigits(2)),
-        ULocale.ENGLISH,
-        "8.765E04",
-        "8.765E03",
-        "8.765E02",
-        "8.765E01",
-        "8.765E00",
-        "8.765E-01",
-        "8.765E-02",
-        "8.765E-03",
-        "0E00");
-
-    assertFormatSingle(
-        "Scientific Negative",
-        "E",
-        NumberFormatter.with().notation(Notation.scientific()),
-        ULocale.ENGLISH,
-        -1000000,
-        "-1E6");
-  }
-
-  @Test
-  public void notationCompact() {
-    assertFormatDescending(
-        "Compact Short",
-        "C",
-        NumberFormatter.with().notation(Notation.compactShort()),
-        ULocale.ENGLISH,
-        "88K",
-        "8.8K",
-        "876",
-        "88",
-        "8.8",
-        "0.88",
-        "0.088",
-        "0.0088",
-        "0");
-
-    assertFormatDescending(
-        "Compact Long",
-        "CC",
-        NumberFormatter.with().notation(Notation.compactLong()),
-        ULocale.ENGLISH,
-        "88 thousand",
-        "8.8 thousand",
-        "876",
-        "88",
-        "8.8",
-        "0.88",
-        "0.088",
-        "0.0088",
-        "0");
-
-    assertFormatDescending(
-        "Compact Short Currency",
-        "C $USD",
-        NumberFormatter.with().notation(Notation.compactShort()).unit(USD),
-        ULocale.ENGLISH,
-        "$88K",
-        "$8.8K",
-        "$876",
-        "$88",
-        "$8.8",
-        "$0.88",
-        "$0.088",
-        "$0.0088",
-        "$0");
-
-    // Note: Most locales don't have compact long currency, so this currently falls back to short.
-    assertFormatDescending(
-        "Compact Long Currency",
-        "CC $USD",
-        NumberFormatter.with().notation(Notation.compactLong()).unit(USD),
-        ULocale.ENGLISH,
-        "$88K",
-        "$8.8K",
-        "$876",
-        "$88",
-        "$8.8",
-        "$0.88",
-        "$0.088",
-        "$0.0088",
-        "$0");
-
-    assertFormatSingle(
-        "Compact Plural One",
-        "CC",
-        NumberFormatter.with().notation(Notation.compactLong()),
-        ULocale.forLanguageTag("es"),
-        1000000,
-        "1 millón");
-
-    assertFormatSingle(
-        "Compact Plural Other",
-        "CC",
-        NumberFormatter.with().notation(Notation.compactLong()),
-        ULocale.forLanguageTag("es"),
-        2000000,
-        "2 millones");
-
-    assertFormatSingle(
-        "Compact with Negative Sign",
-        "C",
-        NumberFormatter.with().notation(Notation.compactShort()),
-        ULocale.ENGLISH,
-        -9876543.21,
-        "-9.9M");
-  }
-
-  @Test
-  public void unitMeasure() {
-    assertFormatDescending(
-        "Meters Short",
-        "U:length:meter",
-        NumberFormatter.with().unit(MeasureUnit.METER),
-        ULocale.ENGLISH,
-        "87,650 m",
-        "8,765 m",
-        "876.5 m",
-        "87.65 m",
-        "8.765 m",
-        "0.8765 m",
-        "0.08765 m",
-        "0.008765 m",
-        "0 m");
-
-    assertFormatDescending(
-        "Meters Long",
-        "U:length:meter unit-width=WIDE",
-        NumberFormatter.with().unit(MeasureUnit.METER).unitWidth(FormatWidth.WIDE),
-        ULocale.ENGLISH,
-        "87,650 meters",
-        "8,765 meters",
-        "876.5 meters",
-        "87.65 meters",
-        "8.765 meters",
-        "0.8765 meters",
-        "0.08765 meters",
-        "0.008765 meters",
-        "0 meters");
-
-    assertFormatDescending(
-        "Compact Meters Long",
-        "CC U:length:meter unit-width=WIDE",
-        NumberFormatter.with()
-            .notation(Notation.compactLong())
-            .unit(MeasureUnit.METER)
-            .unitWidth(FormatWidth.WIDE),
-        ULocale.ENGLISH,
-        "88 thousand meters",
-        "8.8 thousand meters",
-        "876 meters",
-        "88 meters",
-        "8.8 meters",
-        "0.88 meters",
-        "0.088 meters",
-        "0.0088 meters",
-        "0 meters");
-
-    assertFormatSingleMeasure(
-        "Meters with Measure Input",
-        "unit-width=WIDE",
-        NumberFormatter.with().unitWidth(FormatWidth.WIDE),
-        ULocale.ENGLISH,
-        new Measure(5.43, MeasureUnit.METER),
-        "5.43 meters");
-
-    assertFormatSingle(
-        "Meters with Negative Sign",
-        "U:length:meter",
-        NumberFormatter.with().unit(MeasureUnit.METER),
-        ULocale.ENGLISH,
-        -9876543.21,
-        "-9,876,543.21 m");
-  }
-
-  @Test
-  public void unitCurrency() {
-    assertFormatDescending(
-        "Currency",
-        "$GBP",
-        NumberFormatter.with().unit(GBP),
-        ULocale.ENGLISH,
-        "£87,650.00",
-        "£8,765.00",
-        "£876.50",
-        "£87.65",
-        "£8.76",
-        "£0.88",
-        "£0.09",
-        "£0.01",
-        "£0.00");
-
-    assertFormatDescending(
-        "Currency ISO",
-        "$GBP unit-width=SHORT",
-        NumberFormatter.with().unit(GBP).unitWidth(FormatWidth.SHORT),
-        ULocale.ENGLISH,
-        "GBP 87,650.00",
-        "GBP 8,765.00",
-        "GBP 876.50",
-        "GBP 87.65",
-        "GBP 8.76",
-        "GBP 0.88",
-        "GBP 0.09",
-        "GBP 0.01",
-        "GBP 0.00");
-
-    assertFormatDescending(
-        "Currency Long Name",
-        "$GBP unit-width=WIDE",
-        NumberFormatter.with().unit(GBP).unitWidth(FormatWidth.WIDE),
-        ULocale.ENGLISH,
-        "87,650.00 British pounds",
-        "8,765.00 British pounds",
-        "876.50 British pounds",
-        "87.65 British pounds",
-        "8.76 British pounds",
-        "0.88 British pounds",
-        "0.09 British pounds",
-        "0.01 British pounds",
-        "0.00 British pounds");
-
-    assertFormatSingleMeasure(
-        "Currency with CurrencyAmount Input",
-        "",
-        NumberFormatter.with(),
-        ULocale.ENGLISH,
-        new CurrencyAmount(5.43, GBP),
-        "£5.43");
-
-    assertFormatSingle(
-        "Currency Long Name from Pattern Syntax",
-        "$GBP F0 grouping=none integer-width=1- symbols=loc:en_US sign=AUTO decimal=AUTO",
-        NumberPropertyMapper.create("0 ¤¤¤", DecimalFormatSymbols.getInstance()).unit(GBP),
-        ULocale.ENGLISH,
-        1234567.89,
-        "1234568 British pounds");
-
-    assertFormatSingle(
-        "Currency with Negative Sign",
-        "$GBP",
-        NumberFormatter.with().unit(GBP),
-        ULocale.ENGLISH,
-        -9876543.21,
-        "-£9,876,543.21");
-  }
-
-  @Test
-  public void unitPercent() {
-    assertFormatDescending(
-        "Percent",
-        "%",
-        NumberFormatter.with().unit(Dimensionless.PERCENT),
-        ULocale.ENGLISH,
-        "8,765,000%",
-        "876,500%",
-        "87,650%",
-        "8,765%",
-        "876.5%",
-        "87.65%",
-        "8.765%",
-        "0.8765%",
-        "0%");
-
-    assertFormatDescending(
-        "Permille",
-        "%%",
-        NumberFormatter.with().unit(Dimensionless.PERMILLE),
-        ULocale.ENGLISH,
-        "87,650,000‰",
-        "8,765,000‰",
-        "876,500‰",
-        "87,650‰",
-        "8,765‰",
-        "876.5‰",
-        "87.65‰",
-        "8.765‰",
-        "0‰");
-
-    assertFormatSingle(
-        "Percent with Negative Sign",
-        "%",
-        NumberFormatter.with().unit(Dimensionless.PERCENT),
-        ULocale.ENGLISH,
-        -0.987654321,
-        "-98.7654321%");
-  }
-
-  @Test
-  public void roundingFraction() {
-    assertFormatDescending(
-        "Integer",
-        "F0",
-        NumberFormatter.with().rounding(Rounder.integer()),
-        ULocale.ENGLISH,
-        "87,650",
-        "8,765",
-        "876",
-        "88",
-        "9",
-        "1",
-        "0",
-        "0",
-        "0");
-
-    assertFormatDescending(
-        "Fixed Fraction",
-        "F3",
-        NumberFormatter.with().rounding(Rounder.fixedFraction(3)),
-        ULocale.ENGLISH,
-        "87,650.000",
-        "8,765.000",
-        "876.500",
-        "87.650",
-        "8.765",
-        "0.876",
-        "0.088",
-        "0.009",
-        "0.000");
-
-    assertFormatDescending(
-        "Min Fraction",
-        "F1-",
-        NumberFormatter.with().rounding(Rounder.minFraction(1)),
-        ULocale.ENGLISH,
-        "87,650.0",
-        "8,765.0",
-        "876.5",
-        "87.65",
-        "8.765",
-        "0.8765",
-        "0.08765",
-        "0.008765",
-        "0.0");
-
-    assertFormatDescending(
-        "Max Fraction",
-        "F-1",
-        NumberFormatter.with().rounding(Rounder.maxFraction(1)),
-        ULocale.ENGLISH,
-        "87,650",
-        "8,765",
-        "876.5",
-        "87.6",
-        "8.8",
-        "0.9",
-        "0.1",
-        "0",
-        "0");
-
-    assertFormatDescending(
-        "Min/Max Fraction",
-        "F1-3",
-        NumberFormatter.with().rounding(Rounder.minMaxFraction(1, 3)),
-        ULocale.ENGLISH,
-        "87,650.0",
-        "8,765.0",
-        "876.5",
-        "87.65",
-        "8.765",
-        "0.876",
-        "0.088",
-        "0.009",
-        "0.0");
-  }
-
-  @Test
-  public void roundingFigures() {
-    assertFormatSingle(
-        "Fixed Significant",
-        "S3",
-        NumberFormatter.with().rounding(Rounder.fixedFigures(3)),
-        ULocale.ENGLISH,
-        -98,
-        "-98.0");
-
-    assertFormatSingle(
-        "Fixed Significant Rounding",
-        "S3",
-        NumberFormatter.with().rounding(Rounder.fixedFigures(3)),
-        ULocale.ENGLISH,
-        -98.7654321,
-        "-98.8");
-
-    assertFormatSingle(
-        "Fixed Significant Zero",
-        "S3",
-        NumberFormatter.with().rounding(Rounder.fixedFigures(3)),
-        ULocale.ENGLISH,
-        0,
-        "0.00");
-
-    assertFormatSingle(
-        "Min Significant",
-        "S2-",
-        NumberFormatter.with().rounding(Rounder.minFigures(2)),
-        ULocale.ENGLISH,
-        -9,
-        "-9.0");
-
-    assertFormatSingle(
-        "Max Significant",
-        "S-4",
-        NumberFormatter.with().rounding(Rounder.maxFigures(4)),
-        ULocale.ENGLISH,
-        98.7654321,
-        "98.77");
-
-    assertFormatSingle(
-        "Min/Max Significant",
-        "S3-4",
-        NumberFormatter.with().rounding(Rounder.minMaxFigures(3, 4)),
-        ULocale.ENGLISH,
-        9.99999,
-        "10.0");
-  }
-
-  @Test
-  public void roundingFractionFigures() {
-    assertFormatDescending(
-        "Basic Significant", // for comparison
-        "S-2",
-        NumberFormatter.with().rounding(Rounder.maxFigures(2)),
-        ULocale.ENGLISH,
-        "88,000",
-        "8,800",
-        "880",
-        "88",
-        "8.8",
-        "0.88",
-        "0.088",
-        "0.0088",
-        "0");
-
-    assertFormatDescending(
-        "FracSig minMaxFrac minSig",
-        "F1-2>3",
-        NumberFormatter.with().rounding(Rounder.minMaxFraction(1, 2).withMinFigures(3)),
-        ULocale.ENGLISH,
-        "87,650.0",
-        "8,765.0",
-        "876.5",
-        "87.65",
-        "8.76",
-        "0.876", // minSig beats maxFrac
-        "0.0876", // minSig beats maxFrac
-        "0.00876", // minSig beats maxFrac
-        "0.0");
-
-    assertFormatDescending(
-        "FracSig minMaxFrac maxSig A",
-        "F1-3<2",
-        NumberFormatter.with().rounding(Rounder.minMaxFraction(1, 3).withMaxFigures(2)),
-        ULocale.ENGLISH,
-        "88,000.0", // maxSig beats maxFrac
-        "8,800.0", // maxSig beats maxFrac
-        "880.0", // maxSig beats maxFrac
-        "88.0", // maxSig beats maxFrac
-        "8.8", // maxSig beats maxFrac
-        "0.88", // maxSig beats maxFrac
-        "0.088",
-        "0.009",
-        "0.0");
-
-    assertFormatDescending(
-        "FracSig minMaxFrac maxSig B",
-        "F2<2",
-        NumberFormatter.with().rounding(Rounder.fixedFraction(2).withMaxFigures(2)),
-        ULocale.ENGLISH,
-        "88,000.00", // maxSig beats maxFrac
-        "8,800.00", // maxSig beats maxFrac
-        "880.00", // maxSig beats maxFrac
-        "88.00", // maxSig beats maxFrac
-        "8.80", // maxSig beats maxFrac
-        "0.88",
-        "0.09",
-        "0.01",
-        "0.00");
-  }
-
-  @Test
-  public void roundingOther() {
-    assertFormatDescending(
-        "Rounding None",
-        "Y",
-        NumberFormatter.with().rounding(Rounder.none()),
-        ULocale.ENGLISH,
-        "87,650",
-        "8,765",
-        "876.5",
-        "87.65",
-        "8.765",
-        "0.8765",
-        "0.08765",
-        "0.008765",
-        "0");
-
-    assertFormatDescending(
-        "Increment",
-        "M0.5",
-        NumberFormatter.with().rounding(Rounder.increment(BigDecimal.valueOf(0.5))),
-        ULocale.ENGLISH,
-        "87,650.0",
-        "8,765.0",
-        "876.5",
-        "87.5",
-        "9.0",
-        "1.0",
-        "0.0",
-        "0.0",
-        "0.0");
-
-    assertFormatDescending(
-        "Currency Standard",
-        "$CZK GSTANDARD",
-        NumberFormatter.with().rounding(Rounder.currency(CurrencyUsage.STANDARD)).unit(CZK),
-        ULocale.ENGLISH,
-        "CZK 87,650.00",
-        "CZK 8,765.00",
-        "CZK 876.50",
-        "CZK 87.65",
-        "CZK 8.76",
-        "CZK 0.88",
-        "CZK 0.09",
-        "CZK 0.01",
-        "CZK 0.00");
-
-    assertFormatDescending(
-        "Currency Cash",
-        "$CZK GCASH",
-        NumberFormatter.with().rounding(Rounder.currency(CurrencyUsage.CASH)).unit(CZK),
-        ULocale.ENGLISH,
-        "CZK 87,650",
-        "CZK 8,765",
-        "CZK 876",
-        "CZK 88",
-        "CZK 9",
-        "CZK 1",
-        "CZK 0",
-        "CZK 0",
-        "CZK 0");
-
-    assertFormatDescending(
-        "Currency not in top-level fluent chain",
-        "F0",
-        NumberFormatter.with().rounding(Rounder.currency(CurrencyUsage.CASH).withCurrency(CZK)),
-        ULocale.ENGLISH,
-        "87,650",
-        "8,765",
-        "876",
-        "88",
-        "9",
-        "1",
-        "0",
-        "0",
-        "0");
-  }
-
-  @Test
-  public void grouping() {
-    // Dimensionless.PERMILLE multiplies all the number by 10^3 (good for testing grouping).
-    // Note that en-US is already performed in the unitPercent() function.
-    assertFormatDescending(
-        "Indic Grouping",
-        "%% grouping=defaults",
-        NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouper.defaults()),
-        new ULocale("en-IN"),
-        "8,76,50,000‰",
-        "87,65,000‰",
-        "8,76,500‰",
-        "87,650‰",
-        "8,765‰",
-        "876.5‰",
-        "87.65‰",
-        "8.765‰",
-        "0‰");
-
-    assertFormatDescending(
-        "Western Grouping, Min 2",
-        "%% grouping=min2",
-        NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouper.min2()),
-        ULocale.ENGLISH,
-        "87,650,000‰",
-        "8,765,000‰",
-        "876,500‰",
-        "87,650‰",
-        "8765‰",
-        "876.5‰",
-        "87.65‰",
-        "8.765‰",
-        "0‰");
-
-    assertFormatDescending(
-        "Indic Grouping, Min 2",
-        "%% grouping=min2",
-        NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouper.min2()),
-        new ULocale("en-IN"),
-        "8,76,50,000‰",
-        "87,65,000‰",
-        "8,76,500‰",
-        "87,650‰",
-        "8765‰",
-        "876.5‰",
-        "87.65‰",
-        "8.765‰",
-        "0‰");
-
-    assertFormatDescending(
-        "No Grouping",
-        "%% grouping=none",
-        NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouper.none()),
-        new ULocale("en-IN"),
-        "87650000‰",
-        "8765000‰",
-        "876500‰",
-        "87650‰",
-        "8765‰",
-        "876.5‰",
-        "87.65‰",
-        "8.765‰",
-        "0‰");
-  }
-
-  @Test
-  public void padding() {
-    assertFormatDescending(
-        "Padding",
-        "",
-        NumberFormatter.with().padding(Padder.none()),
-        ULocale.ENGLISH,
-        "87,650",
-        "8,765",
-        "876.5",
-        "87.65",
-        "8.765",
-        "0.8765",
-        "0.08765",
-        "0.008765",
-        "0");
-
-    assertFormatDescending(
-        "Padding",
-        "",
-        NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX)),
-        ULocale.ENGLISH,
-        "**87,650",
-        "***8,765",
-        "***876.5",
-        "***87.65",
-        "***8.765",
-        "**0.8765",
-        "*0.08765",
-        "0.008765",
-        "*******0");
-
-    assertFormatDescending(
-        "Padding with code points",
-        "",
-        NumberFormatter.with().padding(Padder.codePoints(0x101E4, 8, PadPosition.AFTER_PREFIX)),
-        ULocale.ENGLISH,
-        "𐇤𐇤87,650",
-        "𐇤𐇤𐇤8,765",
-        "𐇤𐇤𐇤876.5",
-        "𐇤𐇤𐇤87.65",
-        "𐇤𐇤𐇤8.765",
-        "𐇤𐇤0.8765",
-        "𐇤0.08765",
-        "0.008765",
-        "𐇤𐇤𐇤𐇤𐇤𐇤𐇤0");
-
-    assertFormatDescending(
-        "Padding with wide digits",
-        "symbols=ns:mathsanb",
-        NumberFormatter.with()
-            .padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX))
-            .symbols(NumberingSystem.getInstanceByName("mathsanb")),
-        ULocale.ENGLISH,
-        "**𝟴𝟳,𝟲𝟱𝟬",
-        "***𝟴,𝟳𝟲𝟱",
-        "***𝟴𝟳𝟲.𝟱",
-        "***𝟴𝟳.𝟲𝟱",
-        "***𝟴.𝟳𝟲𝟱",
-        "**𝟬.𝟴𝟳𝟲𝟱",
-        "*𝟬.𝟬𝟴𝟳𝟲𝟱",
-        "𝟬.𝟬𝟬𝟴𝟳𝟲𝟱",
-        "*******𝟬");
-
-    assertFormatDescending(
-        "Padding with currency spacing",
-        "$GBP unit-width=SHORT",
-        NumberFormatter.with()
-            .padding(Padder.codePoints('*', 10, PadPosition.AFTER_PREFIX))
-            .unit(GBP)
-            .unitWidth(FormatWidth.SHORT),
-        ULocale.ENGLISH,
-        "GBP 87,650.00",
-        "GBP 8,765.00",
-        "GBP 876.50",
-        "GBP**87.65",
-        "GBP***8.76",
-        "GBP***0.88",
-        "GBP***0.09",
-        "GBP***0.01",
-        "GBP***0.00");
-
-    assertFormatSingle(
-        "Pad Before Prefix",
-        "",
-        NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.BEFORE_PREFIX)),
-        ULocale.ENGLISH,
-        -88.88,
-        "**-88.88");
-
-    assertFormatSingle(
-        "Pad After Prefix",
-        "",
-        NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX)),
-        ULocale.ENGLISH,
-        -88.88,
-        "-**88.88");
-
-    assertFormatSingle(
-        "Pad Before Suffix",
-        "%",
-        NumberFormatter.with()
-            .padding(Padder.codePoints('*', 8, PadPosition.BEFORE_SUFFIX))
-            .unit(Dimensionless.PERCENT),
-        ULocale.ENGLISH,
-        0.8888,
-        "88.88**%");
-
-    assertFormatSingle(
-        "Pad After Suffix",
-        "%",
-        NumberFormatter.with()
-            .padding(Padder.codePoints('*', 8, PadPosition.AFTER_SUFFIX))
-            .unit(Dimensionless.PERCENT),
-        ULocale.ENGLISH,
-        0.8888,
-        "88.88%**");
-  }
-
-  @Test
-  public void integerWidth() {
-    assertFormatDescending(
-        "Integer Width Default",
-        "integer-width=1-",
-        NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(1)),
-        ULocale.ENGLISH,
-        "87,650",
-        "8,765",
-        "876.5",
-        "87.65",
-        "8.765",
-        "0.8765",
-        "0.08765",
-        "0.008765",
-        "0");
-
-    assertFormatDescending(
-        "Integer Width Zero Fill 0",
-        "integer-width=0-",
-        NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(0)),
-        ULocale.ENGLISH,
-        "87,650",
-        "8,765",
-        "876.5",
-        "87.65",
-        "8.765",
-        ".8765",
-        ".08765",
-        ".008765",
-        ""); // FIXME: Avoid the empty string here?
-
-    assertFormatDescending(
-        "Integer Width Zero Fill 3",
-        "integer-width=3-",
-        NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(3)),
-        ULocale.ENGLISH,
-        "87,650",
-        "8,765",
-        "876.5",
-        "087.65",
-        "008.765",
-        "000.8765",
-        "000.08765",
-        "000.008765",
-        "000");
-
-    assertFormatDescending(
-        "Integer Width Max 3",
-        "integer-width=1-3",
-        NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(1).truncateAt(3)),
-        ULocale.ENGLISH,
-        "650",
-        "765",
-        "876.5",
-        "87.65",
-        "8.765",
-        "0.8765",
-        "0.08765",
-        "0.008765",
-        "0");
-
-    assertFormatDescending(
-        "Integer Width Fixed 2",
-        "integer-width=2",
-        NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(2).truncateAt(2)),
-        ULocale.ENGLISH,
-        "50",
-        "65",
-        "76.5",
-        "87.65",
-        "08.765",
-        "00.8765",
-        "00.08765",
-        "00.008765",
-        "00");
-  }
-
-  @Test
-  public void symbols() {
-    assertFormatDescending(
-        "French Symbols with Japanese Data 1",
-        "symbols=loc:fr",
-        NumberFormatter.with().symbols(DecimalFormatSymbols.getInstance(ULocale.FRENCH)),
-        ULocale.JAPAN,
-        "87 650",
-        "8 765",
-        "876,5",
-        "87,65",
-        "8,765",
-        "0,8765",
-        "0,08765",
-        "0,008765",
-        "0");
-
-    assertFormatSingle(
-        "French Symbols with Japanese Data 2",
-        "C symbols=loc:fr",
-        NumberFormatter.with()
-            .notation(Notation.compactShort())
-            .symbols(DecimalFormatSymbols.getInstance(ULocale.FRENCH)),
-        ULocale.JAPAN,
-        12345,
-        "1,2\u4E07");
-
-    assertFormatDescending(
-        "Latin Numbering System with Arabic Data",
-        "$USD symbols=ns:latn",
-        NumberFormatter.with().symbols(NumberingSystem.LATIN).unit(USD),
-        new ULocale("ar"),
-        "87,650.00 US$",
-        "8,765.00 US$",
-        "876.50 US$",
-        "87.65 US$",
-        "8.76 US$",
-        "0.88 US$",
-        "0.09 US$",
-        "0.01 US$",
-        "0.00 US$");
-
-    assertFormatDescending(
-        "Math Numbering System with French Data",
-        "symbols=ns:mathsanb",
-        NumberFormatter.with().symbols(NumberingSystem.getInstanceByName("mathsanb")),
-        ULocale.FRENCH,
-        "𝟴𝟳 𝟲𝟱𝟬",
-        "𝟴 𝟳𝟲𝟱",
-        "𝟴𝟳𝟲,𝟱",
-        "𝟴𝟳,𝟲𝟱",
-        "𝟴,𝟳𝟲𝟱",
-        "𝟬,𝟴𝟳𝟲𝟱",
-        "𝟬,𝟬𝟴𝟳𝟲𝟱",
-        "𝟬,𝟬𝟬𝟴𝟳𝟲𝟱",
-        "𝟬");
-  }
-
-  @Test
-  @Ignore("This feature is not currently available.")
-  public void symbolsOverride() {
-    DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(ULocale.ENGLISH);
-    dfs.setCurrencySymbol("@");
-    dfs.setInternationalCurrencySymbol("foo");
-    assertFormatSingle(
-        "Custom Short Currency Symbol",
-        "$XXX",
-        NumberFormatter.with().unit(Currency.getInstance("XXX")).symbols(dfs),
-        ULocale.ENGLISH,
-        12.3,
-        "@ 12.30");
-  }
-
-  @Test
-  public void sign() {
-    assertFormatSingle(
-        "Sign Auto Positive",
-        "sign=AUTO",
-        NumberFormatter.with().sign(SignDisplay.AUTO),
-        ULocale.ENGLISH,
-        444444,
-        "444,444");
-
-    assertFormatSingle(
-        "Sign Auto Negative",
-        "sign=AUTO",
-        NumberFormatter.with().sign(SignDisplay.AUTO),
-        ULocale.ENGLISH,
-        -444444,
-        "-444,444");
-
-    assertFormatSingle(
-        "Sign Always Positive",
-        "sign=ALWAYS",
-        NumberFormatter.with().sign(SignDisplay.ALWAYS),
-        ULocale.ENGLISH,
-        444444,
-        "+444,444");
-
-    assertFormatSingle(
-        "Sign Always Negative",
-        "sign=ALWAYS",
-        NumberFormatter.with().sign(SignDisplay.ALWAYS),
-        ULocale.ENGLISH,
-        -444444,
-        "-444,444");
-
-    assertFormatSingle(
-        "Sign Never Positive",
-        "sign=NEVER",
-        NumberFormatter.with().sign(SignDisplay.NEVER),
-        ULocale.ENGLISH,
-        444444,
-        "444,444");
-
-    assertFormatSingle(
-        "Sign Never Negative",
-        "sign=NEVER",
-        NumberFormatter.with().sign(SignDisplay.NEVER),
-        ULocale.ENGLISH,
-        -444444,
-        "444,444");
-  }
-
-  @Test
-  public void decimal() {
-    assertFormatDescending(
-        "Decimal Default",
-        "decimal=AUTO",
-        NumberFormatter.with().decimal(DecimalMarkDisplay.AUTO),
-        ULocale.ENGLISH,
-        "87,650",
-        "8,765",
-        "876.5",
-        "87.65",
-        "8.765",
-        "0.8765",
-        "0.08765",
-        "0.008765",
-        "0");
-
-    assertFormatDescending(
-        "Decimal Always Shown",
-        "decimal=ALWAYS",
-        NumberFormatter.with().decimal(DecimalMarkDisplay.ALWAYS),
-        ULocale.ENGLISH,
-        "87,650.",
-        "8,765.",
-        "876.5",
-        "87.65",
-        "8.765",
-        "0.8765",
-        "0.08765",
-        "0.008765",
-        "0.");
-  }
-
-  @Test
-  public void locale() {
-    // Coverage for the locale setters.
-    assertEquals(
-        NumberFormatter.with().locale(ULocale.ENGLISH),
-        NumberFormatter.with().locale(Locale.ENGLISH));
-    assertNotEquals(
-        NumberFormatter.with().locale(ULocale.ENGLISH),
-        NumberFormatter.with().locale(Locale.FRENCH));
-  }
-
-  @Test
-  public void getPrefixSuffix() {
-    Object[][] cases = {
-      {
-        NumberFormatter.withLocale(ULocale.ENGLISH).unit(GBP).unitWidth(FormatWidth.SHORT),
-        "GBP",
-        "",
-        "-GBP",
-        ""
-      },
-      {
-        NumberFormatter.withLocale(ULocale.ENGLISH).unit(GBP).unitWidth(FormatWidth.WIDE),
-        "",
-        " British pounds",
-        "-",
-        " British pounds"
-      }
-    };
-
-    for (Object[] cas : cases) {
-      LocalizedNumberFormatter f = (LocalizedNumberFormatter) cas[0];
-      String posPrefix = (String) cas[1];
-      String posSuffix = (String) cas[2];
-      String negPrefix = (String) cas[3];
-      String negSuffix = (String) cas[4];
-      FormattedNumber positive = f.format(1);
-      FormattedNumber negative = f.format(-1);
-      assertEquals(posPrefix, positive.getPrefix());
-      assertEquals(posSuffix, positive.getSuffix());
-      assertEquals(negPrefix, negative.getPrefix());
-      assertEquals(negSuffix, negative.getSuffix());
+    private static final Currency USD = Currency.getInstance("USD");
+    private static final Currency GBP = Currency.getInstance("GBP");
+    private static final Currency CZK = Currency.getInstance("CZK");
+
+    @Test
+    public void notationSimple() {
+        assertFormatDescending(
+                "Basic",
+                "",
+                NumberFormatter.with(),
+                ULocale.ENGLISH,
+                "87,650",
+                "8,765",
+                "876.5",
+                "87.65",
+                "8.765",
+                "0.8765",
+                "0.08765",
+                "0.008765",
+                "0");
+
+        assertFormatSingle(
+                "Basic with Negative Sign",
+                "",
+                NumberFormatter.with(),
+                ULocale.ENGLISH,
+                -9876543.21,
+                "-9,876,543.21");
+    }
+
+    @Test
+    public void notationScientific() {
+        assertFormatDescending(
+                "Scientific",
+                "E",
+                NumberFormatter.with().notation(Notation.scientific()),
+                ULocale.ENGLISH,
+                "8.765E4",
+                "8.765E3",
+                "8.765E2",
+                "8.765E1",
+                "8.765E0",
+                "8.765E-1",
+                "8.765E-2",
+                "8.765E-3",
+                "0E0");
+
+        assertFormatDescending(
+                "Engineering",
+                "E3",
+                NumberFormatter.with().notation(Notation.engineering()),
+                ULocale.ENGLISH,
+                "87.65E3",
+                "8.765E3",
+                "876.5E0",
+                "87.65E0",
+                "8.765E0",
+                "876.5E-3",
+                "87.65E-3",
+                "8.765E-3",
+                "0E0");
+
+        assertFormatDescending(
+                "Scientific sign always shown",
+                "E+",
+                NumberFormatter.with().notation(Notation.scientific().withExponentSignDisplay(SignDisplay.ALWAYS)),
+                ULocale.ENGLISH,
+                "8.765E+4",
+                "8.765E+3",
+                "8.765E+2",
+                "8.765E+1",
+                "8.765E+0",
+                "8.765E-1",
+                "8.765E-2",
+                "8.765E-3",
+                "0E+0");
+
+        assertFormatDescending(
+                "Scientific min exponent digits",
+                "E00",
+                NumberFormatter.with().notation(Notation.scientific().withMinExponentDigits(2)),
+                ULocale.ENGLISH,
+                "8.765E04",
+                "8.765E03",
+                "8.765E02",
+                "8.765E01",
+                "8.765E00",
+                "8.765E-01",
+                "8.765E-02",
+                "8.765E-03",
+                "0E00");
+
+        assertFormatSingle(
+                "Scientific Negative",
+                "E",
+                NumberFormatter.with().notation(Notation.scientific()),
+                ULocale.ENGLISH,
+                -1000000,
+                "-1E6");
+    }
+
+    @Test
+    public void notationCompact() {
+        assertFormatDescending(
+                "Compact Short",
+                "C",
+                NumberFormatter.with().notation(Notation.compactShort()),
+                ULocale.ENGLISH,
+                "88K",
+                "8.8K",
+                "876",
+                "88",
+                "8.8",
+                "0.88",
+                "0.088",
+                "0.0088",
+                "0");
+
+        assertFormatDescending(
+                "Compact Long",
+                "CC",
+                NumberFormatter.with().notation(Notation.compactLong()),
+                ULocale.ENGLISH,
+                "88 thousand",
+                "8.8 thousand",
+                "876",
+                "88",
+                "8.8",
+                "0.88",
+                "0.088",
+                "0.0088",
+                "0");
+
+        assertFormatDescending(
+                "Compact Short Currency",
+                "C $USD",
+                NumberFormatter.with().notation(Notation.compactShort()).unit(USD),
+                ULocale.ENGLISH,
+                "$88K",
+                "$8.8K",
+                "$876",
+                "$88",
+                "$8.8",
+                "$0.88",
+                "$0.088",
+                "$0.0088",
+                "$0");
+
+        // Note: Most locales don't have compact long currency, so this currently falls back to short.
+        assertFormatDescending(
+                "Compact Long Currency",
+                "CC $USD",
+                NumberFormatter.with().notation(Notation.compactLong()).unit(USD),
+                ULocale.ENGLISH,
+                "$88K",
+                "$8.8K",
+                "$876",
+                "$88",
+                "$8.8",
+                "$0.88",
+                "$0.088",
+                "$0.0088",
+                "$0");
+
+        assertFormatSingle(
+                "Compact Plural One",
+                "CC",
+                NumberFormatter.with().notation(Notation.compactLong()),
+                ULocale.forLanguageTag("es"),
+                1000000,
+                "1 millón");
+
+        assertFormatSingle(
+                "Compact Plural Other",
+                "CC",
+                NumberFormatter.with().notation(Notation.compactLong()),
+                ULocale.forLanguageTag("es"),
+                2000000,
+                "2 millones");
+
+        assertFormatSingle(
+                "Compact with Negative Sign",
+                "C",
+                NumberFormatter.with().notation(Notation.compactShort()),
+                ULocale.ENGLISH,
+                -9876543.21,
+                "-9.9M");
+
+        assertFormatSingle(
+                "Compact Rounding",
+                "C",
+                NumberFormatter.with().notation(Notation.compactShort()),
+                ULocale.ENGLISH,
+                990000,
+                "990K");
+
+        assertFormatSingle(
+                "Compact Rounding",
+                "C",
+                NumberFormatter.with().notation(Notation.compactShort()),
+                ULocale.ENGLISH,
+                999000,
+                "999K");
+
+        assertFormatSingle(
+                "Compact Rounding",
+                "C",
+                NumberFormatter.with().notation(Notation.compactShort()),
+                ULocale.ENGLISH,
+                999900,
+                "1M");
+
+        assertFormatSingle(
+                "Compact Rounding",
+                "C",
+                NumberFormatter.with().notation(Notation.compactShort()),
+                ULocale.ENGLISH,
+                9900000,
+                "9.9M");
+
+        assertFormatSingle(
+                "Compact Rounding",
+                "C",
+                NumberFormatter.with().notation(Notation.compactShort()),
+                ULocale.ENGLISH,
+                9990000,
+                "10M");
+    }
+
+    @Test
+    public void unitMeasure() {
+        assertFormatDescending(
+                "Meters Short",
+                "U:length:meter",
+                NumberFormatter.with().unit(MeasureUnit.METER),
+                ULocale.ENGLISH,
+                "87,650 m",
+                "8,765 m",
+                "876.5 m",
+                "87.65 m",
+                "8.765 m",
+                "0.8765 m",
+                "0.08765 m",
+                "0.008765 m",
+                "0 m");
+
+        assertFormatDescending(
+                "Meters Long",
+                "U:length:meter unit-width=FULL_NAME",
+                NumberFormatter.with().unit(MeasureUnit.METER).unitWidth(UnitWidth.FULL_NAME),
+                ULocale.ENGLISH,
+                "87,650 meters",
+                "8,765 meters",
+                "876.5 meters",
+                "87.65 meters",
+                "8.765 meters",
+                "0.8765 meters",
+                "0.08765 meters",
+                "0.008765 meters",
+                "0 meters");
+
+        assertFormatDescending(
+                "Compact Meters Long",
+                "CC U:length:meter unit-width=FULL_NAME",
+                NumberFormatter.with().notation(Notation.compactLong()).unit(MeasureUnit.METER)
+                        .unitWidth(UnitWidth.FULL_NAME),
+                ULocale.ENGLISH,
+                "88 thousand meters",
+                "8.8 thousand meters",
+                "876 meters",
+                "88 meters",
+                "8.8 meters",
+                "0.88 meters",
+                "0.088 meters",
+                "0.0088 meters",
+                "0 meters");
+
+        assertFormatSingleMeasure(
+                "Meters with Measure Input",
+                "unit-width=FULL_NAME",
+                NumberFormatter.with().unitWidth(UnitWidth.FULL_NAME),
+                ULocale.ENGLISH,
+                new Measure(5.43, MeasureUnit.METER),
+                "5.43 meters");
+
+        assertFormatSingleMeasure(
+                "Measure format method takes precedence over fluent chain",
+                "U:length:meter",
+                NumberFormatter.with().unit(MeasureUnit.METER),
+                ULocale.ENGLISH,
+                new Measure(5.43, USD),
+                "$5.43");
+
+        assertFormatSingle(
+                "Meters with Negative Sign",
+                "U:length:meter",
+                NumberFormatter.with().unit(MeasureUnit.METER),
+                ULocale.ENGLISH,
+                -9876543.21,
+                "-9,876,543.21 m");
+    }
+
+    @Test
+    public void unitCurrency() {
+        assertFormatDescending(
+                "Currency",
+                "$GBP",
+                NumberFormatter.with().unit(GBP),
+                ULocale.ENGLISH,
+                "£87,650.00",
+                "£8,765.00",
+                "£876.50",
+                "£87.65",
+                "£8.76",
+                "£0.88",
+                "£0.09",
+                "£0.01",
+                "£0.00");
+
+        assertFormatDescending(
+                "Currency ISO",
+                "$GBP unit-width=ISO_CODE",
+                NumberFormatter.with().unit(GBP).unitWidth(UnitWidth.ISO_CODE),
+                ULocale.ENGLISH,
+                "GBP 87,650.00",
+                "GBP 8,765.00",
+                "GBP 876.50",
+                "GBP 87.65",
+                "GBP 8.76",
+                "GBP 0.88",
+                "GBP 0.09",
+                "GBP 0.01",
+                "GBP 0.00");
+
+        assertFormatDescending(
+                "Currency Long Name",
+                "$GBP unit-width=FULL_NAME",
+                NumberFormatter.with().unit(GBP).unitWidth(UnitWidth.FULL_NAME),
+                ULocale.ENGLISH,
+                "87,650.00 British pounds",
+                "8,765.00 British pounds",
+                "876.50 British pounds",
+                "87.65 British pounds",
+                "8.76 British pounds",
+                "0.88 British pounds",
+                "0.09 British pounds",
+                "0.01 British pounds",
+                "0.00 British pounds");
+
+        assertFormatSingleMeasure(
+                "Currency with CurrencyAmount Input",
+                "",
+                NumberFormatter.with(),
+                ULocale.ENGLISH,
+                new CurrencyAmount(5.43, GBP),
+                "£5.43");
+
+        assertFormatSingle(
+                "Currency Long Name from Pattern Syntax",
+                "$GBP F0 grouping=none integer-width=1- symbols=loc:en_US sign=AUTO decimal=AUTO",
+                NumberPropertyMapper.create("0 ¤¤¤", DecimalFormatSymbols.getInstance()).unit(GBP),
+                ULocale.ENGLISH,
+                1234567.89,
+                "1234568 British pounds");
+
+        assertFormatSingle(
+                "Currency with Negative Sign",
+                "$GBP",
+                NumberFormatter.with().unit(GBP),
+                ULocale.ENGLISH,
+                -9876543.21,
+                "-£9,876,543.21");
+    }
+
+    @Test
+    public void unitPercent() {
+        assertFormatDescending(
+                "Percent",
+                "%",
+                NumberFormatter.with().unit(NoUnit.PERCENT),
+                ULocale.ENGLISH,
+                "8,765,000%",
+                "876,500%",
+                "87,650%",
+                "8,765%",
+                "876.5%",
+                "87.65%",
+                "8.765%",
+                "0.8765%",
+                "0%");
+
+        assertFormatDescending(
+                "Permille",
+                "%%",
+                NumberFormatter.with().unit(NoUnit.PERMILLE),
+                ULocale.ENGLISH,
+                "87,650,000‰",
+                "8,765,000‰",
+                "876,500‰",
+                "87,650‰",
+                "8,765‰",
+                "876.5‰",
+                "87.65‰",
+                "8.765‰",
+                "0‰");
+
+        assertFormatSingle(
+                "NoUnit Base",
+                "B",
+                NumberFormatter.with().unit(NoUnit.BASE),
+                ULocale.ENGLISH,
+                51423,
+                "51,423");
+
+        assertFormatSingle(
+                "Percent with Negative Sign",
+                "%",
+                NumberFormatter.with().unit(NoUnit.PERCENT),
+                ULocale.ENGLISH,
+                -0.987654321,
+                "-98.7654321%");
     }
-  }
-
-  @Test
-  public void plurals() {
-    // TODO: Expand this test.
-
-    assertFormatSingle(
-        "Plural 1",
-        "$USD F0 unit-width=WIDE",
-        NumberFormatter.with()
-            .unit(USD)
-            .unitWidth(FormatWidth.WIDE)
-            .rounding(Rounder.fixedFraction(0)),
-        ULocale.ENGLISH,
-        1,
-        "1 US dollar");
-
-    assertFormatSingle(
-        "Plural 1.00",
-        "$USD F2 unit-width=WIDE",
-        NumberFormatter.with()
-            .unit(USD)
-            .unitWidth(FormatWidth.WIDE)
-            .rounding(Rounder.fixedFraction(2)),
-        ULocale.ENGLISH,
-        1,
-        "1.00 US dollars");
-  }
-
-  private static void assertFormatDescending(
-      String message,
-      String skeleton,
-      UnlocalizedNumberFormatter f,
-      ULocale locale,
-      String... expected) {
-    assert expected.length == 9;
-    assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton());
-    final double[] inputs =
-        new double[] {87650, 8765, 876.5, 87.65, 8.765, 0.8765, 0.08765, 0.008765, 0};
-    LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation
-    LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation
-    for (int i = 0; i < 9; i++) {
-      double d = inputs[i];
-      String actual1 = l1.format(d).toString();
-      assertEquals(message + ": L1: " + d, expected[i], actual1);
-      String actual2 = l2.format(d).toString();
-      assertEquals(message + ": L2: " + d, expected[i], actual2);
+
+    @Test
+    public void roundingFraction() {
+        assertFormatDescending(
+                "Integer",
+                "F0",
+                NumberFormatter.with().rounding(Rounder.integer()),
+                ULocale.ENGLISH,
+                "87,650",
+                "8,765",
+                "876",
+                "88",
+                "9",
+                "1",
+                "0",
+                "0",
+                "0");
+
+        assertFormatDescending(
+                "Fixed Fraction",
+                "F3",
+                NumberFormatter.with().rounding(Rounder.fixedFraction(3)),
+                ULocale.ENGLISH,
+                "87,650.000",
+                "8,765.000",
+                "876.500",
+                "87.650",
+                "8.765",
+                "0.876",
+                "0.088",
+                "0.009",
+                "0.000");
+
+        assertFormatDescending(
+                "Min Fraction",
+                "F1-",
+                NumberFormatter.with().rounding(Rounder.minFraction(1)),
+                ULocale.ENGLISH,
+                "87,650.0",
+                "8,765.0",
+                "876.5",
+                "87.65",
+                "8.765",
+                "0.8765",
+                "0.08765",
+                "0.008765",
+                "0.0");
+
+        assertFormatDescending(
+                "Max Fraction",
+                "F-1",
+                NumberFormatter.with().rounding(Rounder.maxFraction(1)),
+                ULocale.ENGLISH,
+                "87,650",
+                "8,765",
+                "876.5",
+                "87.6",
+                "8.8",
+                "0.9",
+                "0.1",
+                "0",
+                "0");
+
+        assertFormatDescending(
+                "Min/Max Fraction",
+                "F1-3",
+                NumberFormatter.with().rounding(Rounder.minMaxFraction(1, 3)),
+                ULocale.ENGLISH,
+                "87,650.0",
+                "8,765.0",
+                "876.5",
+                "87.65",
+                "8.765",
+                "0.876",
+                "0.088",
+                "0.009",
+                "0.0");
+    }
+
+    @Test
+    public void roundingFigures() {
+        assertFormatSingle(
+                "Fixed Significant",
+                "S3",
+                NumberFormatter.with().rounding(Rounder.fixedDigits(3)),
+                ULocale.ENGLISH,
+                -98,
+                "-98.0");
+
+        assertFormatSingle(
+                "Fixed Significant Rounding",
+                "S3",
+                NumberFormatter.with().rounding(Rounder.fixedDigits(3)),
+                ULocale.ENGLISH,
+                -98.7654321,
+                "-98.8");
+
+        assertFormatSingle(
+                "Fixed Significant Zero",
+                "S3",
+                NumberFormatter.with().rounding(Rounder.fixedDigits(3)),
+                ULocale.ENGLISH,
+                0,
+                "0.00");
+
+        assertFormatSingle(
+                "Min Significant",
+                "S2-",
+                NumberFormatter.with().rounding(Rounder.minDigits(2)),
+                ULocale.ENGLISH,
+                -9,
+                "-9.0");
+
+        assertFormatSingle(
+                "Max Significant",
+                "S-4",
+                NumberFormatter.with().rounding(Rounder.maxDigits(4)),
+                ULocale.ENGLISH,
+                98.7654321,
+                "98.77");
+
+        assertFormatSingle(
+                "Min/Max Significant",
+                "S3-4",
+                NumberFormatter.with().rounding(Rounder.minMaxDigits(3, 4)),
+                ULocale.ENGLISH,
+                9.99999,
+                "10.0");
+    }
+
+    @Test
+    public void roundingFractionFigures() {
+        assertFormatDescending(
+                "Basic Significant", // for comparison
+                "S-2",
+                NumberFormatter.with().rounding(Rounder.maxDigits(2)),
+                ULocale.ENGLISH,
+                "88,000",
+                "8,800",
+                "880",
+                "88",
+                "8.8",
+                "0.88",
+                "0.088",
+                "0.0088",
+                "0");
+
+        assertFormatDescending(
+                "FracSig minMaxFrac minSig",
+                "F1-2>3",
+                NumberFormatter.with().rounding(Rounder.minMaxFraction(1, 2).withMinDigits(3)),
+                ULocale.ENGLISH,
+                "87,650.0",
+                "8,765.0",
+                "876.5",
+                "87.65",
+                "8.76",
+                "0.876", // minSig beats maxFrac
+                "0.0876", // minSig beats maxFrac
+                "0.00876", // minSig beats maxFrac
+                "0.0");
+
+        assertFormatDescending(
+                "FracSig minMaxFrac maxSig A",
+                "F1-3<2",
+                NumberFormatter.with().rounding(Rounder.minMaxFraction(1, 3).withMaxDigits(2)),
+                ULocale.ENGLISH,
+                "88,000.0", // maxSig beats maxFrac
+                "8,800.0", // maxSig beats maxFrac
+                "880.0", // maxSig beats maxFrac
+                "88.0", // maxSig beats maxFrac
+                "8.8", // maxSig beats maxFrac
+                "0.88", // maxSig beats maxFrac
+                "0.088",
+                "0.009",
+                "0.0");
+
+        assertFormatDescending(
+                "FracSig minMaxFrac maxSig B",
+                "F2<2",
+                NumberFormatter.with().rounding(Rounder.fixedFraction(2).withMaxDigits(2)),
+                ULocale.ENGLISH,
+                "88,000.00", // maxSig beats maxFrac
+                "8,800.00", // maxSig beats maxFrac
+                "880.00", // maxSig beats maxFrac
+                "88.00", // maxSig beats maxFrac
+                "8.80", // maxSig beats maxFrac
+                "0.88",
+                "0.09",
+                "0.01",
+                "0.00");
+    }
+
+    @Test
+    public void roundingOther() {
+        assertFormatDescending(
+                "Rounding None",
+                "Y",
+                NumberFormatter.with().rounding(Rounder.none()),
+                ULocale.ENGLISH,
+                "87,650",
+                "8,765",
+                "876.5",
+                "87.65",
+                "8.765",
+                "0.8765",
+                "0.08765",
+                "0.008765",
+                "0");
+
+        assertFormatDescending(
+                "Increment",
+                "M0.5",
+                NumberFormatter.with().rounding(Rounder.increment(BigDecimal.valueOf(0.5))),
+                ULocale.ENGLISH,
+                "87,650.0",
+                "8,765.0",
+                "876.5",
+                "87.5",
+                "9.0",
+                "1.0",
+                "0.0",
+                "0.0",
+                "0.0");
+
+        assertFormatDescending(
+                "Currency Standard",
+                "$CZK GSTANDARD",
+                NumberFormatter.with().rounding(Rounder.currency(CurrencyUsage.STANDARD)).unit(CZK),
+                ULocale.ENGLISH,
+                "CZK 87,650.00",
+                "CZK 8,765.00",
+                "CZK 876.50",
+                "CZK 87.65",
+                "CZK 8.76",
+                "CZK 0.88",
+                "CZK 0.09",
+                "CZK 0.01",
+                "CZK 0.00");
+
+        assertFormatDescending(
+                "Currency Cash",
+                "$CZK GCASH",
+                NumberFormatter.with().rounding(Rounder.currency(CurrencyUsage.CASH)).unit(CZK),
+                ULocale.ENGLISH,
+                "CZK 87,650",
+                "CZK 8,765",
+                "CZK 876",
+                "CZK 88",
+                "CZK 9",
+                "CZK 1",
+                "CZK 0",
+                "CZK 0",
+                "CZK 0");
+
+        assertFormatDescending(
+                "Currency not in top-level fluent chain",
+                "F0",
+                NumberFormatter.with().rounding(Rounder.currency(CurrencyUsage.CASH).withCurrency(CZK)),
+                ULocale.ENGLISH,
+                "87,650",
+                "8,765",
+                "876",
+                "88",
+                "9",
+                "1",
+                "0",
+                "0",
+                "0");
+    }
+
+    @Test
+    public void grouping() {
+        // NoUnit.PERMILLE multiplies all the number by 10^3 (good for testing grouping).
+        // Note that en-US is already performed in the unitPercent() function.
+        assertFormatDescending(
+                "Indic Grouping",
+                "%% grouping=defaults",
+                NumberFormatter.with().unit(NoUnit.PERMILLE).grouping(Grouper.defaults()),
+                new ULocale("en-IN"),
+                "8,76,50,000‰",
+                "87,65,000‰",
+                "8,76,500‰",
+                "87,650‰",
+                "8,765‰",
+                "876.5‰",
+                "87.65‰",
+                "8.765‰",
+                "0‰");
+
+        assertFormatDescending(
+                "Western Grouping, Min 2",
+                "%% grouping=min2",
+                NumberFormatter.with().unit(NoUnit.PERMILLE).grouping(Grouper.min2()),
+                ULocale.ENGLISH,
+                "87,650,000‰",
+                "8,765,000‰",
+                "876,500‰",
+                "87,650‰",
+                "8765‰",
+                "876.5‰",
+                "87.65‰",
+                "8.765‰",
+                "0‰");
+
+        assertFormatDescending(
+                "Indic Grouping, Min 2",
+                "%% grouping=min2",
+                NumberFormatter.with().unit(NoUnit.PERMILLE).grouping(Grouper.min2()),
+                new ULocale("en-IN"),
+                "8,76,50,000‰",
+                "87,65,000‰",
+                "8,76,500‰",
+                "87,650‰",
+                "8765‰",
+                "876.5‰",
+                "87.65‰",
+                "8.765‰",
+                "0‰");
+
+        assertFormatDescending(
+                "No Grouping",
+                "%% grouping=none",
+                NumberFormatter.with().unit(NoUnit.PERMILLE).grouping(Grouper.none()),
+                new ULocale("en-IN"),
+                "87650000‰",
+                "8765000‰",
+                "876500‰",
+                "87650‰",
+                "8765‰",
+                "876.5‰",
+                "87.65‰",
+                "8.765‰",
+                "0‰");
+    }
+
+    @Test
+    public void padding() {
+        assertFormatDescending(
+                "Padding",
+                "",
+                NumberFormatter.with().padding(Padder.none()),
+                ULocale.ENGLISH,
+                "87,650",
+                "8,765",
+                "876.5",
+                "87.65",
+                "8.765",
+                "0.8765",
+                "0.08765",
+                "0.008765",
+                "0");
+
+        assertFormatDescending(
+                "Padding",
+                "",
+                NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX)),
+                ULocale.ENGLISH,
+                "**87,650",
+                "***8,765",
+                "***876.5",
+                "***87.65",
+                "***8.765",
+                "**0.8765",
+                "*0.08765",
+                "0.008765",
+                "*******0");
+
+        assertFormatDescending(
+                "Padding with code points",
+                "",
+                NumberFormatter.with().padding(Padder.codePoints(0x101E4, 8, PadPosition.AFTER_PREFIX)),
+                ULocale.ENGLISH,
+                "𐇤𐇤87,650",
+                "𐇤𐇤𐇤8,765",
+                "𐇤𐇤𐇤876.5",
+                "𐇤𐇤𐇤87.65",
+                "𐇤𐇤𐇤8.765",
+                "𐇤𐇤0.8765",
+                "𐇤0.08765",
+                "0.008765",
+                "𐇤𐇤𐇤𐇤𐇤𐇤𐇤0");
+
+        assertFormatDescending(
+                "Padding with wide digits",
+                "symbols=ns:mathsanb",
+                NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX))
+                        .symbols(NumberingSystem.getInstanceByName("mathsanb")),
+                ULocale.ENGLISH,
+                "**𝟴𝟳,𝟲𝟱𝟬",
+                "***𝟴,𝟳𝟲𝟱",
+                "***𝟴𝟳𝟲.𝟱",
+                "***𝟴𝟳.𝟲𝟱",
+                "***𝟴.𝟳𝟲𝟱",
+                "**𝟬.𝟴𝟳𝟲𝟱",
+                "*𝟬.𝟬𝟴𝟳𝟲𝟱",
+                "𝟬.𝟬𝟬𝟴𝟳𝟲𝟱",
+                "*******𝟬");
+
+        assertFormatDescending(
+                "Padding with currency spacing",
+                "$GBP unit-width=ISO_CODE",
+                NumberFormatter.with().padding(Padder.codePoints('*', 10, PadPosition.AFTER_PREFIX)).unit(GBP)
+                        .unitWidth(UnitWidth.ISO_CODE),
+                ULocale.ENGLISH,
+                "GBP 87,650.00",
+                "GBP 8,765.00",
+                "GBP 876.50",
+                "GBP**87.65",
+                "GBP***8.76",
+                "GBP***0.88",
+                "GBP***0.09",
+                "GBP***0.01",
+                "GBP***0.00");
+
+        assertFormatSingle(
+                "Pad Before Prefix",
+                "",
+                NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.BEFORE_PREFIX)),
+                ULocale.ENGLISH,
+                -88.88,
+                "**-88.88");
+
+        assertFormatSingle(
+                "Pad After Prefix",
+                "",
+                NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_PREFIX)),
+                ULocale.ENGLISH,
+                -88.88,
+                "-**88.88");
+
+        assertFormatSingle(
+                "Pad Before Suffix",
+                "%",
+                NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.BEFORE_SUFFIX))
+                        .unit(NoUnit.PERCENT),
+                ULocale.ENGLISH,
+                0.8888,
+                "88.88**%");
+
+        assertFormatSingle(
+                "Pad After Suffix",
+                "%",
+                NumberFormatter.with().padding(Padder.codePoints('*', 8, PadPosition.AFTER_SUFFIX))
+                        .unit(NoUnit.PERCENT),
+                ULocale.ENGLISH,
+                0.8888,
+                "88.88%**");
+    }
+
+    @Test
+    public void integerWidth() {
+        assertFormatDescending(
+                "Integer Width Default",
+                "integer-width=1-",
+                NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(1)),
+                ULocale.ENGLISH,
+                "87,650",
+                "8,765",
+                "876.5",
+                "87.65",
+                "8.765",
+                "0.8765",
+                "0.08765",
+                "0.008765",
+                "0");
+
+        assertFormatDescending(
+                "Integer Width Zero Fill 0",
+                "integer-width=0-",
+                NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(0)),
+                ULocale.ENGLISH,
+                "87,650",
+                "8,765",
+                "876.5",
+                "87.65",
+                "8.765",
+                ".8765",
+                ".08765",
+                ".008765",
+                ""); // TODO: Avoid the empty string here?
+
+        assertFormatDescending(
+                "Integer Width Zero Fill 3",
+                "integer-width=3-",
+                NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(3)),
+                ULocale.ENGLISH,
+                "87,650",
+                "8,765",
+                "876.5",
+                "087.65",
+                "008.765",
+                "000.8765",
+                "000.08765",
+                "000.008765",
+                "000");
+
+        assertFormatDescending(
+                "Integer Width Max 3",
+                "integer-width=1-3",
+                NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(1).truncateAt(3)),
+                ULocale.ENGLISH,
+                "650",
+                "765",
+                "876.5",
+                "87.65",
+                "8.765",
+                "0.8765",
+                "0.08765",
+                "0.008765",
+                "0");
+
+        assertFormatDescending(
+                "Integer Width Fixed 2",
+                "integer-width=2",
+                NumberFormatter.with().integerWidth(IntegerWidth.zeroFillTo(2).truncateAt(2)),
+                ULocale.ENGLISH,
+                "50",
+                "65",
+                "76.5",
+                "87.65",
+                "08.765",
+                "00.8765",
+                "00.08765",
+                "00.008765",
+                "00");
+    }
+
+    @Test
+    public void symbols() {
+        assertFormatDescending(
+                "French Symbols with Japanese Data 1",
+                "symbols=loc:fr",
+                NumberFormatter.with().symbols(DecimalFormatSymbols.getInstance(ULocale.FRENCH)),
+                ULocale.JAPAN,
+                "87 650",
+                "8 765",
+                "876,5",
+                "87,65",
+                "8,765",
+                "0,8765",
+                "0,08765",
+                "0,008765",
+                "0");
+
+        assertFormatSingle(
+                "French Symbols with Japanese Data 2",
+                "C symbols=loc:fr",
+                NumberFormatter.with().notation(Notation.compactShort())
+                        .symbols(DecimalFormatSymbols.getInstance(ULocale.FRENCH)),
+                ULocale.JAPAN,
+                12345,
+                "1,2\u4E07");
+
+        assertFormatDescending(
+                "Latin Numbering System with Arabic Data",
+                "$USD symbols=ns:latn",
+                NumberFormatter.with().symbols(NumberingSystem.LATIN).unit(USD),
+                new ULocale("ar"),
+                "87,650.00 US$",
+                "8,765.00 US$",
+                "876.50 US$",
+                "87.65 US$",
+                "8.76 US$",
+                "0.88 US$",
+                "0.09 US$",
+                "0.01 US$",
+                "0.00 US$");
+
+        assertFormatDescending(
+                "Math Numbering System with French Data",
+                "symbols=ns:mathsanb",
+                NumberFormatter.with().symbols(NumberingSystem.getInstanceByName("mathsanb")),
+                ULocale.FRENCH,
+                "𝟴𝟳 𝟲𝟱𝟬",
+                "𝟴 𝟳𝟲𝟱",
+                "𝟴𝟳𝟲,𝟱",
+                "𝟴𝟳,𝟲𝟱",
+                "𝟴,𝟳𝟲𝟱",
+                "𝟬,𝟴𝟳𝟲𝟱",
+                "𝟬,𝟬𝟴𝟳𝟲𝟱",
+                "𝟬,𝟬𝟬𝟴𝟳𝟲𝟱",
+                "𝟬");
+
+        assertFormatSingle(
+                "Swiss Symbols (used in documentation)",
+                "symbols=loc:de_CH",
+                NumberFormatter.with().symbols(DecimalFormatSymbols.getInstance(new ULocale("de-CH"))),
+                ULocale.ENGLISH,
+                12345.67,
+                "12’345.67");
+
+        assertFormatSingle(
+                "Myanmar Symbols (used in documentation)",
+                "symbols=loc:my_MY",
+                NumberFormatter.with().symbols(DecimalFormatSymbols.getInstance(new ULocale("my_MY"))),
+                ULocale.ENGLISH,
+                12345.67,
+                "\u1041\u1042,\u1043\u1044\u1045.\u1046\u1047");
+
+        DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(new ULocale("de-CH"));
+        UnlocalizedNumberFormatter f = NumberFormatter.with().symbols(symbols);
+        symbols.setGroupingSeparatorString("!");
+        assertFormatSingle(
+                "Symbols object should be copied",
+                "symbols=loc:de_CH",
+                f,
+                ULocale.ENGLISH,
+                12345.67,
+                "12’345.67");
+
+        assertFormatSingle(
+                "The last symbols setter wins",
+                "symbols=ns:latn",
+                NumberFormatter.with().symbols(symbols).symbols(NumberingSystem.LATIN),
+                ULocale.ENGLISH,
+                12345.67,
+                "12,345.67");
+
+        assertFormatSingle(
+                "The last symbols setter wins",
+                "symbols=loc:de_CH",
+                NumberFormatter.with().symbols(NumberingSystem.LATIN).symbols(symbols),
+                ULocale.ENGLISH,
+                12345.67,
+                "12!345.67");
+    }
+
+    @Test
+    @Ignore("This feature is not currently available.")
+    public void symbolsOverride() {
+        DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(ULocale.ENGLISH);
+        dfs.setCurrencySymbol("@");
+        dfs.setInternationalCurrencySymbol("foo");
+        assertFormatSingle(
+                "Custom Short Currency Symbol",
+                "$XXX",
+                NumberFormatter.with().unit(Currency.getInstance("XXX")).symbols(dfs),
+                ULocale.ENGLISH,
+                12.3,
+                "@ 12.30");
+    }
+
+    @Test
+    public void sign() {
+        assertFormatSingle(
+                "Sign Auto Positive",
+                "sign=AUTO",
+                NumberFormatter.with().sign(SignDisplay.AUTO),
+                ULocale.ENGLISH,
+                444444,
+                "444,444");
+
+        assertFormatSingle(
+                "Sign Auto Negative",
+                "sign=AUTO",
+                NumberFormatter.with().sign(SignDisplay.AUTO),
+                ULocale.ENGLISH,
+                -444444,
+                "-444,444");
+
+        assertFormatSingle(
+                "Sign Always Positive",
+                "sign=ALWAYS",
+                NumberFormatter.with().sign(SignDisplay.ALWAYS),
+                ULocale.ENGLISH,
+                444444,
+                "+444,444");
+
+        assertFormatSingle(
+                "Sign Always Negative",
+                "sign=ALWAYS",
+                NumberFormatter.with().sign(SignDisplay.ALWAYS),
+                ULocale.ENGLISH,
+                -444444,
+                "-444,444");
+
+        assertFormatSingle(
+                "Sign Never Positive",
+                "sign=NEVER",
+                NumberFormatter.with().sign(SignDisplay.NEVER),
+                ULocale.ENGLISH,
+                444444,
+                "444,444");
+
+        assertFormatSingle(
+                "Sign Never Negative",
+                "sign=NEVER",
+                NumberFormatter.with().sign(SignDisplay.NEVER),
+                ULocale.ENGLISH,
+                -444444,
+                "444,444");
+
+        assertFormatSingle(
+                "Sign Accounting Positive",
+                "$USD sign=ACCOUNTING",
+                NumberFormatter.with().sign(SignDisplay.ACCOUNTING).unit(USD),
+                ULocale.ENGLISH,
+                444444,
+                "$444,444.00");
+
+        assertFormatSingle(
+                "Sign Accounting Negative",
+                "$USD sign=ACCOUNTING",
+                NumberFormatter.with().sign(SignDisplay.ACCOUNTING).unit(USD),
+                ULocale.ENGLISH,
+                -444444,
+                "($444,444.00)");
+
+        assertFormatSingle(
+                "Sign Accounting-Always Positive",
+                "$USD sign=ACCOUNTING_ALWAYS",
+                NumberFormatter.with().sign(SignDisplay.ACCOUNTING_ALWAYS).unit(USD),
+                ULocale.ENGLISH,
+                444444,
+                "+$444,444.00");
+
+        assertFormatSingle(
+                "Sign Accounting-Always Negative",
+                "$USD sign=ACCOUNTING_ALWAYS",
+                NumberFormatter.with().sign(SignDisplay.ACCOUNTING_ALWAYS).unit(USD),
+                ULocale.ENGLISH,
+                -444444,
+                "($444,444.00)");
+    }
+
+    @Test
+    public void decimal() {
+        assertFormatDescending(
+                "Decimal Default",
+                "decimal=AUTO",
+                NumberFormatter.with().decimal(DecimalMarkDisplay.AUTO),
+                ULocale.ENGLISH,
+                "87,650",
+                "8,765",
+                "876.5",
+                "87.65",
+                "8.765",
+                "0.8765",
+                "0.08765",
+                "0.008765",
+                "0");
+
+        assertFormatDescending(
+                "Decimal Always Shown",
+                "decimal=ALWAYS",
+                NumberFormatter.with().decimal(DecimalMarkDisplay.ALWAYS),
+                ULocale.ENGLISH,
+                "87,650.",
+                "8,765.",
+                "876.5",
+                "87.65",
+                "8.765",
+                "0.8765",
+                "0.08765",
+                "0.008765",
+                "0.");
+    }
+
+    @Test
+    public void locale() {
+        // Coverage for the locale setters.
+        assertEquals(NumberFormatter.with().locale(ULocale.ENGLISH), NumberFormatter.with().locale(Locale.ENGLISH));
+        assertNotEquals(NumberFormatter.with().locale(ULocale.ENGLISH), NumberFormatter.with().locale(Locale.FRENCH));
+    }
+
+    @Test
+    public void getPrefixSuffix() {
+        Object[][] cases = {
+                { NumberFormatter.withLocale(ULocale.ENGLISH).unit(GBP).unitWidth(UnitWidth.ISO_CODE), "GBP", "", "-GBP",
+                        "" },
+                { NumberFormatter.withLocale(ULocale.ENGLISH).unit(GBP).unitWidth(UnitWidth.FULL_NAME), "",
+                        " British pounds", "-", " British pounds" } };
+
+        for (Object[] cas : cases) {
+            LocalizedNumberFormatter f = (LocalizedNumberFormatter) cas[0];
+            String posPrefix = (String) cas[1];
+            String posSuffix = (String) cas[2];
+            String negPrefix = (String) cas[3];
+            String negSuffix = (String) cas[4];
+            FormattedNumber positive = f.format(1);
+            FormattedNumber negative = f.format(-1);
+            assertEquals(posPrefix, positive.getPrefix());
+            assertEquals(posSuffix, positive.getSuffix());
+            assertEquals(negPrefix, negative.getPrefix());
+            assertEquals(negSuffix, negative.getSuffix());
+        }
+    }
+
+    @Test
+    public void plurals() {
+        // TODO: Expand this test.
+
+        assertFormatSingle(
+                "Plural 1",
+                "$USD F0 unit-width=FULL_NAME",
+                NumberFormatter.with().unit(USD).unitWidth(UnitWidth.FULL_NAME).rounding(Rounder.fixedFraction(0)),
+                ULocale.ENGLISH,
+                1,
+                "1 US dollar");
+
+        assertFormatSingle(
+                "Plural 1.00",
+                "$USD F2 unit-width=FULL_NAME",
+                NumberFormatter.with().unit(USD).unitWidth(UnitWidth.FULL_NAME).rounding(Rounder.fixedFraction(2)),
+                ULocale.ENGLISH,
+                1,
+                "1.00 US dollars");
+    }
+
+    private static void assertFormatDescending(
+            String message,
+            String skeleton,
+            UnlocalizedNumberFormatter f,
+            ULocale locale,
+            String... expected) {
+        assert expected.length == 9;
+        assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton());
+        final double[] inputs = new double[] { 87650, 8765, 876.5, 87.65, 8.765, 0.8765, 0.08765, 0.008765, 0 };
+        LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation
+        LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation
+        for (int i = 0; i < 9; i++) {
+            double d = inputs[i];
+            String actual1 = l1.format(d).toString();
+            assertEquals(message + ": L1: " + d, expected[i], actual1);
+            String actual2 = l2.format(d).toString();
+            assertEquals(message + ": L2: " + d, expected[i], actual2);
+        }
+    }
+
+    private static void assertFormatSingle(
+            String message,
+            String skeleton,
+            UnlocalizedNumberFormatter f,
+            ULocale locale,
+            Number input,
+            String expected) {
+        assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton());
+        LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation
+        LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation
+        String actual1 = l1.format(input).toString();
+        assertEquals(message + ": Unsafe Path: " + input, expected, actual1);
+        String actual2 = l2.format(input).toString();
+        assertEquals(message + ": Safe Path: " + input, expected, actual2);
+    }
+
+    private static void assertFormatSingleMeasure(
+            String message,
+            String skeleton,
+            UnlocalizedNumberFormatter f,
+            ULocale locale,
+            Measure input,
+            String expected) {
+        assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton());
+        LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation
+        LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation
+        String actual1 = l1.format(input).toString();
+        assertEquals(message + ": Unsafe Path: " + input, expected, actual1);
+        String actual2 = l2.format(input).toString();
+        assertEquals(message + ": Safe Path: " + input, expected, actual2);
     }
-  }
-
-  private static void assertFormatSingle(
-      String message,
-      String skeleton,
-      UnlocalizedNumberFormatter f,
-      ULocale locale,
-      Number input,
-      String expected) {
-    assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton());
-    LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation
-    LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation
-    String actual1 = l1.format(input).toString();
-    assertEquals(message + ": L1: " + input, expected, actual1);
-    String actual2 = l2.format(input).toString();
-    assertEquals(message + ": L2: " + input, expected, actual2);
-  }
-
-  private static void assertFormatSingleMeasure(
-      String message,
-      String skeleton,
-      UnlocalizedNumberFormatter f,
-      ULocale locale,
-      Measure input,
-      String expected) {
-    assertEquals(message + ": Skeleton:", skeleton, f.toSkeleton());
-    LocalizedNumberFormatter l1 = f.threshold(0L).locale(locale); // no self-regulation
-    LocalizedNumberFormatter l2 = f.threshold(1L).locale(locale); // all self-regulation
-    String actual1 = l1.format(input).toString();
-    assertEquals(message + ": L1: " + input, expected, actual1);
-    String actual2 = l2.format(input).toString();
-    assertEquals(message + ": L2: " + input, expected, actual2);
-  }
 }
index 009b8b2383dfb0565f9e4faadf581ad50fac60ad..8d9814cd697c40420e4093ae0f2a642d85d252f9 100644 (file)
@@ -183,6 +183,30 @@ public class NumberStringBuilderTest {
     }
   }
 
+  @Test
+  public void testCodePoints() {
+      NumberStringBuilder nsb = new NumberStringBuilder();
+      assertEquals("First is -1 on empty string", -1, nsb.getFirstCodePoint());
+      assertEquals("Last is -1 on empty string", -1, nsb.getLastCodePoint());
+      assertEquals("Length is 0 on empty string", 0, nsb.codePointCount());
+
+      nsb.append("q", null);
+      assertEquals("First is q", 'q', nsb.getFirstCodePoint());
+      assertEquals("Last is q", 'q', nsb.getLastCodePoint());
+      assertEquals("0th is q", 'q', nsb.codePointAt(0));
+      assertEquals("Before 1st is q", 'q', nsb.codePointBefore(1));
+      assertEquals("Code point count is 1", 1, nsb.codePointCount());
+
+      // 🚀 is two char16s
+      nsb.append("🚀", null);
+      assertEquals("First is still q", 'q', nsb.getFirstCodePoint());
+      assertEquals("Last is space ship", 128640, nsb.getLastCodePoint());
+      assertEquals("1st is space ship", 128640, nsb.codePointAt(1));
+      assertEquals("Before 1st is q", 'q', nsb.codePointBefore(1));
+      assertEquals("Before 3rd is space ship", 128640, nsb.codePointBefore(3));
+      assertEquals("Code point count is 2", 2, nsb.codePointCount());
+  }
+
   private static void assertCharSequenceEquals(CharSequence a, CharSequence b) {
     assertEquals(a.toString(), b.toString());
 
index b9bf2efe97f2b51cf28c7816a6b0b51caa18ad41..39b44f528a0f1d6d10d4310d5e67a38e8214928b 100644 (file)
@@ -7,7 +7,7 @@ import static org.junit.Assert.fail;
 
 import org.junit.Test;
 
-import com.ibm.icu.impl.number.PatternString;
+import com.ibm.icu.impl.number.PatternAndPropertyUtils;
 import com.ibm.icu.impl.number.Properties;
 import com.ibm.icu.text.DecimalFormatSymbols;
 import com.ibm.icu.util.ULocale;
@@ -27,8 +27,8 @@ public class PatternStringTest {
     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));
+    assertEquals(localized, PatternAndPropertyUtils.convertLocalized(standard, symbols, true));
+    assertEquals(toStandard, PatternAndPropertyUtils.convertLocalized(localized, symbols, false));
   }
 
   @Test
@@ -60,8 +60,8 @@ public class PatternStringTest {
       String input = cas[0];
       String output = cas[1];
 
-      Properties properties = PatternString.parseToProperties(input);
-      String actual = PatternString.propertiesToString(properties);
+      Properties properties = PatternAndPropertyUtils.parseToProperties(input);
+      String actual = PatternAndPropertyUtils.propertiesToString(properties);
       assertEquals(
           "Failed on input pattern '" + input + "', properties " + properties, output, actual);
     }
@@ -90,7 +90,7 @@ public class PatternStringTest {
       Properties input = (Properties) cas[0];
       String output = (String) cas[1];
 
-      String actual = PatternString.propertiesToString(input);
+      String actual = PatternAndPropertyUtils.propertiesToString(input);
       assertEquals("Failed on input properties " + input, output, actual);
     }
   }
@@ -103,7 +103,7 @@ public class PatternStringTest {
 
     for (String pattern : invalidPatterns) {
       try {
-        PatternString.parseToProperties(pattern);
+        PatternAndPropertyUtils.parseToProperties(pattern);
         fail("Didn't throw IllegalArgumentException when parsing pattern: " + pattern);
       } catch (IllegalArgumentException e) {
       }
@@ -112,8 +112,8 @@ public class PatternStringTest {
 
   @Test
   public void testBug13117() {
-    Properties expected = PatternString.parseToProperties("0");
-    Properties actual = PatternString.parseToProperties("0;");
+    Properties expected = PatternAndPropertyUtils.parseToProperties("0");
+    Properties actual = PatternAndPropertyUtils.parseToProperties("0;");
     assertEquals("Should not consume negative subpattern", expected, actual);
   }
 }
index d8e786a9a948a73afd10c6baf07f00717208bbdc..25546fe3389b36dd5680e6225fa4b61b601432e5 100644 (file)
@@ -31,7 +31,7 @@ import org.junit.Test;
 import com.ibm.icu.dev.test.serializable.SerializableTestUtility;
 import com.ibm.icu.impl.number.Parse.GroupingMode;
 import com.ibm.icu.impl.number.Parse.ParseMode;
-import com.ibm.icu.impl.number.PatternString;
+import com.ibm.icu.impl.number.PatternAndPropertyUtils;
 import com.ibm.icu.impl.number.Properties;
 import com.ibm.icu.impl.number.ThingsNeedingNewHome.PadPosition;
 import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
@@ -310,7 +310,7 @@ public class PropertiesTest {
     Properties props0 = new Properties();
 
     // Write values to some of the fields
-    PatternString.parseToExistingProperties("A-**####,#00.00#b¤", props0);
+    PatternAndPropertyUtils.parseToExistingProperties("A-**####,#00.00#b¤", props0);
 
     // Write to byte stream
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -337,7 +337,7 @@ public class PropertiesTest {
     public Object[] getTestObjects() {
       return new Object[] {
         new Properties(),
-        PatternString.parseToProperties("x#,##0.00%"),
+        PatternAndPropertyUtils.parseToProperties("x#,##0.00%"),
         new Properties().setCompactStyle(CompactStyle.LONG).setMinimumExponentDigits(2)
       };
     }
index 82a5e8e34116297730c73d46a774756f23ea5ed8..fb1cd7b7641c0cbf94636011153243f2c5d7b695 100644 (file)
@@ -827,7 +827,7 @@ public class SerializableTestUtility {
         map.put("com.ibm.icu.text.PluralRules$FixedDecimal", new PluralRulesTest.FixedDecimalHandler());
         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.util.Dimensionless", new MeasureUnitTest.MeasureUnitHandler());
+        map.put("com.ibm.icu.util.NoUnit", new MeasureUnitTest.MeasureUnitHandler());
         map.put("com.ibm.icu.text.MeasureFormat", new MeasureUnitTest.MeasureFormatHandler());
         map.put("com.ibm.icu.impl.number.Properties", new PropertiesTest.PropertiesHandler());