]> granicus.if.org Git - icu/commitdiff
ICU-13177 Adding new number formatting code to repository.
authorShane Carr <shane@unicode.org>
Sat, 5 Aug 2017 01:02:35 +0000 (01:02 +0000)
committerShane Carr <shane@unicode.org>
Sat, 5 Aug 2017 01:02:35 +0000 (01:02 +0000)
X-SVN-Rev: 40315

29 files changed:
icu4j/main/classes/core/src/com/ibm/icu/impl/number/LdmlPatternInfo.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/NumberFormatter.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/demo.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/AffixPatternProvider.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/CompactData.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/CompactImpl.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/CurrencySpacingEnabledModifier.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/CustomSymbolCurrency.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/DataUtils.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/GroupingImpl.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/IntegerWidthImpl.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/MacroProps.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/MeasureData.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/MicroProps.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/MultiplierImpl.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/MurkyModifier.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/NotationImpl.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/NumberFormatterImpl.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/NumberPropertyMapper.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/PaddingImpl.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/PositiveDecimalImpl.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/QuantityChain.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/QuantityDependentModOuter.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/RoundingImpl.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/ScientificImpl.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/SkeletonBuilder.java [new file with mode: 0644]
icu4j/main/classes/core/src/newapi/impl/Worker1.java [new file with mode: 0644]
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/MurkyModifierTest.java [new file with mode: 0644]
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterTest.java [new file with mode: 0644]

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
new file mode 100644 (file)
index 0000000..ef0781a
--- /dev/null
@@ -0,0 +1,447 @@
+// © 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.formatters.PaddingFormat.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++;
+    }
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/NumberFormatter.java b/icu4j/main/classes/core/src/newapi/NumberFormatter.java
new file mode 100644 (file)
index 0000000..affce44
--- /dev/null
@@ -0,0 +1,684 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.text.AttributedCharacterIterator;
+import java.text.FieldPosition;
+import java.util.Arrays;
+import java.util.Locale;
+
+import com.ibm.icu.impl.number.FormatQuantityBCD;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
+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.text.PluralRules.IFixedDecimal;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyUsage;
+import com.ibm.icu.util.CurrencyAmount;
+import com.ibm.icu.util.ICUUncheckedIOException;
+import com.ibm.icu.util.Measure;
+import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.ULocale;
+
+import newapi.impl.GroupingImpl;
+import newapi.impl.IntegerWidthImpl;
+import newapi.impl.MicroProps;
+import newapi.impl.NotationImpl.NotationCompactImpl;
+import newapi.impl.NotationImpl.NotationScientificImpl;
+import newapi.impl.NumberFormatterImpl;
+import newapi.impl.PaddingImpl;
+import newapi.impl.RoundingImpl.RoundingImplCurrency;
+import newapi.impl.RoundingImpl.RoundingImplFraction;
+import newapi.impl.RoundingImpl.RoundingImplIncrement;
+import newapi.impl.RoundingImpl.RoundingImplInfinity;
+import newapi.impl.RoundingImpl.RoundingImplSignificant;
+
+public final class NumberFormatter {
+
+  public interface IRounding {
+    public BigDecimal round(BigDecimal input);
+  }
+
+  public interface IGrouping {
+    public boolean groupAtPosition(int position, BigDecimal input);
+  }
+
+  // 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_SHOWN,
+  }
+
+  public static enum SignDisplay {
+    AUTO,
+    ALWAYS_SHOWN,
+    NEVER_SHOWN,
+  }
+
+  public static class UnlocalizedNumberFormatter {
+
+    public UnlocalizedNumberFormatter notation(Notation notation) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    public UnlocalizedNumberFormatter unit(MeasureUnit unit) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    public UnlocalizedNumberFormatter rounding(IRounding rounding) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    public UnlocalizedNumberFormatter grouping(IGrouping grouping) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    public UnlocalizedNumberFormatter padding(Padding padding) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    public UnlocalizedNumberFormatter integerWidth(IntegerWidth style) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    public UnlocalizedNumberFormatter symbols(DecimalFormatSymbols symbols) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    public UnlocalizedNumberFormatter symbols(NumberingSystem ns) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    public UnlocalizedNumberFormatter unitWidth(FormatWidth style) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    public UnlocalizedNumberFormatter sign(SignDisplay style) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    public UnlocalizedNumberFormatter decimal(DecimalMarkDisplay style) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    public LocalizedNumberFormatter locale(Locale locale) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    public LocalizedNumberFormatter locale(ULocale locale) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    public String toSkeleton() {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    // Prevent external subclassing with private constructor
+    private UnlocalizedNumberFormatter() {}
+  }
+
+  public static class LocalizedNumberFormatter extends UnlocalizedNumberFormatter {
+
+    @Override
+    public UnlocalizedNumberFormatter notation(Notation notation) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    @Override
+    public LocalizedNumberFormatter unit(MeasureUnit unit) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    @Override
+    public LocalizedNumberFormatter rounding(IRounding rounding) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    @Override
+    public LocalizedNumberFormatter grouping(IGrouping grouping) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    @Override
+    public LocalizedNumberFormatter padding(Padding padding) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    @Override
+    public LocalizedNumberFormatter integerWidth(IntegerWidth style) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    @Override
+    public LocalizedNumberFormatter symbols(DecimalFormatSymbols symbols) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    @Override
+    public LocalizedNumberFormatter symbols(NumberingSystem ns) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    @Override
+    public LocalizedNumberFormatter unitWidth(FormatWidth style) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    @Override
+    public LocalizedNumberFormatter sign(SignDisplay style) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    @Override
+    public LocalizedNumberFormatter decimal(DecimalMarkDisplay style) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    public NumberFormatterResult format(long input) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    public NumberFormatterResult format(double input) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    public NumberFormatterResult format(Number input) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    public NumberFormatterResult format(Measure input) {
+      throw new AssertionError("See NumberFormatterImpl");
+    }
+
+    // Prevent external subclassing with private constructor
+    private LocalizedNumberFormatter() {}
+
+    /**
+     * @internal
+     * @deprecated This API is for ICU internal use only.
+     */
+    @Deprecated
+    public static class Internal extends LocalizedNumberFormatter {}
+  }
+
+  public static UnlocalizedNumberFormatter fromSkeleton(String skeleton) {
+    // FIXME
+    throw new UnsupportedOperationException();
+  }
+
+  public static UnlocalizedNumberFormatter with() {
+    return NumberFormatterImpl.with();
+  }
+
+  public static LocalizedNumberFormatter withLocale(Locale locale) {
+    return NumberFormatterImpl.with().locale(locale);
+  }
+
+  public static LocalizedNumberFormatter withLocale(ULocale locale) {
+    return NumberFormatterImpl.with().locale(locale);
+  }
+
+  public static class NumberFormatterResult {
+    NumberStringBuilder nsb;
+    FormatQuantityBCD fq;
+    MicroProps micros;
+
+    /**
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    @Deprecated
+    public NumberFormatterResult(NumberStringBuilder nsb, FormatQuantityBCD fq, MicroProps micros) {
+      this.nsb = nsb;
+      this.fq = fq;
+      this.micros = micros;
+    }
+
+    @Override
+    public String toString() {
+      return nsb.toString();
+    }
+
+    public <A extends Appendable> A appendTo(A appendable) {
+      try {
+        appendable.append(nsb);
+      } catch (IOException e) {
+        // Throw as an unchecked exception to avoid users needing try/catch
+        throw new ICUUncheckedIOException(e);
+      }
+      return appendable;
+    }
+
+    public AttributedCharacterIterator toAttributedCharacterIterator() {
+      return nsb.getIterator();
+    }
+
+    /**
+     * @internal
+     * @deprecated This API a technology preview. It is not stable and may change or go away in an
+     *     upcoming release.
+     */
+    @Deprecated
+    public void populateFieldPosition(FieldPosition fieldPosition, int offset) {
+      nsb.populateFieldPosition(fieldPosition, offset);
+      fq.populateUFieldPosition(fieldPosition);
+    }
+
+    /**
+     * @internal
+     * @deprecated This API a technology preview. It is not stable and may change or go away in an
+     *     upcoming release.
+     */
+    @Deprecated
+    public String getPrefix() {
+      return micros.modOuter.getPrefix()
+          + micros.modMiddle.getPrefix()
+          + micros.modInner.getPrefix();
+    }
+
+    /**
+     * @internal
+     * @deprecated This API a technology preview. It is not stable and may change or go away in an
+     *     upcoming release.
+     */
+    @Deprecated
+    public String getSuffix() {
+      return micros.modInner.getSuffix()
+          + micros.modMiddle.getSuffix()
+          + micros.modOuter.getSuffix();
+    }
+
+    /**
+     * @internal
+     * @deprecated This API a technology preview. It is not stable and may change or go away in an
+     *     upcoming release.
+     */
+    @Deprecated
+    public IFixedDecimal getFixedDecimal() {
+      return fq;
+    }
+
+    public BigDecimal toBigDecimal() {
+      return fq.toBigDecimal();
+    }
+
+    @Override
+    public int hashCode() {
+      // NumberStringBuilder and BigDecimal are mutable, so we can't call
+      // #equals() or #hashCode() on them directly.
+      return Arrays.hashCode(nsb.toCharArray())
+          ^ Arrays.hashCode(nsb.toFieldArray())
+          ^ fq.toBigDecimal().hashCode();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (this == other) return true;
+      if (other == null) return false;
+      if (!(other instanceof NumberFormatterResult)) return false;
+      // NumberStringBuilder and BigDecimal are mutable, so we can't call
+      // #equals() or #hashCode() on them directly.
+      NumberFormatterResult _other = (NumberFormatterResult) other;
+      return Arrays.equals(nsb.toCharArray(), _other.nsb.toCharArray())
+          ^ Arrays.equals(nsb.toFieldArray(), _other.nsb.toFieldArray())
+          ^ fq.toBigDecimal().equals(_other.fq.toBigDecimal());
+    }
+  }
+
+  public static class Notation {
+
+    // FIXME: Support engineering intervals other than 3?
+    public static final NotationScientific SCIENTIFIC = new NotationScientificImpl(1);
+    public static final NotationScientific ENGINEERING = new NotationScientificImpl(3);
+    public static final NotationCompact COMPACT_SHORT = new NotationCompactImpl(CompactStyle.SHORT);
+    public static final NotationCompact COMPACT_LONG = new NotationCompactImpl(CompactStyle.LONG);
+    public static final NotationSimple SIMPLE = new NotationSimple();
+
+    // Prevent subclassing
+    private Notation() {}
+  }
+
+  @SuppressWarnings("unused")
+  public static class NotationScientific extends Notation {
+
+    public NotationScientific withMinExponentDigits(int minExponentDigits) {
+      // Overridden in NotationImpl
+      throw new AssertionError();
+    }
+
+    public NotationScientific withExponentSignDisplay(SignDisplay exponentSignDisplay) {
+      // Overridden in NotationImpl
+      throw new AssertionError();
+    }
+
+    // Prevent subclassing
+    private NotationScientific() {}
+
+    /**
+     * @internal
+     * @deprecated This API is for ICU internal use only.
+     */
+    @Deprecated
+    public static class Internal extends NotationScientific {}
+  }
+
+  public static class NotationCompact extends Notation {
+
+    // Prevent subclassing
+    private NotationCompact() {}
+
+    /**
+     * @internal
+     * @deprecated This API is for ICU internal use only.
+     */
+    @Deprecated
+    public static class Internal extends NotationCompact {}
+  }
+
+  public static class NotationSimple extends Notation {
+    // Prevent subclassing
+    private NotationSimple() {}
+  }
+
+  public static class Rounding implements IRounding {
+
+    protected static final int MAX_VALUE = 100;
+
+    public static final Rounding NONE = new RoundingImplInfinity();
+    public static final Rounding INTEGER = new RoundingImplFraction();
+
+    public static FractionRounding fixedFraction(int minMaxFrac) {
+      if (minMaxFrac >= 0 && minMaxFrac <= MAX_VALUE) {
+        return RoundingImplFraction.getInstance(minMaxFrac, minMaxFrac);
+      } else {
+        throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE);
+      }
+    }
+
+    public static FractionRounding minFraction(int minFrac) {
+      if (minFrac >= 0 && minFrac < MAX_VALUE) {
+        return RoundingImplFraction.getInstance(minFrac, Integer.MAX_VALUE);
+      } else {
+        throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE);
+      }
+    }
+
+    public static FractionRounding maxFraction(int maxFrac) {
+      if (maxFrac >= 0 && maxFrac < MAX_VALUE) {
+        return RoundingImplFraction.getInstance(0, maxFrac);
+      } else {
+        throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE);
+      }
+    }
+
+    public static FractionRounding minMaxFraction(int minFrac, int maxFrac) {
+      if (minFrac >= 0 && maxFrac <= MAX_VALUE && minFrac <= maxFrac) {
+        return RoundingImplFraction.getInstance(minFrac, maxFrac);
+      } else {
+        throw new IllegalArgumentException("Fraction length must be between 0 and " + MAX_VALUE);
+      }
+    }
+
+    public static Rounding fixedFigures(int minMaxSig) {
+      if (minMaxSig > 0 && minMaxSig <= MAX_VALUE) {
+        return RoundingImplSignificant.getInstance(minMaxSig, minMaxSig);
+      } else {
+        throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
+      }
+    }
+
+    public static Rounding minFigures(int minSig) {
+      if (minSig > 0 && minSig <= MAX_VALUE) {
+        return RoundingImplSignificant.getInstance(minSig, Integer.MAX_VALUE);
+      } else {
+        throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
+      }
+    }
+
+    public static Rounding maxFigures(int maxSig) {
+      if (maxSig > 0 && maxSig <= MAX_VALUE) {
+        return RoundingImplSignificant.getInstance(0, maxSig);
+      } else {
+        throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
+      }
+    }
+
+    public static Rounding minMaxFigures(int minSig, int maxSig) {
+      if (minSig > 0 && maxSig <= MAX_VALUE && minSig <= maxSig) {
+        return RoundingImplSignificant.getInstance(minSig, maxSig);
+      } else {
+        throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
+      }
+    }
+
+    public static Rounding increment(BigDecimal roundingIncrement) {
+      if (roundingIncrement == null) {
+        throw new IllegalArgumentException("Rounding increment must be non-null");
+      } else if (roundingIncrement.compareTo(BigDecimal.ZERO) <= 0) {
+        throw new IllegalArgumentException("Rounding increment must be positive");
+      } else {
+        return RoundingImplIncrement.getInstance(roundingIncrement);
+      }
+    }
+
+    public static CurrencyRounding currency(CurrencyUsage currencyUsage) {
+      if (currencyUsage != CurrencyUsage.STANDARD && currencyUsage != CurrencyUsage.CASH) {
+        throw new IllegalArgumentException("Unknown CurrencyUsage: " + currencyUsage);
+      } else {
+        return RoundingImplCurrency.getInstance(currencyUsage);
+      }
+    }
+
+    /**
+     * Sets the {@link java.math.RoundingMode} to use when picking the direction to round (up or
+     * down).
+     *
+     * <p>Common values include {@link RoundingMode#HALF_EVEN}, {@link RoundingMode#HALF_UP}, and
+     * {@link RoundingMode#CEILING}. The default is HALF_EVEN.
+     *
+     * @param roundingMode The RoundingMode to use.
+     * @return An immutable object for chaining.
+     */
+    public Rounding withMode(RoundingMode roundingMode) {
+      // Overridden in RoundingImpl
+      throw new AssertionError();
+    }
+
+    /**
+     * Sets a MathContext directly instead of RoundingMode.
+     *
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    @Deprecated
+    public Rounding withMode(MathContext mathContext) {
+      // Overridden in RoundingImpl
+      throw new AssertionError();
+    }
+
+    @Override
+    public BigDecimal round(BigDecimal input) {
+      // Overridden in RoundingImpl
+      throw new AssertionError();
+    }
+
+    // Prevent subclassing
+    private Rounding() {}
+
+    /**
+     * @internal
+     * @deprecated This API is for ICU internal use only.
+     */
+    @Deprecated
+    public static class Internal extends Rounding {}
+  }
+
+  /**
+   * A rounding strategy based on a minimum and/or maximum number of fraction digits. Allows for a
+   * minimum or maximum number of significant digits to be specified.
+   */
+  public static class FractionRounding extends Rounding {
+    /**
+     * Ensures that no less than this number of significant figures are retained when rounding
+     * according to fraction rules.
+     *
+     * <p>For example, with integer rounding, the number 3.141 becomes "3". However, with minimum
+     * figures set to 2, 3.141 becomes "3.1" instead.
+     *
+     * <p>This setting does not affect the number of trailing zeros. For example, 3.01 would print
+     * as "3", not "3.0".
+     *
+     * @param minFigures The number of significant figures to guarantee.
+     * @return An immutable object for chaining.
+     */
+    public Rounding withMinFigures(int minFigures) {
+      // Overridden in RoundingImpl
+      throw new AssertionError();
+    }
+
+    /**
+     * Ensures that no more than this number of significant figures are retained when rounding
+     * according to fraction rules.
+     *
+     * <p>For example, with integer rounding, the number 123.4 becomes "123". However, with maximum
+     * figures set to 2, 123.4 becomes "120" instead.
+     *
+     * <p>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
+     * @return An immutable object for chaining.
+     */
+    public Rounding withMaxFigures(int maxFigures) {
+      // Overridden in RoundingImpl
+      throw new AssertionError();
+    }
+
+    // Prevent subclassing
+    private FractionRounding() {}
+
+    /**
+     * @internal
+     * @deprecated This API is for ICU internal use only.
+     */
+    @Deprecated
+    public static class Internal extends FractionRounding {}
+  }
+
+  /** A rounding strategy parameterized by a currency. */
+  public static class CurrencyRounding extends Rounding {
+    /**
+     * Associates a {@link com.ibm.icu.util.Currency} with this rounding strategy. Only applies to
+     * rounding strategies returned from {@link #currency(CurrencyUsage)}.
+     *
+     * <p><strong>Calling this method is <em>not required</em></strong>, because the currency
+     * specified in {@link NumberFormatter#unit(MeasureUnit)} or via a {@link CurrencyAmount} passed
+     * into {@link LocalizedNumberFormatter#format(Measure)} is automatically applied to currency
+     * rounding strategies. However, this method enables you to override that automatic association.
+     *
+     * <p>This method also enables numbers to be formatted using currency rounding rules without
+     * explicitly using a currency format.
+     *
+     * @param currency The currency to associate with this rounding strategy.
+     * @return An immutable object for chaining.
+     */
+    public Rounding withCurrency(Currency currency) {
+      // Overridden in RoundingImpl
+      throw new AssertionError();
+    }
+
+    // Prevent subclassing
+    private CurrencyRounding() {}
+
+    /**
+     * @internal
+     * @deprecated This API is for ICU internal use only.
+     */
+    @Deprecated
+    public static class Internal extends CurrencyRounding {}
+  }
+
+  public static class Grouping implements IGrouping {
+
+    public static final Grouping DEFAULT = new GroupingImpl(GroupingImpl.TYPE_PLACEHOLDER);
+    public static final Grouping DEFAULT_MIN_2_DIGITS = new GroupingImpl(GroupingImpl.TYPE_MIN2);
+    public static final Grouping NONE = new GroupingImpl(GroupingImpl.TYPE_NONE);
+
+    @Override
+    public boolean groupAtPosition(int position, BigDecimal input) {
+      throw new UnsupportedOperationException(
+          "This grouping strategy cannot be used outside of number formatting.");
+    }
+
+    // Prevent subclassing
+    private Grouping() {}
+
+    /**
+     * @internal
+     * @deprecated This API is for ICU internal use only.
+     */
+    @Deprecated
+    public static class Internal extends Grouping {}
+  }
+
+  public static class Padding {
+
+    public static final Padding NONE = new PaddingImpl();
+
+    public static Padding codePoints(int cp, int targetWidth, PadPosition position) {
+      String paddingString = String.valueOf(Character.toChars(cp));
+      return PaddingImpl.getInstance(paddingString, targetWidth, position);
+    }
+
+    // Prevent subclassing
+    private Padding() {}
+
+    /**
+     * @internal
+     * @deprecated This API is for ICU internal use only.
+     */
+    @Deprecated
+    public static class Internal extends Padding {}
+  }
+
+  @SuppressWarnings("unused")
+  public static class IntegerWidth {
+
+    public static final IntegerWidth DEFAULT = new IntegerWidthImpl();
+
+    public static IntegerWidth zeroFillTo(int minInt) {
+      return new IntegerWidthImpl(minInt, Integer.MAX_VALUE);
+    }
+
+    public IntegerWidth truncateAt(int maxInt) {
+      // Implemented in IntegerWidthImpl
+      throw new AssertionError();
+    }
+
+    // Prevent subclassing
+    private IntegerWidth() {}
+
+    /**
+     * @internal
+     * @deprecated This API is for ICU internal use only.
+     */
+    @Deprecated
+    public static class Internal extends IntegerWidth {}
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/demo.java b/icu4j/main/classes/core/src/newapi/demo.java
new file mode 100644 (file)
index 0000000..6a4ae8e
--- /dev/null
@@ -0,0 +1,63 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi;
+
+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.ULocale;
+
+import newapi.NumberFormatter.DecimalMarkDisplay;
+import newapi.NumberFormatter.Grouping;
+import newapi.NumberFormatter.Notation;
+import newapi.NumberFormatter.Rounding;
+import newapi.NumberFormatter.SignDisplay;
+import newapi.NumberFormatter.UnlocalizedNumberFormatter;
+
+public class demo {
+  public static void main(String[] args) {
+    System.out.println(NumberingSystem.LATIN.getDescription());
+    UnlocalizedNumberFormatter formatter =
+        NumberFormatter.with()
+            .notation(Notation.COMPACT_SHORT)
+            .notation(Notation.SCIENTIFIC.withExponentSignDisplay(SignDisplay.ALWAYS_SHOWN))
+            .notation(Notation.ENGINEERING.withMinExponentDigits(2))
+            .notation(Notation.SIMPLE)
+            .unit(Currency.getInstance("GBP"))
+            .unit(Dimensionless.PERCENT)
+            .unit(MeasureUnit.CUBIC_METER)
+            .unitWidth(FormatWidth.SHORT)
+            // .rounding(Rounding.fixedSignificantDigits(3))
+//            .rounding(
+//                (BigDecimal input) -> {
+//                  return input.divide(new BigDecimal("0.02"), 0).multiply(new BigDecimal("0.02"));
+//                })
+            .rounding(Rounding.fixedFraction(2).withMode(RoundingMode.HALF_UP))
+            .rounding(Rounding.INTEGER.withMode(RoundingMode.CEILING))
+            .rounding(Rounding.currency(CurrencyUsage.STANDARD))
+//            .grouping(
+//                (int position, BigDecimal number) -> {
+//                  return (position % 3) == 0;
+//                })
+            .grouping(Grouping.DEFAULT)
+            .grouping(Grouping.NONE)
+            .grouping(Grouping.DEFAULT_MIN_2_DIGITS)
+            // .padding(Padding.codePoints(' ', 8, PadPosition.AFTER_PREFIX))
+            .sign(SignDisplay.ALWAYS_SHOWN)
+            .decimal(DecimalMarkDisplay.ALWAYS_SHOWN)
+            .symbols(DecimalFormatSymbols.getInstance(new ULocale("fr@digits=ascii")))
+            .symbols(NumberingSystem.getInstanceByName("arab"))
+            .symbols(NumberingSystem.LATIN);
+    System.out.println(formatter.toSkeleton());
+    System.out.println(formatter.locale(ULocale.ENGLISH).format(0.98381).toString());
+    //            .locale(Locale.ENGLISH)
+    //            .format(123.45)
+    //            .toString();
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/AffixPatternProvider.java b/icu4j/main/classes/core/src/newapi/impl/AffixPatternProvider.java
new file mode 100644 (file)
index 0000000..87554b7
--- /dev/null
@@ -0,0 +1,26 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+public interface AffixPatternProvider {
+  public static final class Flags {
+    public static final int PLURAL_MASK = 0xff;
+    public static final int PREFIX = 0x100;
+    public static final int NEGATIVE_SUBPATTERN = 0x200;
+    public static final int PADDING = 0x400;
+  }
+
+  public char charAt(int flags, int i);
+
+  public int length(int flags);
+
+  public boolean hasCurrencySign();
+
+  public boolean positiveHasPlusSign();
+
+  public boolean hasNegativeSubpattern();
+
+  public boolean negativeHasMinusSign();
+
+  public boolean containsSymbolType(int type);
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/CompactData.java b/icu4j/main/classes/core/src/newapi/impl/CompactData.java
new file mode 100644 (file)
index 0000000..f18ce45
--- /dev/null
@@ -0,0 +1,276 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.MissingResourceException;
+import java.util.Set;
+
+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.CompactDecimalFormat.CompactStyle;
+import com.ibm.icu.text.CompactDecimalFormat.CompactType;
+import com.ibm.icu.text.NumberingSystem;
+import com.ibm.icu.util.ULocale;
+import com.ibm.icu.util.UResourceBundle;
+
+class CompactData implements RoundingImpl.MultiplierProducer {
+
+  public static CompactData getInstance(
+      ULocale locale, CompactType compactType, CompactStyle compactStyle) {
+    // TODO: Add a data cache? It would be keyed by locale, compact type, and compact style.
+    CompactData data = new CompactData();
+    CompactDataSink sink = new CompactDataSink(data, compactType, compactStyle);
+    String nsName = NumberingSystem.getInstance(locale).getName();
+    ICUResourceBundle rb =
+        (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale);
+    CompactData.internalPopulateData(nsName, rb, sink, data);
+    if (data.isEmpty() && compactStyle == CompactStyle.LONG) {
+      // No long data is available; load short data instead
+      sink.compactStyle = CompactStyle.SHORT;
+      CompactData.internalPopulateData(nsName, rb, sink, data);
+    }
+    return data;
+  }
+
+  public static CompactData getInstance(
+      Map<String, Map<String, String>> powersToPluralsToPatterns) {
+    CompactData data = new CompactData();
+    for (Map.Entry<String, Map<String, String>> magnitudeEntry :
+        powersToPluralsToPatterns.entrySet()) {
+      byte magnitude = (byte) (magnitudeEntry.getKey().length() - 1);
+      for (Map.Entry<String, String> pluralEntry : magnitudeEntry.getValue().entrySet()) {
+        StandardPlural plural = StandardPlural.fromString(pluralEntry.getKey().toString());
+        String patternString = pluralEntry.getValue().toString();
+        data.setPattern(patternString, magnitude, plural);
+        int numZeros = countZeros(patternString);
+        if (numZeros > 0) { // numZeros==0 in certain cases, like Somali "Kun"
+          data.setMultiplier(magnitude, (byte) (numZeros - magnitude - 1));
+        }
+      }
+    }
+    return data;
+  }
+
+  private static void internalPopulateData(
+      String nsName, ICUResourceBundle rb, CompactDataSink sink, CompactData data) {
+    try {
+      rb.getAllItemsWithFallback("NumberElements/" + nsName, sink);
+    } catch (MissingResourceException e) {
+      // Fall back to latn
+    }
+    if (data.isEmpty() && !nsName.equals("latn")) {
+      rb.getAllItemsWithFallback("NumberElements/latn", sink);
+    }
+    if (sink.exception != null) {
+      throw sink.exception;
+    }
+  }
+
+  // A dummy object used when a "0" compact decimal entry is encountered.  This is necessary
+  // in order to prevent falling back to root.  Object equality ("==") is intended.
+  private static final String USE_FALLBACK = "<USE FALLBACK>";
+
+  private final String[] patterns;
+  private final byte[] multipliers;
+  private boolean isEmpty;
+  private int largestMagnitude;
+
+  private static final int MAX_DIGITS = 15;
+
+  private CompactData() {
+    patterns = new String[(CompactData.MAX_DIGITS + 1) * StandardPlural.COUNT];
+    multipliers = new byte[CompactData.MAX_DIGITS + 1];
+    isEmpty = true;
+    largestMagnitude = 0;
+  }
+
+  public boolean isEmpty() {
+    return isEmpty;
+  }
+
+  @Override
+  public int getMultiplier(int magnitude) {
+    if (magnitude < 0) {
+      return 0;
+    }
+    if (magnitude > largestMagnitude) {
+      magnitude = largestMagnitude;
+    }
+    return multipliers[magnitude];
+  }
+
+  /** Returns the multiplier from the array directly without bounds checking. */
+  public int getMultiplierDirect(int magnitude) {
+    return multipliers[magnitude];
+  }
+
+  private void setMultiplier(int magnitude, byte multiplier) {
+    if (multipliers[magnitude] != 0) {
+      assert multipliers[magnitude] == multiplier;
+      return;
+    }
+    multipliers[magnitude] = multiplier;
+    isEmpty = false;
+    if (magnitude > largestMagnitude) largestMagnitude = magnitude;
+  }
+
+  public String getPattern(int magnitude, StandardPlural plural) {
+    if (magnitude < 0) {
+      return null;
+    }
+    if (magnitude > largestMagnitude) {
+      magnitude = largestMagnitude;
+    }
+    String patternString = patterns[getIndex(magnitude, plural)];
+    if (patternString == null && plural != StandardPlural.OTHER) {
+      // Fall back to "other" plural variant
+      patternString = patterns[getIndex(magnitude, StandardPlural.OTHER)];
+    }
+    if (patternString == USE_FALLBACK) {
+      // Return null if USE_FALLBACK is present
+      patternString = null;
+    }
+    return patternString;
+  }
+
+  public Set<String> getAllPatterns() {
+    Set<String> result = new HashSet<String>();
+    result.addAll(Arrays.asList(patterns));
+    result.remove(USE_FALLBACK);
+    result.remove(null);
+    return result;
+  }
+
+  private boolean has(int magnitude, StandardPlural plural) {
+    // Return true if USE_FALLBACK is present
+    return patterns[getIndex(magnitude, plural)] != null;
+  }
+
+  private void setPattern(String patternString, int magnitude, StandardPlural plural) {
+    patterns[getIndex(magnitude, plural)] = patternString;
+    isEmpty = false;
+    if (magnitude > largestMagnitude) largestMagnitude = magnitude;
+  }
+
+  private void setNoFallback(int magnitude, StandardPlural plural) {
+    setPattern(USE_FALLBACK, magnitude, plural);
+  }
+
+  private static final int getIndex(int magnitude, StandardPlural plural) {
+    return magnitude * StandardPlural.COUNT + plural.ordinal();
+  }
+
+  private static final class CompactDataSink extends UResource.Sink {
+
+    CompactData data;
+    CompactStyle compactStyle;
+    CompactType compactType;
+    IllegalArgumentException exception;
+
+    /*
+     * NumberElements{              <-- top (numbering system table)
+     *  latn{                       <-- patternsTable (one per numbering system)
+     *    patternsLong{             <-- formatsTable (one per pattern)
+     *      decimalFormat{          <-- powersOfTenTable (one per format)
+     *        1000{                 <-- pluralVariantsTable (one per power of ten)
+     *          one{"0 thousand"}   <-- plural variant and template
+     */
+
+    public CompactDataSink(CompactData data, CompactType compactType, CompactStyle compactStyle) {
+      this.data = data;
+      this.compactType = compactType;
+      this.compactStyle = compactStyle;
+    }
+
+    @Override
+    public void put(UResource.Key key, UResource.Value value, boolean isRoot) {
+      UResource.Table patternsTable = value.getTable();
+      for (int i1 = 0; patternsTable.getKeyAndValue(i1, key, value); ++i1) {
+        if (key.contentEquals("patternsShort") && compactStyle == CompactStyle.SHORT) {
+        } else if (key.contentEquals("patternsLong") && compactStyle == CompactStyle.LONG) {
+        } else {
+          continue;
+        }
+
+        // traverse into the table of formats
+        UResource.Table formatsTable = value.getTable();
+        for (int i2 = 0; formatsTable.getKeyAndValue(i2, key, value); ++i2) {
+          if (key.contentEquals("decimalFormat") && compactType == CompactType.DECIMAL) {
+          } else if (key.contentEquals("currencyFormat") && compactType == CompactType.CURRENCY) {
+          } else {
+            continue;
+          }
+
+          // traverse into the table of powers of ten
+          UResource.Table powersOfTenTable = value.getTable();
+          for (int i3 = 0; powersOfTenTable.getKeyAndValue(i3, key, value); ++i3) {
+
+            // Assumes that the keys are always of the form "10000" where the magnitude is the
+            // length of the key minus one
+            byte magnitude = (byte) (key.length() - 1);
+            byte multiplier = (byte) data.getMultiplierDirect(magnitude);
+
+            // Silently ignore divisors that are too big.
+            if (magnitude >= CompactData.MAX_DIGITS) continue;
+
+            // Iterate over the plural variants ("one", "other", etc)
+            UResource.Table pluralVariantsTable = value.getTable();
+            for (int i4 = 0; pluralVariantsTable.getKeyAndValue(i4, key, value); ++i4) {
+
+              // Skip this magnitude/plural if we already have it from a child locale.
+              StandardPlural plural = StandardPlural.fromString(key.toString());
+              if (data.has(magnitude, plural)) {
+                continue;
+              }
+
+              // The value "0" means that we need to use the default pattern and not fall back
+              // to parent locales.  Example locale where this is relevant: 'it'.
+              String patternString = value.toString();
+              if (patternString.equals("0")) {
+                data.setNoFallback(magnitude, plural);
+                continue;
+              }
+
+              // Save the pattern string.  We will parse it lazily.
+              data.setPattern(patternString, magnitude, plural);
+
+              // If necessary, compute the multiplier: the difference between the magnitude
+              // and the number of zeros in the pattern.
+              if (multiplier == 0) {
+                int numZeros = countZeros(patternString);
+                if (numZeros > 0) { // numZeros==0 in certain cases, like Somali "Kun"
+                  multiplier = (byte) (numZeros - magnitude - 1);
+                }
+              }
+            }
+
+            data.setMultiplier(magnitude, multiplier);
+          }
+
+          // We want only one table of compact decimal formats, so if we get here, stop consuming.
+          // The data.isEmpty() check will prevent further bundles from being traversed.
+          return;
+        }
+      }
+    }
+  }
+
+  private static final int countZeros(String patternString) {
+    // NOTE: This strategy for computing the number of zeros is a hack for efficiency.
+    // It could break if there are any 0s that aren't part of the main pattern.
+    int numZeros = 0;
+    for (int i = 0; i < patternString.length(); i++) {
+      if (patternString.charAt(i) == '0') {
+        numZeros++;
+      } else if (numZeros > 0) {
+        break; // zeros should always be contiguous
+      }
+    }
+    return numZeros;
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/CompactImpl.java b/icu4j/main/classes/core/src/newapi/impl/CompactImpl.java
new file mode 100644 (file)
index 0000000..b68d6d6
--- /dev/null
@@ -0,0 +1,114 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.util.HashMap;
+import java.util.Map;
+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.text.CompactDecimalFormat.CompactStyle;
+import com.ibm.icu.text.CompactDecimalFormat.CompactType;
+import com.ibm.icu.text.PluralRules;
+import com.ibm.icu.util.ULocale;
+
+import newapi.impl.MurkyModifier.ImmutableMurkyModifier;
+import newapi.impl.RoundingImpl.RoundingImplDummy;
+
+public class CompactImpl implements QuantityChain {
+
+  final PluralRules rules;
+  final CompactData data;
+  /* final */ Map<String, CompactModInfo> precomputedMods;
+  /* final */ QuantityChain parent;
+
+  public static CompactImpl getInstance(
+      ULocale dataLocale, CompactType compactType, CompactStyle compactStyle, PluralRules rules) {
+    CompactData data = CompactData.getInstance(dataLocale, compactType, compactStyle);
+    return new CompactImpl(data, rules);
+  }
+
+  public static CompactImpl getInstance(
+      Map<String, Map<String, String>> compactCustomData, PluralRules rules) {
+    CompactData data = CompactData.getInstance(compactCustomData);
+    return new CompactImpl(data, rules);
+  }
+
+  private CompactImpl(CompactData data, PluralRules rules) {
+    this.data = data;
+    this.rules = rules;
+  }
+
+  /** To be used by the building code path */
+  public void precomputeAllModifiers(MurkyModifier reference) {
+    precomputedMods = new HashMap<String, CompactModInfo>();
+    Set<String> allPatterns = data.getAllPatterns();
+    for (String patternString : allPatterns) {
+      CompactModInfo info = new CompactModInfo();
+      PatternParseResult patternInfo = LdmlPatternInfo.parse(patternString);
+      reference.setPatternInfo(patternInfo);
+      info.mod = reference.createImmutable();
+      info.numDigits = patternInfo.positive.totalIntegerDigits;
+      precomputedMods.put(patternString, info);
+    }
+  }
+
+  private static class CompactModInfo {
+    public ImmutableMurkyModifier mod;
+    public int numDigits;
+  }
+
+  @Override
+  public QuantityChain chain(QuantityChain parent) {
+    this.parent = parent;
+    return this;
+  }
+
+  @Override
+  public MicroProps withQuantity(FormatQuantity input) {
+    MicroProps micros = parent.withQuantity(input);
+    assert micros.rounding != null;
+
+    // Treat zero as if it had magnitude 0
+    int magnitude;
+    if (input.isZero()) {
+      magnitude = 0;
+      micros.rounding.apply(input);
+    } else {
+      // TODO: Revisit chooseMultiplierAndApply
+      int multiplier = micros.rounding.chooseMultiplierAndApply(input, data);
+      magnitude = input.isZero() ? 0 : input.getMagnitude();
+      magnitude -= multiplier;
+    }
+
+    StandardPlural plural = input.getStandardPlural(rules);
+    String patternString = data.getPattern(magnitude, plural);
+    int numDigits = -1;
+    if (patternString == null) {
+      // Use the default (non-compact) modifier.
+      // No need to take any action.
+    } else if (precomputedMods != null) {
+      // Build code path.
+      CompactModInfo info = precomputedMods.get(patternString);
+      info.mod.applyToMicros(micros, input);
+      numDigits = info.numDigits;
+    } else {
+      // Non-build 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;
+    }
+
+    // FIXME: Deal with numDigits == 0  (Awaiting a test case)
+
+    // We already performed rounding.  Do not perform it again.
+    micros.rounding = RoundingImplDummy.INSTANCE;
+
+    return micros;
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/CurrencySpacingEnabledModifier.java b/icu4j/main/classes/core/src/newapi/impl/CurrencySpacingEnabledModifier.java
new file mode 100644 (file)
index 0000000..eed7ed1
--- /dev/null
@@ -0,0 +1,173 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.modifiers.ConstantMultiFieldModifier;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.NumberFormat;
+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();
+
+  // 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;
+
+  /** Build 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;
+    }
+    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;
+  }
+
+  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;
+    }
+    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);
+    }
+  }
+
+  private static String getInsertString(DecimalFormatSymbols symbols, byte affix) {
+    return symbols.getPatternForCurrencySpacing(
+        DecimalFormatSymbols.CURRENCY_SPC_INSERT, affix == SUFFIX);
+  }
+
+  @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);
+    }
+    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);
+    }
+
+    // Call super for the remaining logic
+    length += super.apply(output, leftIndex, rightIndex + length);
+    return length;
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/CustomSymbolCurrency.java b/icu4j/main/classes/core/src/newapi/impl/CustomSymbolCurrency.java
new file mode 100644 (file)
index 0000000..664694b
--- /dev/null
@@ -0,0 +1,61 @@
+// © 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.text.DecimalFormatSymbols;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.ULocale;
+
+public class CustomSymbolCurrency extends Currency {
+  private String symbol1;
+  private String symbol2;
+
+  public static Currency resolve(Currency currency, ULocale locale, DecimalFormatSymbols symbols) {
+    if (currency == null) {
+      currency = symbols.getCurrency();
+    }
+    String currency1Sym = symbols.getCurrencySymbol();
+    String currency2Sym = symbols.getInternationalCurrencySymbol();
+    if (currency == null) {
+      return new CustomSymbolCurrency("XXX", currency1Sym, currency2Sym);
+    }
+    if (!currency.equals(symbols.getCurrency())) {
+      return currency;
+    }
+    String currency1 = currency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null);
+    String currency2 = currency.getCurrencyCode();
+    if (!currency1.equals(currency1Sym) || !currency2.equals(currency2Sym)) {
+      return new CustomSymbolCurrency(currency2, currency1Sym, currency2Sym);
+    }
+    return currency;
+  }
+
+  public CustomSymbolCurrency(String isoCode, String currency1Sym, String currency2Sym) {
+    super(isoCode);
+    this.symbol1 = currency1Sym;
+    this.symbol2 = currency2Sym;
+  }
+
+  @Override
+  public String getName(ULocale locale, int nameStyle, boolean[] isChoiceFormat) {
+    if (nameStyle == SYMBOL_NAME) {
+      return symbol1;
+    }
+    return super.getName(locale, nameStyle, isChoiceFormat);
+  }
+
+  @Override
+  public String getName(
+      ULocale locale, int nameStyle, String pluralCount, boolean[] isChoiceFormat) {
+    if (nameStyle == PLURAL_LONG_NAME && subType.equals("XXX")) {
+      // Plural in absence of a currency should return the symbol
+      return symbol1;
+    }
+    return super.getName(locale, nameStyle, pluralCount, isChoiceFormat);
+  }
+
+  @Override
+  public String getCurrencyCode() {
+    return symbol2;
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/DataUtils.java b/icu4j/main/classes/core/src/newapi/impl/DataUtils.java
new file mode 100644 (file)
index 0000000..f505d01
--- /dev/null
@@ -0,0 +1,67 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.util.EnumMap;
+import java.util.Map;
+
+import com.ibm.icu.impl.CurrencyData;
+import com.ibm.icu.impl.SimpleFormatterImpl;
+import com.ibm.icu.impl.StandardPlural;
+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.util.Currency;
+import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.ULocale;
+
+public class DataUtils {
+
+  public static Map<StandardPlural, Modifier> 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();
+    for (Map.Entry<String, String> e : data.entrySet()) {
+      String pluralKeyword = e.getKey();
+      StandardPlural plural = StandardPlural.fromString(e.getKey());
+      String longName = currency.getName(loc, Currency.PLURAL_LONG_NAME, pluralKeyword, null);
+      String simpleFormat = e.getValue(); // e.g., "{0} {1}"
+      simpleFormat = simpleFormat.replace("{1}", longName);
+      String compiled = SimpleFormatterImpl.compileToStringMinMaxArguments(simpleFormat, sb, 1, 1);
+      Modifier mod = new SimpleModifier(compiled, Field.CURRENCY, false);
+      result.put(plural, mod);
+    }
+    return result;
+  }
+
+  public static Map<StandardPlural, Modifier> getMeasureUnitModifiers(
+      ULocale loc, MeasureUnit unit, FormatWidth width) {
+    Map<StandardPlural, String> simpleFormats = MeasureData.getMeasureData(loc, unit, width);
+    Map<StandardPlural, Modifier> result =
+        new EnumMap<StandardPlural, Modifier>(StandardPlural.class);
+    StringBuilder sb = new StringBuilder();
+    for (StandardPlural plural : StandardPlural.VALUES) {
+      if (simpleFormats.get(plural) == null) {
+        plural = StandardPlural.OTHER;
+      }
+      String simpleFormat = simpleFormats.get(plural);
+      String compiled = SimpleFormatterImpl.compileToStringMinMaxArguments(simpleFormat, sb, 1, 1);
+      Modifier mod = new SimpleModifier(compiled, Field.CURRENCY, false);
+      result.put(plural, mod);
+    }
+    return result;
+    //    Map<StandardPlural, Modifier> result =
+    //        new EnumMap<StandardPlural, Modifier>(StandardPlural.class);
+    //    // TODO: Get the data directly instead of taking the detour through MeasureFormat.
+    //    MeasureFormat mf = MeasureFormat.getInstance(loc, width);
+    //    for (StandardPlural plural : StandardPlural.VALUES) {
+    //      String compiled = mf.getPluralFormatter(unit, width, plural.ordinal());
+    //      Modifier mod = new SimpleModifier(compiled, null, false);
+    //      result.put(plural, mod);
+    //    }
+    //    return result;
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/GroupingImpl.java b/icu4j/main/classes/core/src/newapi/impl/GroupingImpl.java
new file mode 100644 (file)
index 0000000..9e9d207
--- /dev/null
@@ -0,0 +1,134 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.math.BigDecimal;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult;
+
+import newapi.NumberFormatter.Grouping;
+import newapi.NumberFormatter.IGrouping;
+
+public class GroupingImpl extends Grouping.Internal {
+
+  // Conveniences for Java handling of shorts
+  private static final short S2 = 2;
+  private static final short S3 = 3;
+
+  // For the "placeholder constructor"
+  public static final char TYPE_PLACEHOLDER = 0;
+  public static final char TYPE_MIN2 = 1;
+  public static final char TYPE_NONE = 2;
+
+  // Statically initialized objects (cannot be used statically by other ICU classes)
+  static final GroupingImpl NONE = new GroupingImpl(TYPE_NONE);
+  static final GroupingImpl GROUPING_3 = new GroupingImpl(S3, S3, false);
+  static final GroupingImpl GROUPING_3_2 = new GroupingImpl(S3, S2, false);
+  static final GroupingImpl GROUPING_3_MIN2 = new GroupingImpl(S3, S3, true);
+  static final GroupingImpl GROUPING_3_2_MIN2 = new GroupingImpl(S3, S2, true);
+
+  static GroupingImpl getInstance(short grouping1, short grouping2, boolean min2) {
+    if (grouping1 == -1) {
+      return NONE;
+    } else if (!min2 && grouping1 == 3 && grouping2 == 3) {
+      return GROUPING_3;
+    } else if (!min2 && grouping1 == 3 && grouping2 == 2) {
+      return GROUPING_3_2;
+    } else if (min2 && grouping1 == 3 && grouping2 == 3) {
+      return GROUPING_3_MIN2;
+    } else if (min2 && grouping1 == 3 && grouping2 == 2) {
+      return GROUPING_3_2_MIN2;
+    } else {
+      return new GroupingImpl(grouping1, grouping2, min2);
+    }
+  }
+
+  public static GroupingImpl normalizeType(IGrouping grouping, PatternParseResult patternInfo) {
+    assert grouping != null;
+    if (grouping instanceof GroupingImpl) {
+      return ((GroupingImpl) grouping).withLocaleData(patternInfo);
+    } else {
+      return new GroupingImpl(grouping);
+    }
+  }
+
+  final IGrouping lambda;
+  final short grouping1; // -2 means "needs locale data"; -1 means "no grouping"
+  final short grouping2;
+  final boolean min2;
+
+  /** The "placeholder constructor". Pass in one of the GroupingImpl.TYPE_* variables. */
+  public GroupingImpl(char type) {
+    lambda = null;
+    switch (type) {
+      case TYPE_PLACEHOLDER:
+        grouping1 = -2;
+        grouping2 = -2;
+        min2 = false;
+        break;
+      case TYPE_MIN2:
+        grouping1 = -2;
+        grouping2 = -2;
+        min2 = true;
+        break;
+      case TYPE_NONE:
+        grouping1 = -1;
+        grouping2 = -1;
+        min2 = false;
+        break;
+      default:
+        throw new AssertionError();
+    }
+  }
+
+  private GroupingImpl(short grouping1, short grouping2, boolean min2) {
+    this.lambda = null;
+    this.grouping1 = grouping1;
+    this.grouping2 = grouping2;
+    this.min2 = min2;
+  }
+
+  private GroupingImpl(IGrouping lambda) {
+    this.lambda = lambda;
+    this.grouping1 = -3;
+    this.grouping2 = -3;
+    this.min2 = false;
+  }
+
+  GroupingImpl withLocaleData(PatternParseResult patternInfo) {
+    if (grouping1 != -2) {
+      return this;
+    }
+    assert lambda == null;
+    short grouping1 = (short) (patternInfo.positive.groupingSizes & 0xffff);
+    short grouping2 = (short) ((patternInfo.positive.groupingSizes >>> 16) & 0xffff);
+    short grouping3 = (short) ((patternInfo.positive.groupingSizes >>> 32) & 0xffff);
+    if (grouping2 == -1) {
+      grouping1 = -1;
+    }
+    if (grouping3 == -1) {
+      grouping2 = grouping1;
+    }
+    return getInstance(grouping1, grouping2, min2);
+  }
+
+  boolean groupAtPosition(int position, FormatQuantity value) {
+    // Check for lambda function
+    if (lambda != null) {
+      // TODO: Cache the BigDecimal
+      BigDecimal temp = value.toBigDecimal();
+      return lambda.groupAtPosition(position, temp);
+    }
+
+    assert grouping1 != -2;
+    if (grouping1 == -1 || grouping1 == 0) {
+      // Either -1 or 0 means "no grouping"
+      return false;
+    }
+    position -= grouping1;
+    return position >= 0
+        && (position % grouping2) == 0
+        && value.getUpperDisplayMagnitude() - grouping1 + 1 >= (min2 ? 2 : 1);
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/IntegerWidthImpl.java b/icu4j/main/classes/core/src/newapi/impl/IntegerWidthImpl.java
new file mode 100644 (file)
index 0000000..795652f
--- /dev/null
@@ -0,0 +1,27 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import newapi.NumberFormatter.IntegerWidth;
+
+public final class IntegerWidthImpl extends IntegerWidth.Internal {
+  public final int minInt;
+  public final int maxInt;
+
+  public static final IntegerWidthImpl DEFAULT = new IntegerWidthImpl();
+
+  /** Default constructor */
+  public IntegerWidthImpl() {
+    this(1, Integer.MAX_VALUE);
+  }
+
+  public IntegerWidthImpl(int minInt, int maxInt) {
+    this.minInt = minInt;
+    this.maxInt = maxInt;
+  }
+
+  @Override
+public IntegerWidthImpl truncateAt(int maxInt) {
+    return new IntegerWidthImpl(minInt, maxInt);
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/MacroProps.java b/icu4j/main/classes/core/src/newapi/impl/MacroProps.java
new file mode 100644 (file)
index 0000000..e9f8c3d
--- /dev/null
@@ -0,0 +1,105 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package 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;
+
+import newapi.NumberFormatter.DecimalMarkDisplay;
+import newapi.NumberFormatter.IGrouping;
+import newapi.NumberFormatter.IRounding;
+import newapi.NumberFormatter.IntegerWidth;
+import newapi.NumberFormatter.Notation;
+import newapi.NumberFormatter.Padding;
+import newapi.NumberFormatter.SignDisplay;
+
+public class MacroProps implements Cloneable {
+  public Notation notation;
+  public MeasureUnit unit;
+  public IRounding rounding;
+  public IGrouping grouping;
+  public Padding padding;
+  public IntegerWidth integerWidth;
+  public Object symbols;
+  public FormatWidth unitWidth;
+  public SignDisplay sign;
+  public DecimalMarkDisplay decimal;
+  public AffixPatternProvider affixProvider; // not in API; for JDK compatibility mode only
+  public MultiplierImpl multiplier; // not in API; for JDK compatibility mode only
+  public PluralRules rules; // not in API; could be made public in the future
+  public ULocale loc;
+
+  /**
+   * Copies values from fallback into this instance if they are null in this instance.
+   *
+   * @param fallback The instance to copy from; not modified by this operation.
+   */
+  public void fallback(MacroProps fallback) {
+    if (notation == null) notation = fallback.notation;
+    if (unit == null) unit = fallback.unit;
+    if (rounding == null) rounding = fallback.rounding;
+    if (grouping == null) grouping = fallback.grouping;
+    if (padding == null) padding = fallback.padding;
+    if (integerWidth == null) integerWidth = fallback.integerWidth;
+    if (symbols == null) symbols = fallback.symbols;
+    if (unitWidth == null) unitWidth = fallback.unitWidth;
+    if (sign == null) sign = fallback.sign;
+    if (decimal == null) decimal = fallback.decimal;
+    if (affixProvider == null) affixProvider = fallback.affixProvider;
+    if (multiplier == null) multiplier = fallback.multiplier;
+    if (rules == null) rules = fallback.rules;
+    if (loc == null) loc = fallback.loc;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(
+        notation,
+        unit,
+        rounding,
+        grouping,
+        padding,
+        integerWidth,
+        symbols,
+        unitWidth,
+        sign,
+        decimal,
+        affixProvider,
+        multiplier,
+        rules,
+        loc);
+  }
+
+  @Override
+  public boolean equals(Object _other) {
+    MacroProps other = (MacroProps) _other;
+    return Objects.equals(notation, other.notation)
+        && Objects.equals(unit, other.unit)
+        && Objects.equals(rounding, other.rounding)
+        && Objects.equals(grouping, other.grouping)
+        && Objects.equals(padding, other.padding)
+        && Objects.equals(integerWidth, other.integerWidth)
+        && Objects.equals(symbols, other.symbols)
+        && Objects.equals(unitWidth, other.unitWidth)
+        && Objects.equals(sign, other.sign)
+        && Objects.equals(decimal, other.decimal)
+        && Objects.equals(affixProvider, other.affixProvider)
+        && Objects.equals(multiplier, other.multiplier)
+        && Objects.equals(rules, other.rules)
+        && Objects.equals(loc, other.loc);
+  }
+
+  @Override
+  public Object clone() {
+    // TODO: Remove this method?
+    try {
+      return super.clone();
+    } catch (CloneNotSupportedException e) {
+      throw new AssertionError();
+    }
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/MeasureData.java b/icu4j/main/classes/core/src/newapi/impl/MeasureData.java
new file mode 100644 (file)
index 0000000..d48c129
--- /dev/null
@@ -0,0 +1,64 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.util.EnumMap;
+import java.util.Map;
+
+import com.ibm.icu.impl.ICUData;
+import com.ibm.icu.impl.ICUResourceBundle;
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.UResource;
+import com.ibm.icu.text.MeasureFormat.FormatWidth;
+import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.ULocale;
+import com.ibm.icu.util.UResourceBundle;
+
+public class MeasureData {
+
+  private static final class ShanesMeasureUnitSink extends UResource.Sink {
+
+    Map<StandardPlural, String> output;
+
+    public ShanesMeasureUnitSink(Map<StandardPlural, String> output) {
+      this.output = output;
+    }
+
+    @Override
+    public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
+      UResource.Table pluralsTable = value.getTable();
+      for (int i1 = 0; pluralsTable.getKeyAndValue(i1, key, value); ++i1) {
+        if (key.contentEquals("dnam") || key.contentEquals("per")) {
+          continue;
+        }
+        StandardPlural plural = StandardPlural.fromString(key);
+        if (output.containsKey(plural)) {
+          continue;
+        }
+        String formatString = value.getString();
+        output.put(plural, formatString);
+      }
+    }
+  }
+
+  public static Map<StandardPlural, String> getMeasureData(
+      ULocale locale, MeasureUnit unit, FormatWidth width) {
+    ICUResourceBundle resource =
+        (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_UNIT_BASE_NAME, locale);
+    StringBuilder key = new StringBuilder();
+    key.append("units");
+    if (width == FormatWidth.NARROW) {
+      key.append("Narrow");
+    } else if (width == FormatWidth.SHORT) {
+      key.append("Short");
+    }
+    key.append("/");
+    key.append(unit.getType());
+    key.append("/");
+    key.append(unit.getSubtype());
+    Map<StandardPlural, String> output = new EnumMap<StandardPlural, String>(StandardPlural.class);
+    ShanesMeasureUnitSink sink = new ShanesMeasureUnitSink(output);
+    resource.getAllItemsWithFallback(key.toString(), sink);
+    return output;
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/MicroProps.java b/icu4j/main/classes/core/src/newapi/impl/MicroProps.java
new file mode 100644 (file)
index 0000000..50c218f
--- /dev/null
@@ -0,0 +1,58 @@
+// © 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;
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.text.DecimalFormatSymbols;
+
+import newapi.NumberFormatter.DecimalMarkDisplay;
+import newapi.NumberFormatter.SignDisplay;
+
+public class MicroProps implements Cloneable, QuantityChain {
+  // Populated globally:
+  public SignDisplay sign;
+  public DecimalFormatSymbols symbols;
+  public PaddingImpl padding;
+  public DecimalMarkDisplay decimal;
+  public IntegerWidthImpl integerWidth;
+
+  // Populated by notation/unit:
+  public Modifier modOuter;
+  public Modifier modMiddle;
+  public Modifier modInner;
+  public RoundingImpl rounding;
+  public GroupingImpl grouping;
+  public int multiplier;
+  public boolean useCurrency;
+
+  private boolean frozen = false;
+
+  public void enableCloneInChain() {
+    frozen = true;
+  }
+
+  @Override
+  public QuantityChain chain(QuantityChain parent) {
+    // The MicroProps instance should always be at the top of the chain!
+    throw new AssertionError();
+  }
+
+  @Override
+  public MicroProps withQuantity(FormatQuantity quantity) {
+    if (frozen) {
+      return (MicroProps) this.clone();
+    } else {
+      return this;
+    }
+  }
+
+  @Override
+  public Object clone() {
+    try {
+      return super.clone();
+    } catch (CloneNotSupportedException e) {
+      throw new AssertionError();
+    }
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/MultiplierImpl.java b/icu4j/main/classes/core/src/newapi/impl/MultiplierImpl.java
new file mode 100644 (file)
index 0000000..11448f4
--- /dev/null
@@ -0,0 +1,39 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.math.BigDecimal;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+
+public class MultiplierImpl implements QuantityChain {
+  final int magnitudeMultiplier;
+  final BigDecimal bigDecimalMultiplier;
+  /* final */ QuantityChain parent;
+
+  public MultiplierImpl(int magnitudeMultiplier) {
+    this.magnitudeMultiplier = magnitudeMultiplier;
+    this.bigDecimalMultiplier = null;
+  }
+
+  public MultiplierImpl(BigDecimal bigDecimalMultiplier) {
+    this.magnitudeMultiplier = 0;
+    this.bigDecimalMultiplier = bigDecimalMultiplier;
+  }
+
+  @Override
+  public QuantityChain chain(QuantityChain parent) {
+    this.parent = parent;
+    return this;
+  }
+
+  @Override
+  public MicroProps withQuantity(FormatQuantity quantity) {
+    MicroProps micros = parent.withQuantity(quantity);
+    quantity.adjustMagnitude(magnitudeMultiplier);
+    if (bigDecimalMultiplier != null) {
+      quantity.multiplyBy(bigDecimalMultiplier);
+    }
+    return micros;
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/MurkyModifier.java b/icu4j/main/classes/core/src/newapi/impl/MurkyModifier.java
new file mode 100644 (file)
index 0000000..125bf19
--- /dev/null
@@ -0,0 +1,414 @@
+// © 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.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.Modifier;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.modifiers.ConstantMultiFieldModifier;
+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;
+
+/**
+ * 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!!!
+ *
+ * <p>This class takes a parsed pattern and returns a Modifier, without creating any objects. When
+ * the Modifier methods are called, symbols are substituted directly into the output
+ * NumberStringBuilder, without creating any intermediate Strings.
+ */
+public class MurkyModifier implements Modifier, SymbolProvider, CharSequence, QuantityChain {
+
+  // Modifier details
+  final boolean isStrong;
+
+  // Pattern details
+  AffixPatternProvider patternInfo;
+  SignDisplay signDisplay;
+  boolean perMilleReplacesPercent;
+
+  // Symbol details
+  DecimalFormatSymbols symbols;
+  FormatWidth unitWidth;
+  String currency1;
+  String currency2;
+  String[] currency3;
+  PluralRules rules;
+
+  // Number details
+  boolean isNegative;
+  StandardPlural plural;
+
+  // QuantityChain details
+  QuantityChain parent;
+
+  // Transient CharSequence fields
+  boolean inCharSequenceMode;
+  int flags;
+  int length;
+  boolean prependSign;
+  boolean plusReplacesMinusSign;
+
+  public MurkyModifier(boolean isStrong) {
+    this.isStrong = isStrong;
+  }
+
+  public void setPatternInfo(AffixPatternProvider patternInfo) {
+    this.patternInfo = patternInfo;
+  }
+
+  public void setPatternAttributes(SignDisplay signDisplay, boolean perMille) {
+    this.signDisplay = signDisplay;
+    this.perMilleReplacesPercent = perMille;
+  }
+
+  public void setSymbols(
+      DecimalFormatSymbols symbols, Currency currency, FormatWidth unitWidth, PluralRules rules) {
+    assert (rules != null) == needsPlurals();
+    this.symbols = symbols;
+    this.unitWidth = unitWidth;
+    this.rules = rules;
+
+    currency1 = currency.getName(symbols.getULocale(), Currency.SYMBOL_NAME, null);
+    currency2 = currency.getCurrencyCode();
+
+    if (rules != null) {
+      currency3 = new String[StandardPlural.COUNT];
+      for (StandardPlural plural : StandardPlural.VALUES) {
+        currency3[plural.ordinal()] =
+            currency.getName(
+                symbols.getULocale(), Currency.PLURAL_LONG_NAME, plural.getKeyword(), null);
+      }
+    }
+  }
+
+  public void setNumberProperties(boolean isNegative, StandardPlural plural) {
+    assert (plural != null) == needsPlurals();
+    this.isNegative = isNegative;
+    this.plural = plural;
+  }
+
+  /**
+   * Returns true if the pattern represented by this MurkyModifier requires a plural keyword in
+   * order to localize. This is currently true only if there is a currency long name placeholder in
+   * the pattern.
+   */
+  public boolean needsPlurals() {
+    return patternInfo.containsSymbolType(AffixPatternUtils.TYPE_CURRENCY_TRIPLE);
+  }
+
+  @Override
+  public QuantityChain chain(QuantityChain parent) {
+    this.parent = parent;
+    return this;
+  }
+
+  @Override
+  public MicroProps withQuantity(FormatQuantity fq) {
+    MicroProps micros = parent.withQuantity(fq);
+    if (needsPlurals()) {
+      // TODO: Fix this. Avoid the copy.
+      FormatQuantity copy = fq.createCopy();
+      micros.rounding.apply(copy);
+      setNumberProperties(fq.isNegative(), copy.getStandardPlural(rules));
+    } else {
+      setNumberProperties(fq.isNegative(), null);
+    }
+    micros.modMiddle = this;
+    return micros;
+  }
+
+  /**
+   * Creates a new quantity-dependent Modifier that behaves the same as the current instance, but
+   * which is immutable and can be saved for future use. The current instance is not changed by
+   * calling this method except for the number properties.
+   *
+   * @return An immutable that supports both positive and negative numbers.
+   */
+  public ImmutableMurkyModifier createImmutable() {
+    NumberStringBuilder a = new NumberStringBuilder();
+    NumberStringBuilder b = new NumberStringBuilder();
+    if (needsPlurals()) {
+      // Slower path when we require the plural keyword.
+      Modifier[] mods = new Modifier[ImmutableMurkyModifierWithPlurals.getModsLength()];
+      for (StandardPlural plural : StandardPlural.VALUES) {
+        setNumberProperties(false, plural);
+        Modifier positive = createConstantModifier(a, b);
+        setNumberProperties(true, plural);
+        Modifier negative = createConstantModifier(a, b);
+        mods[ImmutableMurkyModifierWithPlurals.getModIndex(false, plural)] = positive;
+        mods[ImmutableMurkyModifierWithPlurals.getModIndex(true, plural)] = negative;
+      }
+      return new ImmutableMurkyModifierWithPlurals(mods, rules);
+    } else {
+      // Faster path when plural keyword is not needed.
+      setNumberProperties(false, null);
+      Modifier positive = createConstantModifier(a, b);
+      setNumberProperties(true, null);
+      Modifier negative = createConstantModifier(a, b);
+      return new ImmutableMurkyModifierWithoutPlurals(positive, negative);
+    }
+  }
+
+  private Modifier createConstantModifier(NumberStringBuilder a, NumberStringBuilder b) {
+    insertPrefix(a.clear(), 0);
+    insertSuffix(b.clear(), 0);
+    if (patternInfo.hasCurrencySign()) {
+      return new CurrencySpacingEnabledModifier(a, b, isStrong, symbols);
+    } else {
+      return new ConstantMultiFieldModifier(a, b, isStrong);
+    }
+  }
+
+  public static interface ImmutableMurkyModifier extends QuantityChain {
+    public void applyToMicros(MicroProps micros, FormatQuantity quantity);
+  }
+
+  public static class ImmutableMurkyModifierWithoutPlurals implements ImmutableMurkyModifier {
+    final Modifier positive;
+    final Modifier negative;
+    /* final */ QuantityChain parent;
+
+    public ImmutableMurkyModifierWithoutPlurals(Modifier positive, Modifier negative) {
+      this.positive = positive;
+      this.negative = negative;
+    }
+
+    @Override
+    public QuantityChain chain(QuantityChain parent) {
+      this.parent = parent;
+      return this;
+    }
+
+    @Override
+    public MicroProps withQuantity(FormatQuantity quantity) {
+      MicroProps micros = parent.withQuantity(quantity);
+      applyToMicros(micros, quantity);
+      return micros;
+    }
+
+    @Override
+    public void applyToMicros(MicroProps micros, FormatQuantity quantity) {
+      if (quantity.isNegative()) {
+        micros.modMiddle = negative;
+      } else {
+        micros.modMiddle = positive;
+      }
+    }
+  }
+
+  public static class ImmutableMurkyModifierWithPlurals implements ImmutableMurkyModifier {
+    final Modifier[] mods;
+    final PluralRules rules;
+    /* final */ QuantityChain parent;
+
+    public ImmutableMurkyModifierWithPlurals(Modifier[] mods, PluralRules rules) {
+      assert mods.length == getModsLength();
+      assert rules != null;
+      this.mods = mods;
+      this.rules = rules;
+    }
+
+    public static int getModsLength() {
+      return 2 * StandardPlural.COUNT;
+    }
+
+    public static int getModIndex(boolean isNegative, StandardPlural plural) {
+      return plural.ordinal() * 2 + (isNegative ? 1 : 0);
+    }
+
+    @Override
+    public QuantityChain chain(QuantityChain parent) {
+      this.parent = parent;
+      return this;
+    }
+
+    @Override
+    public MicroProps withQuantity(FormatQuantity quantity) {
+      MicroProps micros = parent.withQuantity(quantity);
+      applyToMicros(micros, quantity);
+      return micros;
+    }
+
+    @Override
+    public void applyToMicros(MicroProps micros, FormatQuantity quantity) {
+      // TODO: Fix this. Avoid the copy.
+      FormatQuantity copy = quantity.createCopy();
+      copy.roundToInfinity();
+      StandardPlural plural = copy.getStandardPlural(rules);
+      Modifier mod = mods[getModIndex(quantity.isNegative(), plural)];
+      micros.modMiddle = mod;
+    }
+  }
+
+  @Override
+  public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
+    int prefixLen = insertPrefix(output, leftIndex);
+    int suffixLen = insertSuffix(output, rightIndex + prefixLen);
+    CurrencySpacingEnabledModifier.applyCurrencySpacing(
+        output, leftIndex, prefixLen, rightIndex + prefixLen, suffixLen, symbols);
+    return prefixLen + suffixLen;
+  }
+
+  @Override
+  public boolean isStrong() {
+    return isStrong;
+  }
+
+  @Override
+  public String getPrefix() {
+    NumberStringBuilder sb = new NumberStringBuilder(10);
+    insertPrefix(sb, 0);
+    return sb.toString();
+  }
+
+  @Override
+  public String getSuffix() {
+    NumberStringBuilder sb = new NumberStringBuilder(10);
+    insertSuffix(sb, 0);
+    return sb.toString();
+  }
+
+  private int insertPrefix(NumberStringBuilder sb, int position) {
+    enterCharSequenceMode(true);
+    int length = AffixPatternUtils.unescape(this, sb, position, this);
+    exitCharSequenceMode();
+    return length;
+  }
+
+  private int insertSuffix(NumberStringBuilder sb, int position) {
+    enterCharSequenceMode(false);
+    int length = AffixPatternUtils.unescape(this, sb, position, this);
+    exitCharSequenceMode();
+    return length;
+  }
+
+  @Override
+  public CharSequence getSymbol(int type) {
+    switch (type) {
+      case AffixPatternUtils.TYPE_MINUS_SIGN:
+        return symbols.getMinusSignString();
+      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:
+        // FormatWidth ISO overrides the singular currency symbol
+        if (unitWidth == FormatWidth.SHORT) {
+          return currency2;
+        } else {
+          return currency1;
+        }
+      case AffixPatternUtils.TYPE_CURRENCY_DOUBLE:
+        return currency2;
+      case AffixPatternUtils.TYPE_CURRENCY_TRIPLE:
+        // NOTE: This is the code path only for patterns containing "".
+        // Most plural currencies are formatted in DataUtils.
+        assert plural != null;
+        if (currency3 == null) {
+          return currency2;
+        } else {
+          return currency3[plural.ordinal()];
+        }
+      case AffixPatternUtils.TYPE_CURRENCY_QUAD:
+        return "\uFFFD";
+      case AffixPatternUtils.TYPE_CURRENCY_QUINT:
+        return "\uFFFD";
+      default:
+        throw new AssertionError();
+    }
+  }
+
+  /** This method contains the heart of the logic for rendering LDML affix strings. */
+  private void enterCharSequenceMode(boolean isPrefix) {
+    assert !inCharSequenceMode;
+    inCharSequenceMode = true;
+
+    // Should the output render '+' where '-' would normally appear in the pattern?
+    plusReplacesMinusSign =
+        !isNegative
+            && signDisplay == SignDisplay.ALWAYS_SHOWN
+            && patternInfo.positiveHasPlusSign() == false;
+
+    // Should we use the negative affix pattern? (If not, we will use the positive one)
+    boolean useNegativeAffixPattern =
+        patternInfo.hasNegativeSubpattern()
+            && (isNegative || (patternInfo.negativeHasMinusSign() && plusReplacesMinusSign));
+
+    // Resolve the flags for the affix pattern.
+    flags = 0;
+    if (useNegativeAffixPattern) {
+      flags |= AffixPatternProvider.Flags.NEGATIVE_SUBPATTERN;
+    }
+    if (isPrefix) {
+      flags |= AffixPatternProvider.Flags.PREFIX;
+    }
+    if (plural != null) {
+      assert plural.ordinal() == (AffixPatternProvider.Flags.PLURAL_MASK & plural.ordinal());
+      flags |= plural.ordinal();
+    }
+
+    // Should we prepend a sign to the pattern?
+    if (!isPrefix || useNegativeAffixPattern) {
+      prependSign = false;
+    } else if (isNegative) {
+      prependSign = signDisplay != SignDisplay.NEVER_SHOWN;
+    } else {
+      prependSign = plusReplacesMinusSign;
+    }
+
+    // Finally, compute the length of the affix pattern.
+    length = patternInfo.length(flags) + (prependSign ? 1 : 0);
+  }
+
+  private void exitCharSequenceMode() {
+    assert inCharSequenceMode;
+    inCharSequenceMode = false;
+  }
+
+  @Override
+  public int length() {
+    if (inCharSequenceMode) {
+      return length;
+    } else {
+      NumberStringBuilder sb = new NumberStringBuilder(20);
+      apply(sb, 0, 0);
+      return sb.length();
+    }
+  }
+
+  @Override
+  public char charAt(int index) {
+    assert inCharSequenceMode;
+    char candidate;
+    if (prependSign && index == 0) {
+      candidate = '-';
+    } else if (prependSign) {
+      candidate = patternInfo.charAt(flags, index - 1);
+    } else {
+      candidate = patternInfo.charAt(flags, index);
+    }
+    if (plusReplacesMinusSign && candidate == '-') {
+      return '+';
+    }
+    if (perMilleReplacesPercent && candidate == '%') {
+      return '‰';
+    }
+    return candidate;
+  }
+
+  @Override
+  public CharSequence subSequence(int start, int end) {
+    // Should never be called in normal circumstances
+    throw new AssertionError();
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/NotationImpl.java b/icu4j/main/classes/core/src/newapi/impl/NotationImpl.java
new file mode 100644 (file)
index 0000000..dbe5bc7
--- /dev/null
@@ -0,0 +1,81 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.util.Map;
+
+import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
+
+import newapi.NumberFormatter.NotationCompact;
+import newapi.NumberFormatter.NotationScientific;
+import newapi.NumberFormatter.SignDisplay;
+
+@SuppressWarnings("deprecation")
+public class NotationImpl {
+
+  public static class NotationScientificImpl extends NotationScientific.Internal
+      implements Cloneable {
+
+    int engineeringInterval;
+    boolean requireMinInt;
+    int minExponentDigits;
+    SignDisplay exponentSignDisplay;
+
+    public NotationScientificImpl(int engineeringInterval) {
+      this.engineeringInterval = engineeringInterval;
+      requireMinInt = false;
+      minExponentDigits = 1;
+      exponentSignDisplay = SignDisplay.AUTO;
+    }
+
+    public NotationScientificImpl(
+        int engineeringInterval,
+        boolean requireMinInt,
+        int minExponentDigits,
+        SignDisplay exponentSignDisplay) {
+      this.engineeringInterval = engineeringInterval;
+      this.requireMinInt = requireMinInt;
+      this.minExponentDigits = minExponentDigits;
+      this.exponentSignDisplay = exponentSignDisplay;
+    }
+
+    @Override
+    public NotationScientific withMinExponentDigits(int minExponentDigits) {
+      NotationScientificImpl other = (NotationScientificImpl) this.clone();
+      other.minExponentDigits = minExponentDigits;
+      return other;
+    }
+
+    @Override
+    public NotationScientific withExponentSignDisplay(SignDisplay exponentSignDisplay) {
+      NotationScientificImpl other = (NotationScientificImpl) this.clone();
+      other.exponentSignDisplay = exponentSignDisplay;
+      return other;
+    }
+
+    @Override
+    public Object clone() {
+      try {
+        return super.clone();
+      } catch (CloneNotSupportedException e) {
+        // Should not happen since parent is Object
+        throw new AssertionError(e);
+      }
+    }
+  }
+
+  public static class NotationCompactImpl extends NotationCompact.Internal {
+    final CompactStyle compactStyle;
+    final Map<String, Map<String, String>> compactCustomData;
+
+    public NotationCompactImpl(CompactStyle compactStyle) {
+      compactCustomData = null;
+      this.compactStyle = compactStyle;
+    }
+
+    public NotationCompactImpl(Map<String, Map<String, String>> compactCustomData) {
+      compactStyle = null;
+      this.compactCustomData = compactCustomData;
+    }
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/NumberFormatterImpl.java b/icu4j/main/classes/core/src/newapi/impl/NumberFormatterImpl.java
new file mode 100644 (file)
index 0000000..b7727ee
--- /dev/null
@@ -0,0 +1,328 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.util.Locale;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.ibm.icu.impl.number.FormatQuantity4;
+import com.ibm.icu.impl.number.FormatQuantityBCD;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.PatternString;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.MeasureFormat.FormatWidth;
+import com.ibm.icu.text.NumberingSystem;
+import com.ibm.icu.util.Measure;
+import com.ibm.icu.util.MeasureUnit;
+import com.ibm.icu.util.ULocale;
+
+import newapi.NumberFormatter;
+import newapi.NumberFormatter.DecimalMarkDisplay;
+import newapi.NumberFormatter.IGrouping;
+import newapi.NumberFormatter.IRounding;
+import newapi.NumberFormatter.IntegerWidth;
+import newapi.NumberFormatter.Notation;
+import newapi.NumberFormatter.NumberFormatterResult;
+import newapi.NumberFormatter.Padding;
+import newapi.NumberFormatter.SignDisplay;
+import newapi.NumberFormatter.UnlocalizedNumberFormatter;
+
+/** @author sffc */
+public class NumberFormatterImpl extends NumberFormatter.LocalizedNumberFormatter.Internal {
+
+  private static final NumberFormatterImpl BASE = new NumberFormatterImpl();
+
+  // TODO: Set a good value here.
+  static final int DEFAULT_THRESHOLD = 3;
+
+  static final int KEY_MACROS = 0;
+  static final int KEY_LOCALE = 1;
+  static final int KEY_NOTATION = 2;
+  static final int KEY_UNIT = 3;
+  static final int KEY_ROUNDING = 4;
+  static final int KEY_GROUPING = 5;
+  static final int KEY_PADDING = 6;
+  static final int KEY_INTEGER = 7;
+  static final int KEY_SYMBOLS = 8;
+  static final int KEY_UNIT_WIDTH = 9;
+  static final int KEY_SIGN = 10;
+  static final int KEY_DECIMAL = 11;
+  static final int KEY_MAX = 12;
+
+  public static NumberFormatterImpl with() {
+    return BASE;
+  }
+
+  /** Internal method to set a starting macros. */
+  public static NumberFormatterImpl fromMacros(MacroProps macros) {
+    return new NumberFormatterImpl(BASE, KEY_MACROS, macros);
+  }
+
+  /**
+   * Internal method to construct a chain from a pattern using {@link NumberPropertyMapper}. Could
+   * be added to the public API if the feature is requested. In that case, a more efficient
+   * implementation may be desired.
+   */
+  public static UnlocalizedNumberFormatter fromPattern(
+      String string, DecimalFormatSymbols symbols) {
+    Properties props = PatternString.parseToProperties(string);
+    MacroProps macros = NumberPropertyMapper.oldToNew(props, symbols, null);
+    return fromMacros(macros);
+  }
+
+  // TODO: Reduce the number of fields.
+  final NumberFormatterImpl parent;
+  final int key;
+  final Object value;
+  volatile MacroProps resolvedMacros;
+  volatile AtomicInteger callCount;
+  volatile NumberFormatterImpl savedWithUnit;
+  volatile Worker1 compiled;
+
+  /** Base constructor; called during startup only */
+  private NumberFormatterImpl() {
+    parent = null;
+    key = -1;
+    value = null;
+  }
+
+  /** Primary constructor */
+  private NumberFormatterImpl(NumberFormatterImpl parent, int key, Object value) {
+    this.parent = parent;
+    this.key = key;
+    this.value = value;
+  }
+
+  @Override
+  public NumberFormatterImpl notation(Notation notation) {
+    return new NumberFormatterImpl(this, KEY_NOTATION, notation);
+  }
+
+  @Override
+  public NumberFormatterImpl unit(MeasureUnit unit) {
+    return new NumberFormatterImpl(this, KEY_UNIT, unit);
+  }
+
+  @Override
+  public NumberFormatterImpl rounding(IRounding rounding) {
+    return new NumberFormatterImpl(this, KEY_ROUNDING, rounding);
+  }
+
+  @Override
+  public NumberFormatterImpl grouping(IGrouping grouping) {
+    return new NumberFormatterImpl(this, KEY_GROUPING, grouping);
+  }
+
+  @Override
+  public NumberFormatterImpl padding(Padding padding) {
+    return new NumberFormatterImpl(this, KEY_PADDING, padding);
+  }
+
+  @Override
+  public NumberFormatterImpl integerWidth(IntegerWidth style) {
+    return new NumberFormatterImpl(this, KEY_INTEGER, style);
+  }
+
+  @Override
+  public NumberFormatterImpl symbols(DecimalFormatSymbols symbols) {
+    return new NumberFormatterImpl(this, KEY_SYMBOLS, symbols);
+  }
+
+  @Override
+  public NumberFormatterImpl symbols(NumberingSystem ns) {
+    return new NumberFormatterImpl(this, KEY_SYMBOLS, ns);
+  }
+
+  @Override
+  public NumberFormatterImpl unitWidth(FormatWidth style) {
+    return new NumberFormatterImpl(this, KEY_UNIT_WIDTH, style);
+  }
+
+  @Override
+  public NumberFormatterImpl sign(SignDisplay style) {
+    return new NumberFormatterImpl(this, KEY_SIGN, style);
+  }
+
+  @Override
+  public NumberFormatterImpl decimal(DecimalMarkDisplay style) {
+    return new NumberFormatterImpl(this, KEY_DECIMAL, style);
+  }
+
+  @Override
+  public NumberFormatterImpl locale(Locale locale) {
+    return new NumberFormatterImpl(this, KEY_LOCALE, ULocale.forLocale(locale));
+  }
+
+  @Override
+  public NumberFormatterImpl locale(ULocale locale) {
+    return new NumberFormatterImpl(this, KEY_LOCALE, locale);
+  }
+
+  @Override
+  public String toSkeleton() {
+    return SkeletonBuilder.macrosToSkeleton(resolve());
+  }
+
+  @Override
+  public NumberFormatterResult format(long input) {
+    return format(new FormatQuantity4(input), DEFAULT_THRESHOLD);
+  }
+
+  @Override
+  public NumberFormatterResult format(double input) {
+    return format(new FormatQuantity4(input), DEFAULT_THRESHOLD);
+  }
+
+  @Override
+  public NumberFormatterResult format(Number input) {
+    return format(new FormatQuantity4(input), DEFAULT_THRESHOLD);
+  }
+
+  @Override
+  public NumberFormatterResult format(Measure input) {
+    return formatWithThreshold(input, DEFAULT_THRESHOLD);
+  }
+
+  /**
+   * Internal version of format with support for a custom regulation threshold. A threshold of 1
+   * causes the data structures to be built right away. A threshold of 0 prevents the data
+   * structures from being built.
+   */
+  public NumberFormatterResult formatWithThreshold(Number number, int threshold) {
+    return format(new FormatQuantity4(number), threshold);
+  }
+
+  /**
+   * Internal version of format with support for a custom regulation threshold. A threshold of 1
+   * causes the data structures to be built right away. A threshold of 0 prevents the data
+   * structures from being built.
+   */
+  public NumberFormatterResult formatWithThreshold(Measure input, int threshold) {
+    MeasureUnit unit = input.getUnit();
+    Number number = input.getNumber();
+    // Use this formatter if possible
+    if (Objects.equals(resolve().unit, unit)) {
+      return formatWithThreshold(number, threshold);
+    }
+    // This mechanism saves the previously used unit, so if the user calls this method with the
+    // same unit multiple times in a row, they get a more efficient code path.
+    NumberFormatterImpl withUnit = savedWithUnit;
+    if (withUnit == null || !Objects.equals(withUnit.resolve().unit, unit)) {
+      withUnit = new NumberFormatterImpl(this, KEY_UNIT, unit);
+      savedWithUnit = withUnit;
+    }
+    return withUnit.formatWithThreshold(number, threshold);
+  }
+
+  private NumberFormatterResult format(FormatQuantityBCD fq, int threshold) {
+    NumberStringBuilder string = new NumberStringBuilder();
+    // Lazily create the AtomicInteger
+    if (callCount == null) {
+      callCount = new AtomicInteger();
+    }
+    int currentCount = callCount.incrementAndGet();
+    MicroProps micros;
+    if (currentCount == threshold) {
+      compiled = Worker1.fromMacros(resolve());
+      micros = compiled.apply(fq, string);
+    } else if (compiled != null) {
+      micros = compiled.apply(fq, string);
+    } else {
+      micros = Worker1.applyStatic(resolve(), fq, string);
+    }
+    return new NumberFormatterResult(string, fq, micros);
+  }
+
+  @Override
+  public int hashCode() {
+    return resolve().hashCode();
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (this == other) return true;
+    if (other == null) return false;
+    if (!(other instanceof NumberFormatterImpl)) return false;
+    return resolve().equals(((NumberFormatterImpl) other).resolve());
+  }
+
+  private MacroProps resolve() {
+    if (resolvedMacros != null) {
+      return resolvedMacros;
+    }
+    // Although the linked-list fluent storage approach requires this method,
+    // my benchmarks show that linked-list is still faster than a full clone
+    // of a MacroProps object at each step.
+    MacroProps macros = new MacroProps();
+    NumberFormatterImpl current = this;
+    while (current != BASE) {
+      switch (current.key) {
+        case KEY_MACROS:
+          macros.fallback((MacroProps) current.value);
+          break;
+        case KEY_LOCALE:
+          if (macros.loc == null) {
+            macros.loc = (ULocale) current.value;
+          }
+          break;
+        case KEY_NOTATION:
+          if (macros.notation == null) {
+            macros.notation = (Notation) current.value;
+          }
+          break;
+        case KEY_UNIT:
+          if (macros.unit == null) {
+            macros.unit = (MeasureUnit) current.value;
+          }
+          break;
+        case KEY_ROUNDING:
+          if (macros.rounding == null) {
+            macros.rounding = (IRounding) current.value;
+          }
+          break;
+        case KEY_GROUPING:
+          if (macros.grouping == null) {
+            macros.grouping = (IGrouping) current.value;
+          }
+          break;
+        case KEY_PADDING:
+          if (macros.padding == null) {
+            macros.padding = (Padding) current.value;
+          }
+          break;
+        case KEY_INTEGER:
+          if (macros.integerWidth == null) {
+            macros.integerWidth = (IntegerWidth) current.value;
+          }
+          break;
+        case KEY_SYMBOLS:
+          if (macros.symbols == null) {
+            macros.symbols = /*(Object)*/ current.value;
+          }
+          break;
+        case KEY_UNIT_WIDTH:
+          if (macros.unitWidth == null) {
+            macros.unitWidth = (FormatWidth) current.value;
+          }
+          break;
+        case KEY_SIGN:
+          if (macros.sign == null) {
+            macros.sign = (SignDisplay) current.value;
+          }
+          break;
+        case KEY_DECIMAL:
+          if (macros.decimal == null) {
+            macros.decimal = (DecimalMarkDisplay) current.value;
+          }
+          break;
+        default:
+          throw new AssertionError();
+      }
+      current = current.parent;
+    }
+    resolvedMacros = macros;
+    return macros;
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/NumberPropertyMapper.java b/icu4j/main/classes/core/src/newapi/impl/NumberPropertyMapper.java
new file mode 100644 (file)
index 0000000..ae28af0
--- /dev/null
@@ -0,0 +1,447 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.math.BigDecimal;
+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.Properties;
+import com.ibm.icu.impl.number.RoundingUtils;
+import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
+import com.ibm.icu.text.CurrencyPluralInfo;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyUsage;
+import com.ibm.icu.util.ULocale;
+
+import newapi.NumberFormatter.CurrencyRounding;
+import newapi.NumberFormatter.DecimalMarkDisplay;
+import newapi.NumberFormatter.IntegerWidth;
+import newapi.NumberFormatter.Notation;
+import newapi.NumberFormatter.Rounding;
+import newapi.NumberFormatter.SignDisplay;
+import newapi.impl.NotationImpl.NotationScientificImpl;
+import newapi.impl.RoundingImpl.RoundingImplCurrency;
+import newapi.impl.RoundingImpl.RoundingImplFraction;
+import newapi.impl.RoundingImpl.RoundingImplIncrement;
+import newapi.impl.RoundingImpl.RoundingImplSignificant;
+
+/** @author sffc */
+public final class NumberPropertyMapper {
+
+  /**
+   * Creates a new {@link MacroProps} object based on the content of a {@link Properties} object. In
+   * other words, maps Properties to MacroProps. This function is used by the JDK-compatibility API
+   * to call into the ICU 60 fluent number formatting pipeline.
+   *
+   * @param properties The property bag to be mapped.
+   * @param symbols The symbols associated with the property bag.
+   * @param exportedProperties A property bag in which to store validated properties.
+   * @return A new MacroProps containing all of the information in the Properties.
+   */
+  public static MacroProps oldToNew(
+      Properties properties, DecimalFormatSymbols symbols, Properties exportedProperties) {
+    MacroProps macros = new MacroProps();
+    ULocale locale = symbols.getULocale();
+
+    /////////////
+    // SYMBOLS //
+    /////////////
+
+    macros.symbols = symbols;
+
+    //////////////////
+    // PLURAL RULES //
+    //////////////////
+
+    macros.rules = properties.getPluralRules();
+
+    /////////////
+    // AFFIXES //
+    /////////////
+
+    AffixPatternProvider affixProvider;
+    if (properties.getCurrencyPluralInfo() == null) {
+      affixProvider =
+          new PropertiesAffixPatternProvider(
+              properties.getPositivePrefix() != null
+                  ? AffixPatternUtils.escape(properties.getPositivePrefix())
+                  : properties.getPositivePrefixPattern(),
+              properties.getPositiveSuffix() != null
+                  ? AffixPatternUtils.escape(properties.getPositiveSuffix())
+                  : properties.getPositiveSuffixPattern(),
+              properties.getNegativePrefix() != null
+                  ? AffixPatternUtils.escape(properties.getNegativePrefix())
+                  : properties.getNegativePrefixPattern(),
+              properties.getNegativeSuffix() != null
+                  ? AffixPatternUtils.escape(properties.getNegativeSuffix())
+                  : properties.getNegativeSuffixPattern());
+    } else {
+      affixProvider = new CurrencyPluralInfoAffixProvider(properties.getCurrencyPluralInfo());
+    }
+    macros.affixProvider = affixProvider;
+
+    ///////////
+    // UNITS //
+    ///////////
+
+    boolean useCurrency =
+        ((properties.getCurrency() != null)
+            || properties.getCurrencyPluralInfo() != null
+            || properties.getCurrencyUsage() != null
+            || affixProvider.hasCurrencySign());
+    Currency currency = CustomSymbolCurrency.resolve(properties.getCurrency(), locale, symbols);
+    CurrencyUsage currencyUsage = properties.getCurrencyUsage();
+    boolean explicitCurrencyUsage = currencyUsage != Properties.DEFAULT_CURRENCY_USAGE;
+    if (!explicitCurrencyUsage) {
+      currencyUsage = CurrencyUsage.STANDARD;
+    }
+    if (useCurrency) {
+      macros.unit = currency;
+    }
+
+    ///////////////////////
+    // ROUNDING STRATEGY //
+    ///////////////////////
+
+    int maxInt = properties.getMaximumIntegerDigits();
+    int minInt = properties.getMinimumIntegerDigits();
+    int maxFrac = properties.getMaximumFractionDigits();
+    int minFrac = properties.getMinimumFractionDigits();
+    int minSig = properties.getMinimumSignificantDigits();
+    int maxSig = properties.getMaximumSignificantDigits();
+    BigDecimal roundingIncrement = properties.getRoundingIncrement();
+    MathContext mathContext = RoundingUtils.getMathContextOrUnlimited(properties);
+    boolean explicitMinMaxFrac =
+        minFrac != Properties.DEFAULT_MINIMUM_FRACTION_DIGITS
+            || maxFrac != Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS;
+    boolean explicitMinMaxSig =
+        minSig != Properties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS
+            || maxSig != Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS;
+    // Validate min/max int/frac.
+    // For backwards compatibility, minimum overrides maximum if the two conflict.
+    // The following logic ensures that there is always a minimum of at least one digit.
+    if (minInt == 0 && maxFrac != 0) {
+      // Force a digit after the decimal point.
+      minFrac = minFrac <= 0 ? 1 : minFrac;
+      maxFrac = maxFrac < 0 ? Integer.MAX_VALUE : maxFrac < minFrac ? minFrac : maxFrac;
+      minInt = 0;
+      maxInt = maxInt < 0 ? Integer.MAX_VALUE : maxInt;
+    } else {
+      // Force a digit before the decimal point.
+      minFrac = minFrac < 0 ? 0 : minFrac;
+      maxFrac = maxFrac < 0 ? Integer.MAX_VALUE : maxFrac < minFrac ? minFrac : maxFrac;
+      minInt = minInt <= 0 ? 1 : minInt;
+      maxInt = maxInt < 0 ? Integer.MAX_VALUE : maxInt < minInt ? minInt : maxInt;
+    }
+    Rounding rounding = null;
+    if (explicitCurrencyUsage) {
+      rounding = RoundingImplCurrency.getInstance(currencyUsage).withCurrency(currency);
+    } else if (roundingIncrement != null) {
+      rounding = RoundingImplIncrement.getInstance(roundingIncrement);
+    } else if (explicitMinMaxSig) {
+      minSig = minSig < 1 ? 1 : minSig > 1000 ? 1000 : minSig;
+      maxSig = maxSig < 0 ? 1000 : maxSig < minSig ? minSig : maxSig > 1000 ? 1000 : maxSig;
+      rounding = RoundingImplSignificant.getInstance(minSig, maxSig);
+    } else if (explicitMinMaxFrac) {
+      rounding = RoundingImplFraction.getInstance(minFrac, maxFrac);
+    } else if (useCurrency) {
+      rounding = RoundingImplCurrency.getInstance(currencyUsage);
+    }
+    if (rounding != null) {
+      rounding = rounding.withMode(mathContext);
+      macros.rounding = rounding;
+    }
+
+    ///////////////////
+    // INTEGER WIDTH //
+    ///////////////////
+
+    macros.integerWidth = IntegerWidth.zeroFillTo(minInt).truncateAt(maxInt);
+
+    ///////////////////////
+    // GROUPING STRATEGY //
+    ///////////////////////
+
+    int grouping1 = properties.getGroupingSize();
+    int grouping2 = properties.getSecondaryGroupingSize();
+    int minGrouping = properties.getMinimumGroupingDigits();
+    assert grouping1 >= -2; // value of -2 means to forward no grouping information
+    grouping1 = grouping1 > 0 ? grouping1 : grouping2 > 0 ? grouping2 : grouping1;
+    grouping2 = grouping2 > 0 ? grouping2 : grouping1;
+    // TODO: Is it important to handle minGrouping > 2?
+    macros.grouping =
+        GroupingImpl.getInstance((short) grouping1, (short) grouping2, minGrouping == 2);
+
+    /////////////
+    // PADDING //
+    /////////////
+
+    if (properties.getFormatWidth() != Properties.DEFAULT_FORMAT_WIDTH) {
+      macros.padding =
+          PaddingImpl.getInstance(
+              properties.getPadString(), properties.getFormatWidth(), properties.getPadPosition());
+    }
+
+    ///////////////////////////////
+    // DECIMAL MARK ALWAYS SHOWN //
+    ///////////////////////////////
+
+    macros.decimal =
+        properties.getDecimalSeparatorAlwaysShown()
+            ? DecimalMarkDisplay.ALWAYS_SHOWN
+            : DecimalMarkDisplay.AUTO;
+
+    ///////////////////////
+    // SIGN ALWAYS SHOWN //
+    ///////////////////////
+
+    macros.sign = properties.getSignAlwaysShown() ? SignDisplay.ALWAYS_SHOWN : SignDisplay.AUTO;
+
+    /////////////////////////
+    // SCIENTIFIC NOTATION //
+    /////////////////////////
+
+    if (properties.getMinimumExponentDigits() != Properties.DEFAULT_MINIMUM_EXPONENT_DIGITS) {
+      // Scientific notation is required.
+      // The mapping from property bag to scientific notation is nontrivial due to LDML rules.
+      // The maximum of 8 engineering digits has unknown origins and is not in the spec.
+      int engineering =
+          (maxInt != Integer.MAX_VALUE) ? maxInt : properties.getMaximumIntegerDigits();
+      engineering = (engineering < 0) ? 0 : (engineering > 8) ? minInt : engineering;
+      macros.notation =
+          new NotationScientificImpl(
+              // Engineering interval:
+              engineering,
+              // Enforce minimum integer digits (for patterns like "000.00E0"):
+              (engineering == minInt),
+              // Minimum exponent digits:
+              properties.getMinimumExponentDigits(),
+              // Exponent sign always shown:
+              properties.getExponentSignAlwaysShown()
+                  ? SignDisplay.ALWAYS_SHOWN
+                  : SignDisplay.AUTO);
+      // Scientific notation also involves overriding the rounding mode.
+      if (macros.rounding instanceof RoundingImplFraction) {
+        int minInt_ = properties.getMinimumIntegerDigits();
+        int minFrac_ = properties.getMinimumFractionDigits();
+        int maxFrac_ = properties.getMaximumFractionDigits();
+        if (minInt_ == 0 && maxFrac_ == 0) {
+          // Patterns like "#E0" and "##E0", which mean no rounding!
+          macros.rounding = Rounding.NONE.withMode(mathContext);
+        } else if (minInt_ == 0 && minFrac_ == 0) {
+          // Patterns like "#.##E0" (no zeros in the mantissa), which mean round to maxFrac+1
+          macros.rounding = new RoundingImplSignificant(1, maxFrac_ + 1).withMode(mathContext);
+        } else {
+          // All other scientific patterns, which mean round to minInt+maxFrac
+          macros.rounding =
+              new RoundingImplSignificant(minInt_ + minFrac_, minInt_ + maxFrac_)
+                  .withMode(mathContext);
+        }
+      }
+    }
+
+    //////////////////////
+    // COMPACT NOTATION //
+    //////////////////////
+
+    if (properties.getCompactStyle() != Properties.DEFAULT_COMPACT_STYLE) {
+      if (properties.getCompactCustomData() != null) {
+        macros.notation = new NotationImpl.NotationCompactImpl(properties.getCompactCustomData());
+      } else if (properties.getCompactStyle() == CompactStyle.LONG) {
+        macros.notation = Notation.COMPACT_LONG;
+      } else {
+        macros.notation = Notation.COMPACT_SHORT;
+      }
+      // Do not forward the affix provider.
+      macros.affixProvider = null;
+    }
+
+    /////////////////
+    // MULTIPLIERS //
+    /////////////////
+
+    if (properties.getMagnitudeMultiplier() != Properties.DEFAULT_MAGNITUDE_MULTIPLIER) {
+      macros.multiplier = new MultiplierImpl(properties.getMagnitudeMultiplier());
+    } else if (properties.getMultiplier() != Properties.DEFAULT_MULTIPLIER) {
+      macros.multiplier = new MultiplierImpl(properties.getMultiplier());
+    }
+
+    //////////////////////
+    // PROPERTY EXPORTS //
+    //////////////////////
+
+    if (exportedProperties != null) {
+
+      exportedProperties.setMathContext(mathContext);
+      exportedProperties.setRoundingMode(mathContext.getRoundingMode());
+      exportedProperties.setMinimumIntegerDigits(minInt);
+      exportedProperties.setMaximumIntegerDigits(maxInt);
+
+      Rounding rounding_;
+      if (rounding instanceof CurrencyRounding) {
+        rounding_ = ((CurrencyRounding) rounding).withCurrency(currency);
+      } else {
+        rounding_ = rounding;
+      }
+      int minFrac_ = minFrac;
+      int maxFrac_ = maxFrac;
+      int minSig_ = minSig;
+      int maxSig_ = maxSig;
+      BigDecimal increment_ = null;
+      if (rounding_ instanceof RoundingImplFraction) {
+        minFrac_ = ((RoundingImplFraction) rounding_).minFrac;
+        maxFrac_ = ((RoundingImplFraction) rounding_).maxFrac;
+      } else if (rounding_ instanceof RoundingImplIncrement) {
+        increment_ = ((RoundingImplIncrement) rounding_).increment;
+        minFrac_ = increment_.scale();
+        maxFrac_ = increment_.scale();
+      } else if (rounding_ instanceof RoundingImplSignificant) {
+        minSig_ = ((RoundingImplSignificant) rounding_).minSig;
+        maxSig_ = ((RoundingImplSignificant) rounding_).maxSig;
+      }
+
+      exportedProperties.setMinimumFractionDigits(minFrac_);
+      exportedProperties.setMaximumFractionDigits(maxFrac_);
+      exportedProperties.setMinimumSignificantDigits(minSig_);
+      exportedProperties.setMaximumSignificantDigits(maxSig_);
+      exportedProperties.setRoundingIncrement(increment_);
+    }
+
+    return macros;
+  }
+
+  private static class PropertiesAffixPatternProvider implements AffixPatternProvider {
+    private final String posPrefixPattern;
+    private final String posSuffixPattern;
+    private final String negPrefixPattern;
+    private final String negSuffixPattern;
+
+    public PropertiesAffixPatternProvider(String ppp, String psp, String npp, String nsp) {
+      if (ppp == null) ppp = "";
+      if (psp == null) psp = "";
+      if (npp == null && nsp != null) npp = "-"; // TODO: This is a hack.
+      if (nsp == null && npp != null) nsp = "";
+      posPrefixPattern = ppp;
+      posSuffixPattern = psp;
+      negPrefixPattern = npp;
+      negSuffixPattern = nsp;
+    }
+
+    @Override
+    public char charAt(int flags, int i) {
+      boolean prefix = (flags & Flags.PREFIX) != 0;
+      boolean negative = (flags & Flags.NEGATIVE_SUBPATTERN) != 0;
+      if (prefix && negative) {
+        return negPrefixPattern.charAt(i);
+      } else if (prefix) {
+        return posPrefixPattern.charAt(i);
+      } else if (negative) {
+        return negSuffixPattern.charAt(i);
+      } else {
+        return posSuffixPattern.charAt(i);
+      }
+    }
+
+    @Override
+    public int length(int flags) {
+      boolean prefix = (flags & Flags.PREFIX) != 0;
+      boolean negative = (flags & Flags.NEGATIVE_SUBPATTERN) != 0;
+      if (prefix && negative) {
+        return negPrefixPattern.length();
+      } else if (prefix) {
+        return posPrefixPattern.length();
+      } else if (negative) {
+        return negSuffixPattern.length();
+      } else {
+        return posSuffixPattern.length();
+      }
+    }
+
+    @Override
+    public boolean positiveHasPlusSign() {
+      return AffixPatternUtils.containsType(posPrefixPattern, AffixPatternUtils.TYPE_PLUS_SIGN)
+          || AffixPatternUtils.containsType(posSuffixPattern, AffixPatternUtils.TYPE_PLUS_SIGN);
+    }
+
+    @Override
+    public boolean hasNegativeSubpattern() {
+      return negPrefixPattern != null;
+    }
+
+    @Override
+    public boolean negativeHasMinusSign() {
+      return AffixPatternUtils.containsType(negPrefixPattern, AffixPatternUtils.TYPE_MINUS_SIGN)
+          || AffixPatternUtils.containsType(negSuffixPattern, AffixPatternUtils.TYPE_MINUS_SIGN);
+    }
+
+    @Override
+    public boolean hasCurrencySign() {
+      return AffixPatternUtils.hasCurrencySymbols(posPrefixPattern)
+          || AffixPatternUtils.hasCurrencySymbols(posSuffixPattern)
+          || AffixPatternUtils.hasCurrencySymbols(negPrefixPattern)
+          || AffixPatternUtils.hasCurrencySymbols(negSuffixPattern);
+    }
+
+    @Override
+    public boolean containsSymbolType(int type) {
+      return AffixPatternUtils.containsType(posPrefixPattern, type)
+          || AffixPatternUtils.containsType(posSuffixPattern, type)
+          || AffixPatternUtils.containsType(negPrefixPattern, type)
+          || AffixPatternUtils.containsType(negSuffixPattern, type);
+    }
+  }
+
+  private static class CurrencyPluralInfoAffixProvider implements AffixPatternProvider {
+    private final AffixPatternProvider[] affixesByPlural;
+
+    public CurrencyPluralInfoAffixProvider(CurrencyPluralInfo cpi) {
+      affixesByPlural = new PatternParseResult[StandardPlural.COUNT];
+      for (StandardPlural plural : StandardPlural.VALUES) {
+        affixesByPlural[plural.ordinal()] =
+            LdmlPatternInfo.parse(cpi.getCurrencyPluralPattern(plural.getKeyword()));
+      }
+    }
+
+    @Override
+    public char charAt(int flags, int i) {
+      int pluralOrdinal = (flags & Flags.PLURAL_MASK);
+      return affixesByPlural[pluralOrdinal].charAt(flags, i);
+    }
+
+    @Override
+    public int length(int flags) {
+      int pluralOrdinal = (flags & Flags.PLURAL_MASK);
+      return affixesByPlural[pluralOrdinal].length(flags);
+    }
+
+    @Override
+    public boolean positiveHasPlusSign() {
+      return affixesByPlural[StandardPlural.OTHER.ordinal()].positiveHasPlusSign();
+    }
+
+    @Override
+    public boolean hasNegativeSubpattern() {
+      return affixesByPlural[StandardPlural.OTHER.ordinal()].hasNegativeSubpattern();
+    }
+
+    @Override
+    public boolean negativeHasMinusSign() {
+      return affixesByPlural[StandardPlural.OTHER.ordinal()].negativeHasMinusSign();
+    }
+
+    @Override
+    public boolean hasCurrencySign() {
+      return affixesByPlural[StandardPlural.OTHER.ordinal()].hasCurrencySign();
+    }
+
+    @Override
+    public boolean containsSymbolType(int type) {
+      return affixesByPlural[StandardPlural.OTHER.ordinal()].containsSymbolType(type);
+    }
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/PaddingImpl.java b/icu4j/main/classes/core/src/newapi/impl/PaddingImpl.java
new file mode 100644 (file)
index 0000000..f747ebb
--- /dev/null
@@ -0,0 +1,109 @@
+// © 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.NumberStringBuilder;
+import com.ibm.icu.impl.number.formatters.PaddingFormat.PadPosition;
+
+import newapi.NumberFormatter.Padding;
+
+public class PaddingImpl extends Padding.Internal {
+
+  String paddingString;
+  int targetWidth;
+  PadPosition position;
+
+  public static final PaddingImpl NONE = new PaddingImpl();
+
+  public static PaddingImpl getInstance(
+      String paddingString, int targetWidth, PadPosition position) {
+    // TODO: Add a few default implementations
+    return new PaddingImpl(paddingString, targetWidth, position);
+  }
+
+  /** Default constructor, producing an empty instance */
+  public PaddingImpl() {
+    paddingString = null;
+    targetWidth = -1;
+    position = null;
+  }
+
+  private PaddingImpl(String paddingString, int targetWidth, PadPosition position) {
+    this.paddingString = (paddingString == null) ? " " : paddingString;
+    this.targetWidth = targetWidth;
+    this.position = (position == null) ? PadPosition.BEFORE_PREFIX : position;
+  }
+
+  public int applyModsAndMaybePad(
+      MicroProps micros, NumberStringBuilder string, int leftIndex, int rightIndex) {
+    // Apply modInner (scientific notation) before padding
+    int innerLength = micros.modInner.apply(string, leftIndex, rightIndex);
+
+    // No padding; apply the mods and leave.
+    if (targetWidth < 0) {
+      return applyMicroMods(micros, string, leftIndex, rightIndex + innerLength);
+    }
+
+    // Estimate the padding width needed.
+    // TODO: Make this more efficient (less copying)
+    // TODO: How to handle when padding is inserted between a currency sign and the number
+    // when currency spacing is in play?
+    NumberStringBuilder backup = new NumberStringBuilder(string);
+    int length = innerLength + applyMicroMods(micros, string, leftIndex, rightIndex + innerLength);
+    int requiredPadding = targetWidth - string.codePointCount();
+
+    if (requiredPadding <= 0) {
+      // Padding is not required.
+      return length;
+    }
+
+    length = innerLength;
+    string.copyFrom(backup);
+    if (position == PadPosition.AFTER_PREFIX) {
+      length += addPaddingHelper(paddingString, requiredPadding, string, leftIndex);
+    } else if (position == PadPosition.BEFORE_SUFFIX) {
+      length += addPaddingHelper(paddingString, requiredPadding, string, rightIndex + length);
+    }
+    length += applyMicroMods(micros, string, leftIndex, rightIndex + length);
+    if (position == PadPosition.BEFORE_PREFIX) {
+      length = addPaddingHelper(paddingString, requiredPadding, string, leftIndex);
+    } else if (position == PadPosition.AFTER_SUFFIX) {
+      length = addPaddingHelper(paddingString, requiredPadding, string, rightIndex + length);
+    }
+
+    // The length might not be exactly right due to currency spacing.
+    // Make an adjustment if needed.
+    while (string.codePointCount() < targetWidth) {
+      int insertIndex;
+      switch (position) {
+        case AFTER_PREFIX:
+          insertIndex = leftIndex + length;
+          break;
+        case BEFORE_SUFFIX:
+          insertIndex = rightIndex + length;
+          break;
+        default:
+          // Should not happen since currency spacing is always on the inside.
+          throw new AssertionError();
+      }
+      length += string.insert(insertIndex, paddingString, null);
+    }
+
+    return length;
+  }
+
+  private static int applyMicroMods(
+      MicroProps micros, NumberStringBuilder string, int leftIndex, int rightIndex) {
+    int length = micros.modMiddle.apply(string, leftIndex, rightIndex);
+    length += micros.modOuter.apply(string, leftIndex, rightIndex + length);
+    return length;
+  }
+
+  private static int addPaddingHelper(
+      String paddingString, int requiredPadding, NumberStringBuilder string, int index) {
+    for (int i = 0; i < requiredPadding; i++) {
+      string.insert(index, paddingString, null);
+    }
+    return paddingString.length() * requiredPadding;
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/PositiveDecimalImpl.java b/icu4j/main/classes/core/src/newapi/impl/PositiveDecimalImpl.java
new file mode 100644 (file)
index 0000000..8c59a71
--- /dev/null
@@ -0,0 +1,114 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+// License & terms of use: http://www.unicode.org/copyright.html#License
+
+import com.ibm.icu.impl.number.Format;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.Properties;
+import com.ibm.icu.text.NumberFormat;
+
+import newapi.NumberFormatter.DecimalMarkDisplay;
+
+public class PositiveDecimalImpl implements Format.TargetFormat {
+
+  @Override
+  public int target(FormatQuantity input, NumberStringBuilder string, int startIndex) {
+    // FIXME
+    throw new UnsupportedOperationException();
+  }
+
+  /**
+   * @param micros
+   * @param fq
+   * @param output
+   * @return
+   */
+  public static int apply(MicroProps micros, FormatQuantity input, NumberStringBuilder string) {
+    int length = 0;
+    if (input.isInfinite()) {
+      length += string.insert(length, micros.symbols.getInfinity(), NumberFormat.Field.INTEGER);
+
+    } else if (input.isNaN()) {
+      length += string.insert(length, micros.symbols.getNaN(), NumberFormat.Field.INTEGER);
+
+    } else {
+      // Add the integer digits
+      length += addIntegerDigits(micros, input, string);
+
+      // Add the decimal point
+      if (input.getLowerDisplayMagnitude() < 0
+          || micros.decimal == DecimalMarkDisplay.ALWAYS_SHOWN) {
+        length +=
+            string.insert(
+                length,
+                micros.useCurrency
+                    ? micros.symbols.getMonetaryDecimalSeparatorString()
+                    : micros.symbols.getDecimalSeparatorString(),
+                NumberFormat.Field.DECIMAL_SEPARATOR);
+      }
+
+      // Add the fraction digits
+      length += addFractionDigits(micros, input, string);
+    }
+
+    return length;
+  }
+
+  private static int addIntegerDigits(
+      MicroProps micros, FormatQuantity input, NumberStringBuilder string) {
+    int length = 0;
+    int integerCount = input.getUpperDisplayMagnitude() + 1;
+    for (int i = 0; i < integerCount; i++) {
+      // Add grouping separator
+      if (micros.grouping.groupAtPosition(i, input)) {
+        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);
+      if (micros.symbols.getCodePointZero() != -1) {
+        length +=
+            string.insertCodePoint(
+                0, micros.symbols.getCodePointZero() + nextDigit, NumberFormat.Field.INTEGER);
+      } else {
+        length +=
+            string.insert(
+                0, micros.symbols.getDigitStringsLocal()[nextDigit], NumberFormat.Field.INTEGER);
+      }
+    }
+    return length;
+  }
+
+  private static int addFractionDigits(
+      MicroProps micros, FormatQuantity input, NumberStringBuilder string) {
+    int length = 0;
+    int fractionCount = -input.getLowerDisplayMagnitude();
+    for (int i = 0; i < fractionCount; i++) {
+      // Get and append the next digit value
+      byte nextDigit = input.getDigit(-i - 1);
+      if (micros.symbols.getCodePointZero() != -1) {
+        length +=
+            string.appendCodePoint(
+                micros.symbols.getCodePointZero() + nextDigit, NumberFormat.Field.FRACTION);
+      } else {
+        length +=
+            string.append(
+                micros.symbols.getDigitStringsLocal()[nextDigit], NumberFormat.Field.FRACTION);
+      }
+    }
+    return length;
+  }
+
+  @Override
+  public void export(Properties properties) {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/QuantityChain.java b/icu4j/main/classes/core/src/newapi/impl/QuantityChain.java
new file mode 100644 (file)
index 0000000..9efedec
--- /dev/null
@@ -0,0 +1,10 @@
+// © 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 chain(QuantityChain parent);
+  MicroProps withQuantity(FormatQuantity quantity);
+}
\ No newline at end of file
diff --git a/icu4j/main/classes/core/src/newapi/impl/QuantityDependentModOuter.java b/icu4j/main/classes/core/src/newapi/impl/QuantityDependentModOuter.java
new file mode 100644 (file)
index 0000000..45d1ad0
--- /dev/null
@@ -0,0 +1,37 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.util.Map;
+
+import com.ibm.icu.impl.StandardPlural;
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.text.PluralRules;
+
+public class QuantityDependentModOuter implements QuantityChain {
+  final Map<StandardPlural, Modifier> data;
+  final PluralRules rules;
+  /* final */ QuantityChain parent;
+
+  public QuantityDependentModOuter(Map<StandardPlural, Modifier> data, PluralRules rules) {
+    this.data = data;
+    this.rules = rules;
+  }
+
+  @Override
+  public QuantityChain chain(QuantityChain parent) {
+    this.parent = parent;
+    return this;
+  }
+
+  @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;
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/RoundingImpl.java b/icu4j/main/classes/core/src/newapi/impl/RoundingImpl.java
new file mode 100644 (file)
index 0000000..30f204c
--- /dev/null
@@ -0,0 +1,445 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+
+import com.ibm.icu.impl.number.FormatQuantity;
+import com.ibm.icu.impl.number.FormatQuantity4;
+import com.ibm.icu.impl.number.LdmlPatternInfo.PatternParseResult;
+import com.ibm.icu.impl.number.RoundingUtils;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.Currency.CurrencyUsage;
+
+import newapi.NumberFormatter.CurrencyRounding;
+import newapi.NumberFormatter.FractionRounding;
+import newapi.NumberFormatter.IRounding;
+import newapi.NumberFormatter.Rounding;
+
+/**
+ * The internal version of {@link Rounding} with additional methods.
+ *
+ * <p>Although it seems as though RoundingImpl should extend Rounding, it actually extends
+ * FractionRounding. This is because instances of FractionRounding are self-contained rounding
+ * instances themselves, and they need to implement RoundingImpl. When ICU adopts Java 8, there will
+ * be more options for the polymorphism, such as multiple inheritance with interfaces having default
+ * methods and static factory methods on interfaces.
+ */
+@SuppressWarnings("deprecation")
+public abstract class RoundingImpl extends FractionRounding.Internal implements Cloneable {
+
+  public static RoundingImpl forPattern(PatternParseResult patternInfo) {
+    if (patternInfo.positive.rounding != null) {
+      return RoundingImplIncrement.getInstance(patternInfo.positive.rounding.toBigDecimal());
+    } else if (patternInfo.positive.minimumSignificantDigits > 0) {
+      return RoundingImplSignificant.getInstance(
+          patternInfo.positive.minimumSignificantDigits,
+          patternInfo.positive.maximumSignificantDigits);
+    } else if (patternInfo.positive.exponentDigits > 0) {
+      // FIXME
+      throw new UnsupportedOperationException();
+    } else {
+      return RoundingImplFraction.getInstance(
+          patternInfo.positive.minimumFractionDigits, patternInfo.positive.maximumFractionDigits);
+    }
+  }
+
+  /**
+   * Returns a RoundingImpl no matter what is the type of the provided argument. If the argument is
+   * already a RoundingImpl, this method just returns the same object. Otherwise, it does some
+   * processing to build a RoundingImpl.
+   *
+   * @param rounding The input object, which might or might not be a RoundingImpl.
+   * @param currency A currency object to use in case the input object needs it.
+   * @return A RoundingImpl object.
+   */
+  public static RoundingImpl normalizeType(IRounding rounding, Currency currency) {
+    if (rounding instanceof RoundingImpl) {
+      return (RoundingImpl) rounding;
+    } else if (rounding instanceof RoundingImplCurrency) {
+      return ((RoundingImplCurrency) rounding).withCurrency(currency);
+    } else {
+      return RoundingImplLambda.getInstance(rounding);
+    }
+  }
+
+  private static final MathContext DEFAULT_MATH_CONTEXT =
+      RoundingUtils.mathContextUnlimited(RoundingMode.HALF_EVEN);
+
+  public MathContext mathContext;
+
+  public RoundingImpl() {
+    this.mathContext = DEFAULT_MATH_CONTEXT;
+    // TODO: This is ugly, but necessary if a RoundingImpl is created
+    // before this class has been initialized.
+    if (this.mathContext == null) {
+      this.mathContext = RoundingUtils.mathContextUnlimited(RoundingMode.HALF_EVEN);
+    }
+  }
+
+  @Override
+  public Rounding withMode(RoundingMode roundingMode) {
+    return withMode(RoundingUtils.mathContextUnlimited(roundingMode));
+  }
+
+  @Override
+  public Rounding withMode(MathContext mathContext) {
+    if (this.mathContext.equals(mathContext)) {
+      return this;
+    }
+    RoundingImpl other = (RoundingImpl) this.clone();
+    other.mathContext = mathContext;
+    return other;
+  }
+
+  abstract void apply(FormatQuantity value);
+
+  @Override
+  public BigDecimal round(BigDecimal input) {
+    // Provided for API compatibility.
+    FormatQuantity fq = new FormatQuantity4(input);
+    this.apply(fq);
+    return fq.toBigDecimal();
+  }
+
+  static interface MultiplierProducer {
+    int getMultiplier(int magnitude);
+  }
+
+  int chooseMultiplierAndApply(FormatQuantity input, MultiplierProducer producer) {
+    // TODO: Make a better and more efficient implementation.
+    // TODO: Avoid the object creation here.
+    FormatQuantity copy = input.createCopy();
+
+    assert !input.isZero();
+    int magnitude = input.getMagnitude();
+    int multiplier = producer.getMultiplier(magnitude);
+    input.adjustMagnitude(multiplier);
+    apply(input);
+
+    // If the number turned to zero when rounding, do not re-attempt the rounding.
+    if (!input.isZero() && input.getMagnitude() == magnitude + multiplier + 1) {
+      magnitude += 1;
+      input.copyFrom(copy);
+      multiplier = producer.getMultiplier(magnitude);
+      input.adjustMagnitude(multiplier);
+      assert input.getMagnitude() == magnitude + multiplier - 1;
+      apply(input);
+      assert input.getMagnitude() == magnitude + multiplier;
+    }
+
+    return multiplier;
+  }
+
+  @Override
+  public Object clone() {
+    try {
+      return super.clone();
+    } catch (CloneNotSupportedException e) {
+      // Should not happen since parent is Object
+      throw new AssertionError(e);
+    }
+  }
+
+  /** A dummy class used when the number has already been rounded elsewhere. */
+  public static class RoundingImplDummy extends RoundingImpl {
+    public static final RoundingImplDummy INSTANCE = new RoundingImplDummy();
+
+    private RoundingImplDummy() {}
+
+    @Override
+    void apply(FormatQuantity value) {}
+  }
+
+  public static class RoundingImplInfinity extends RoundingImpl {
+    @Override
+    void apply(FormatQuantity value) {
+      value.roundToInfinity();
+      value.setFractionLength(0, Integer.MAX_VALUE);
+    }
+  }
+
+  public static class RoundingImplFraction extends RoundingImpl {
+    int minFrac;
+    int maxFrac;
+
+    private static final RoundingImplFraction FIXED_0 = new RoundingImplFraction(0, 0);
+    private static final RoundingImplFraction FIXED_2 = new RoundingImplFraction(2, 2);
+
+    /** Assumes that minFrac <= maxFrac. */
+    public static RoundingImplFraction getInstance(int minFrac, int maxFrac) {
+      assert minFrac >= 0 && minFrac <= maxFrac;
+      if (minFrac == 0 && maxFrac == 0) {
+        return FIXED_0;
+      } else if (minFrac == 2 && maxFrac == 2) {
+        return FIXED_2;
+      } else {
+        return new RoundingImplFraction(minFrac, maxFrac);
+      }
+    }
+
+    /** Hook for public static final; uses integer rounding */
+    public RoundingImplFraction() {
+      this(0, 0);
+    }
+
+    private RoundingImplFraction(int minFrac, int maxFrac) {
+      this.minFrac = minFrac;
+      this.maxFrac = maxFrac;
+    }
+
+    @Override
+    void apply(FormatQuantity value) {
+      value.roundToMagnitude(getRoundingMagnitude(maxFrac), mathContext);
+      value.setFractionLength(Math.max(0, -getDisplayMagnitude(minFrac)), Integer.MAX_VALUE);
+    }
+
+    static int getRoundingMagnitude(int maxFrac) {
+      if (maxFrac == Integer.MAX_VALUE) {
+        return Integer.MIN_VALUE;
+      }
+      return -maxFrac;
+    }
+
+    static int getDisplayMagnitude(int minFrac) {
+      if (minFrac == 0) {
+        return Integer.MAX_VALUE;
+      }
+      return -minFrac;
+    }
+
+    @Override
+    public Rounding withMinFigures(int minFigures) {
+      if (minFigures > 0 && minFigures <= MAX_VALUE) {
+        return RoundingImplFractionSignificant.getInstance(this, minFigures, -1);
+      } else {
+        throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
+      }
+    }
+
+    @Override
+    public Rounding withMaxFigures(int maxFigures) {
+      if (maxFigures > 0 && maxFigures <= MAX_VALUE) {
+        return RoundingImplFractionSignificant.getInstance(this, -1, maxFigures);
+      } else {
+        throw new IllegalArgumentException("Significant digits must be between 0 and " + MAX_VALUE);
+      }
+    }
+  }
+
+  public static class RoundingImplSignificant extends RoundingImpl {
+    int minSig;
+    int maxSig;
+
+    private static final RoundingImplSignificant FIXED_2 = new RoundingImplSignificant(2, 2);
+    private static final RoundingImplSignificant FIXED_3 = new RoundingImplSignificant(3, 3);
+    private static final RoundingImplSignificant RANGE_2_3 = new RoundingImplSignificant(2, 3);
+
+    /** Assumes that minSig <= maxSig. */
+    public static RoundingImplSignificant getInstance(int minSig, int maxSig) {
+      assert minSig >= 0 && minSig <= maxSig;
+      if (minSig == 2 && maxSig == 2) {
+        return FIXED_2;
+      } else if (minSig == 3 && maxSig == 3) {
+        return FIXED_3;
+      } else if (minSig == 2 && maxSig == 3) {
+        return RANGE_2_3;
+      } else {
+        return new RoundingImplSignificant(minSig, maxSig);
+      }
+    }
+
+    RoundingImplSignificant(int minSig, int maxSig) {
+      this.minSig = minSig;
+      this.maxSig = maxSig;
+    }
+
+    @Override
+    void apply(FormatQuantity value) {
+      value.roundToMagnitude(getRoundingMagnitude(value, maxSig), mathContext);
+      value.setFractionLength(Math.max(0, -getDisplayMagnitude(value, minSig)), Integer.MAX_VALUE);
+    }
+
+    /** Version of {@link #apply} that obeys minInt constraints. */
+    public void apply(FormatQuantity quantity, int minInt) {
+      assert quantity.isZero();
+      quantity.setFractionLength(minSig - minInt, Integer.MAX_VALUE);
+    }
+
+    static int getRoundingMagnitude(FormatQuantity value, int maxSig) {
+      if (maxSig == Integer.MAX_VALUE) {
+        return Integer.MIN_VALUE;
+      }
+      int magnitude = value.isZero() ? 0 : value.getMagnitude();
+      return magnitude - maxSig + 1;
+    }
+
+    static int getDisplayMagnitude(FormatQuantity value, int minSig) {
+      int magnitude = value.isZero() ? 0 : value.getMagnitude();
+      return magnitude - minSig + 1;
+    }
+  }
+
+  public static class RoundingImplFractionSignificant extends RoundingImpl {
+    int minFrac;
+    int maxFrac;
+    int minSig;
+    int maxSig;
+
+    // Package-private
+    static final RoundingImplFractionSignificant COMPACT_STRATEGY =
+        new RoundingImplFractionSignificant(0, 0, 2, -1);
+
+    public static Rounding getInstance(FractionRounding _base, int minSig, int maxSig) {
+      assert _base instanceof RoundingImplFraction;
+      RoundingImplFraction base = (RoundingImplFraction) _base;
+      if (base.minFrac == 0 && base.maxFrac == 0 && minSig == 2 /* && maxSig == -1 */) {
+        return COMPACT_STRATEGY;
+      } else {
+        return new RoundingImplFractionSignificant(base.minFrac, base.maxFrac, minSig, maxSig);
+      }
+    }
+
+    /** Assumes that minFrac <= maxFrac and minSig <= maxSig except for -1. */
+    private RoundingImplFractionSignificant(int minFrac, int maxFrac, int minSig, int maxSig) {
+      // Exactly one of the arguments should be -1, either minSig or maxSig.
+      assert (minFrac != -1 && maxFrac != -1 && minSig == -1 && maxSig != -1 && minFrac <= maxFrac)
+          || (minFrac != -1 && maxFrac != -1 && minSig != -1 && maxSig == -1 && minFrac <= maxFrac);
+      this.minFrac = minFrac;
+      this.maxFrac = maxFrac;
+      this.minSig = minSig;
+      this.maxSig = maxSig;
+    }
+
+    @Override
+    void apply(FormatQuantity value) {
+      int displayMag = RoundingImplFraction.getDisplayMagnitude(minFrac);
+      int roundingMag = RoundingImplFraction.getRoundingMagnitude(maxFrac);
+      if (minSig == -1) {
+        // Max Sig override
+        int candidate = RoundingImplSignificant.getRoundingMagnitude(value, maxSig);
+        roundingMag = Math.max(roundingMag, candidate);
+      } else {
+        // Min Sig override
+        int candidate = RoundingImplSignificant.getDisplayMagnitude(value, minSig);
+        roundingMag = Math.min(roundingMag, candidate);
+      }
+      value.roundToMagnitude(roundingMag, mathContext);
+      value.setFractionLength(Math.max(0, -displayMag), Integer.MAX_VALUE);
+    }
+  }
+
+  public static class RoundingImplIncrement extends RoundingImpl {
+    BigDecimal increment;
+
+    private static final RoundingImplIncrement NICKEL =
+        new RoundingImplIncrement(BigDecimal.valueOf(0.5));
+
+    public static RoundingImplIncrement getInstance(BigDecimal increment) {
+      assert increment != null;
+      if (increment.compareTo(NICKEL.increment) == 0) {
+        return NICKEL;
+      } else {
+        return new RoundingImplIncrement(increment);
+      }
+    }
+
+    private RoundingImplIncrement(BigDecimal increment) {
+      this.increment = increment;
+    }
+
+    @Override
+    void apply(FormatQuantity value) {
+      value.roundToIncrement(increment, mathContext);
+      value.setFractionLength(increment.scale(), increment.scale());
+    }
+  }
+
+  public static class RoundingImplLambda extends RoundingImpl {
+    IRounding lambda;
+
+    public static RoundingImplLambda getInstance(IRounding lambda) {
+      assert !(lambda instanceof Rounding);
+      return new RoundingImplLambda(lambda);
+    }
+
+    private RoundingImplLambda(IRounding lambda) {
+      this.lambda = lambda;
+    }
+
+    @Override
+    void apply(FormatQuantity value) {
+      // TODO: Cache the BigDecimal between calls?
+      BigDecimal temp = value.toBigDecimal();
+      temp = lambda.round(temp);
+      value.setToBigDecimal(temp);
+      value.setFractionLength(temp.scale(), Integer.MAX_VALUE);
+    }
+  }
+
+  /**
+   * NOTE: This is unlike the other classes here. It is NOT a standalone rounder and it does NOT
+   * extend RoundingImpl.
+   */
+  public static class RoundingImplCurrency extends CurrencyRounding.Internal {
+    final CurrencyUsage usage;
+    final MathContext mc;
+
+    private static final RoundingImplCurrency MONETARY_STANDARD =
+        new RoundingImplCurrency(CurrencyUsage.STANDARD, DEFAULT_MATH_CONTEXT);
+
+    private static final RoundingImplCurrency MONETARY_CASH =
+        new RoundingImplCurrency(CurrencyUsage.CASH, DEFAULT_MATH_CONTEXT);
+
+    public static RoundingImplCurrency getInstance(CurrencyUsage usage) {
+      if (usage == CurrencyUsage.STANDARD) {
+        return MONETARY_STANDARD;
+      } else if (usage == CurrencyUsage.CASH) {
+        return MONETARY_CASH;
+      } else {
+        throw new AssertionError();
+      }
+    }
+
+    private RoundingImplCurrency(CurrencyUsage usage, MathContext mc) {
+      this.usage = usage;
+      this.mc = mc;
+    }
+
+    @Override
+    public RoundingImpl withCurrency(Currency currency) {
+      assert currency != null;
+      double incrementDouble = currency.getRoundingIncrement(usage);
+      if (incrementDouble != 0.0) {
+        BigDecimal increment = BigDecimal.valueOf(incrementDouble);
+        return RoundingImplIncrement.getInstance(increment);
+      } else {
+        int minMaxFrac = currency.getDefaultFractionDigits(usage);
+        return RoundingImplFraction.getInstance(minMaxFrac, minMaxFrac);
+      }
+    }
+
+    @Override
+    public RoundingImplCurrency withMode(RoundingMode roundingMode) {
+      // This is similar to RoundingImpl#withMode().
+      return withMode(RoundingUtils.mathContextUnlimited(roundingMode));
+    }
+
+    @Override
+    public RoundingImplCurrency withMode(MathContext mathContext) {
+      // This is similar to RoundingImpl#withMode().
+      if (mc.equals(mathContext)) {
+        return this;
+      }
+      return new RoundingImplCurrency(usage, mathContext);
+    }
+
+    @Override
+    public BigDecimal round(BigDecimal input) {
+      throw new UnsupportedOperationException(
+          "A currency must be specified before calling this method.");
+    }
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/ScientificImpl.java b/icu4j/main/classes/core/src/newapi/impl/ScientificImpl.java
new file mode 100644 (file)
index 0000000..8e40ce8
--- /dev/null
@@ -0,0 +1,144 @@
+// © 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;
+import com.ibm.icu.impl.number.Modifier;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.text.DecimalFormatSymbols;
+import com.ibm.icu.text.NumberFormat;
+
+import newapi.NumberFormatter.SignDisplay;
+import newapi.impl.RoundingImpl.RoundingImplDummy;
+import newapi.impl.RoundingImpl.RoundingImplSignificant;
+
+public class ScientificImpl implements QuantityChain, RoundingImpl.MultiplierProducer {
+
+  final NotationImpl.NotationScientificImpl notation;
+  final DecimalFormatSymbols symbols;
+  final ScientificModifier[] precomputedMods;
+  /* final */ QuantityChain parent;
+
+  public static ScientificImpl getInstance(
+      NotationImpl.NotationScientificImpl notation, DecimalFormatSymbols symbols, boolean build) {
+    return new ScientificImpl(notation, symbols, build);
+  }
+
+  private ScientificImpl(
+      NotationImpl.NotationScientificImpl notation, DecimalFormatSymbols symbols, boolean build) {
+    this.notation = notation;
+    this.symbols = symbols;
+
+    if (build) {
+      // Pre-build the modifiers for exponents -12 through 12
+      precomputedMods = new ScientificModifier[25];
+      for (int i = -12; i <= 12; i++) {
+        precomputedMods[i + 12] = new ScientificModifier(i);
+      }
+    } else {
+      precomputedMods = null;
+    }
+  }
+
+  @Override
+  public QuantityChain chain(QuantityChain parent) {
+    this.parent = parent;
+    return this;
+  }
+
+  @Override
+  public MicroProps withQuantity(FormatQuantity quantity) {
+    MicroProps micros = parent.withQuantity(quantity);
+    assert micros.rounding != null;
+
+    // Treat zero as if it had magnitude 0
+    int exponent;
+    if (quantity.isZero()) {
+      if (notation.requireMinInt && micros.rounding instanceof RoundingImplSignificant) {
+        // Shown "00.000E0" on pattern "00.000E0"
+        ((RoundingImplSignificant) micros.rounding).apply(quantity, notation.engineeringInterval);
+        exponent = 0;
+      } else {
+        micros.rounding.apply(quantity);
+        exponent = 0;
+      }
+    } else {
+      exponent = -micros.rounding.chooseMultiplierAndApply(quantity, this);
+    }
+
+    // Add the Modifier for the scientific format.
+    if (precomputedMods != null && exponent >= -12 && exponent <= 12) {
+      micros.modInner = precomputedMods[exponent + 12];
+    } else {
+      micros.modInner = new ScientificModifier(exponent);
+    }
+
+    // We already performed rounding.  Do not perform it again.
+    micros.rounding = RoundingImplDummy.INSTANCE;
+
+    return micros;
+  }
+
+  private class ScientificModifier implements Modifier {
+    final int exponent;
+
+    ScientificModifier(int exponent) {
+      this.exponent = exponent;
+    }
+
+    @Override
+    public int apply(NumberStringBuilder output, int leftIndex, int rightIndex) {
+      // FIXME: Localized exponent separator location.
+      int i = rightIndex;
+      // Append the exponent separator and sign
+      i += output.insert(i, symbols.getExponentSeparator(), NumberFormat.Field.EXPONENT_SYMBOL);
+      if (exponent < 0 && notation.exponentSignDisplay != SignDisplay.NEVER_SHOWN) {
+        i += output.insert(i, symbols.getMinusSignString(), NumberFormat.Field.EXPONENT_SIGN);
+      } else if (notation.exponentSignDisplay == SignDisplay.ALWAYS_SHOWN) {
+        i += output.insert(i, symbols.getPlusSignString(), NumberFormat.Field.EXPONENT_SIGN);
+      }
+      // Append the exponent digits (using a simple inline algorithm)
+      int disp = Math.abs(exponent);
+      for (int j = 0; j < notation.minExponentDigits || disp > 0; j++, disp /= 10) {
+        int d = disp % 10;
+        String digitString = symbols.getDigitStringsLocal()[d];
+        i += output.insert(i - j, digitString, NumberFormat.Field.EXPONENT);
+      }
+      return i - rightIndex;
+    }
+
+    @Override
+    public boolean isStrong() {
+      return true;
+    }
+
+    @Override
+    public String getPrefix() {
+      // Should never get called
+      throw new AssertionError();
+    }
+
+    @Override
+    public String getSuffix() {
+      // Should never get called
+      throw new AssertionError();
+    }
+  }
+
+  @Override
+  public int getMultiplier(int magnitude) {
+    int interval = notation.engineeringInterval;
+    int digitsShown;
+    if (notation.requireMinInt) {
+      // For patterns like "000.00E0" and ".00E0"
+      digitsShown = interval;
+    } else if (interval <= 1) {
+      // For patterns like "0.00E0" and "@@@E0"
+      digitsShown = 1;
+    } else {
+      // For patterns like "##0.00"
+      digitsShown = ((magnitude % interval + interval) % interval) + 1;
+    }
+    return digitsShown - magnitude - 1;
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/SkeletonBuilder.java b/icu4j/main/classes/core/src/newapi/impl/SkeletonBuilder.java
new file mode 100644 (file)
index 0000000..ef1e0ba
--- /dev/null
@@ -0,0 +1,549 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+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 newapi.NumberFormatter.DecimalMarkDisplay;
+import newapi.NumberFormatter.FractionRounding;
+import newapi.NumberFormatter.Grouping;
+import newapi.NumberFormatter.IGrouping;
+import newapi.NumberFormatter.IRounding;
+import newapi.NumberFormatter.IntegerWidth;
+import newapi.NumberFormatter.Notation;
+import newapi.NumberFormatter.NotationCompact;
+import newapi.NumberFormatter.NotationScientific;
+import newapi.NumberFormatter.NotationSimple;
+import newapi.NumberFormatter.Padding;
+import newapi.NumberFormatter.Rounding;
+import newapi.NumberFormatter.SignDisplay;
+import newapi.impl.RoundingImpl.RoundingImplCurrency;
+import newapi.impl.RoundingImpl.RoundingImplFraction;
+import newapi.impl.RoundingImpl.RoundingImplFractionSignificant;
+import newapi.impl.RoundingImpl.RoundingImplIncrement;
+import newapi.impl.RoundingImpl.RoundingImplInfinity;
+import newapi.impl.RoundingImpl.RoundingImplSignificant;
+
+public final class SkeletonBuilder {
+
+  public static String macrosToSkeleton(MacroProps macros) {
+    // Print out the values in their canonical order.
+    StringBuilder sb = new StringBuilder();
+    if (macros.notation != null) {
+      // sb.append("notation=");
+      notationToSkeleton(macros.notation, sb);
+      sb.append(' ');
+    }
+    if (macros.unit != null) {
+      // sb.append("unit=");
+      unitToSkeleton(macros.unit, sb);
+      sb.append(' ');
+    }
+    if (macros.rounding != null) {
+      // sb.append("rounding=");
+      roundingToSkeleton(macros.rounding, sb);
+      sb.append(' ');
+    }
+    if (macros.grouping != null) {
+      sb.append("grouping=");
+      groupingToSkeleton(macros.grouping, sb);
+      sb.append(' ');
+    }
+    if (macros.padding != null) {
+      sb.append("padding=");
+      paddingToSkeleton(macros.padding, sb);
+      sb.append(' ');
+    }
+    if (macros.integerWidth != null) {
+      sb.append("integer-width=");
+      integerWidthToSkeleton(macros.integerWidth, sb);
+      sb.append(' ');
+    }
+    if (macros.symbols != null) {
+      sb.append("symbols=");
+      symbolsToSkeleton(macros.symbols, sb);
+      sb.append(' ');
+    }
+    if (macros.unitWidth != null) {
+      sb.append("unit-width=");
+      unitWidthToSkeleton(macros.unitWidth, sb);
+      sb.append(' ');
+    }
+    if (macros.sign != null) {
+      sb.append("sign=");
+      signToSkeleton(macros.sign, sb);
+      sb.append(' ');
+    }
+    if (macros.decimal != null) {
+      sb.append("decimal=");
+      decimalToSkeleton(macros.decimal, sb);
+      sb.append(' ');
+    }
+    if (sb.length() > 0) {
+      // Remove the trailing space
+      sb.setLength(sb.length() - 1);
+    }
+    return sb.toString();
+  }
+
+  public static MacroProps skeletonToMacros(String skeleton) {
+    MacroProps macros = new MacroProps();
+    for (int offset = 0; offset < skeleton.length(); ) {
+      char c = skeleton.charAt(offset);
+      switch (c) {
+        case ' ':
+          offset++;
+          break;
+        case 'E':
+        case 'C':
+        case 'I':
+          offset += skeletonToNotation(skeleton, offset, macros);
+          break;
+        case '%':
+        case 'B':
+        case '$':
+        case 'U':
+          offset += skeletonToUnit(skeleton, offset, macros);
+          break;
+        case 'F':
+        case 'S':
+        case 'M':
+        case 'G':
+        case 'Y':
+          offset += skeletonToRounding(skeleton, offset, macros);
+          break;
+        default:
+          if (skeleton.regionMatches(offset, "notation=", 0, 9)) {
+            offset += 9;
+            offset += skeletonToNotation(skeleton, offset, macros);
+          } else if (skeleton.regionMatches(offset, "unit=", 0, 5)) {
+            offset += 5;
+            offset += skeletonToUnit(skeleton, offset, macros);
+          } else if (skeleton.regionMatches(offset, "rounding=", 0, 9)) {
+            offset += 9;
+            offset += skeletonToRounding(skeleton, offset, macros);
+          } else if (skeleton.regionMatches(offset, "grouping=", 0, 9)) {
+            offset += 9;
+            offset += skeletonToGrouping(skeleton, offset, macros);
+          } else if (skeleton.regionMatches(offset, "padding=", 0, 9)) {
+            offset += 8;
+            offset += skeletonToPadding(skeleton, offset, macros);
+          } else if (skeleton.regionMatches(offset, "integer-width=", 0, 9)) {
+            offset += 14;
+            offset += skeletonToIntegerWidth(skeleton, offset, macros);
+          } else if (skeleton.regionMatches(offset, "symbols=", 0, 9)) {
+            offset += 8;
+            offset += skeletonToSymbols(skeleton, offset, macros);
+          } else if (skeleton.regionMatches(offset, "unit-width=", 0, 9)) {
+            offset += 11;
+            offset += skeletonToUnitWidth(skeleton, offset, macros);
+          } else if (skeleton.regionMatches(offset, "sign=", 0, 9)) {
+            offset += 5;
+            offset += skeletonToSign(skeleton, offset, macros);
+          } else if (skeleton.regionMatches(offset, "decimal=", 0, 9)) {
+            offset += 8;
+            offset += skeletonToDecimal(skeleton, offset, macros);
+          } else {
+            throw new IllegalArgumentException(
+                "Unexpected token at offset " + offset + " in skeleton string: " + c);
+          }
+      }
+    }
+    return macros;
+  }
+
+  private static void notationToSkeleton(Notation value, StringBuilder sb) {
+    if (value instanceof NotationScientific) {
+      NotationImpl.NotationScientificImpl notation = (NotationImpl.NotationScientificImpl) value;
+      sb.append('E');
+      if (notation.engineeringInterval != 1) {
+        sb.append(notation.engineeringInterval);
+      }
+      if (notation.exponentSignDisplay == SignDisplay.ALWAYS_SHOWN) {
+        sb.append('+');
+      } else if (notation.exponentSignDisplay == SignDisplay.NEVER_SHOWN) {
+        sb.append('!');
+      } else {
+        assert notation.exponentSignDisplay == SignDisplay.AUTO;
+      }
+      if (notation.minExponentDigits != 1) {
+        for (int i = 0; i < notation.minExponentDigits; i++) {
+          sb.append('0');
+        }
+      }
+    } else if (value instanceof NotationCompact) {
+      NotationImpl.NotationCompactImpl notation = (NotationImpl.NotationCompactImpl) value;
+      if (notation.compactStyle == CompactStyle.SHORT) {
+        sb.append('C');
+      } else {
+        // FIXME: CCC or CCCC instead?
+        sb.append("CC");
+      }
+    } else {
+      assert value instanceof NotationSimple;
+      sb.append('I');
+    }
+  }
+
+  private static int skeletonToNotation(String skeleton, int offset, MacroProps output) {
+    int originalOffset = offset;
+    char c0 = skeleton.charAt(offset++);
+    Notation result = null;
+    if (c0 == 'E') {
+      int engineering = 1;
+      SignDisplay sign = SignDisplay.AUTO;
+      int minExponentDigits = 0;
+      char c = safeCharAt(skeleton, offset++);
+      if (c >= '1' && c <= '9') {
+        engineering = c - '0';
+        c = safeCharAt(skeleton, offset++);
+      }
+      if (c == '+') {
+        sign = SignDisplay.ALWAYS_SHOWN;
+        c = safeCharAt(skeleton, offset++);
+      }
+      if (c == '!') {
+        sign = SignDisplay.NEVER_SHOWN;
+        c = safeCharAt(skeleton, offset++);
+      }
+      while (c == '0') {
+        minExponentDigits++;
+        c = safeCharAt(skeleton, offset++);
+      }
+      minExponentDigits = Math.max(1, minExponentDigits);
+      result = new NotationImpl.NotationScientificImpl(engineering, false, minExponentDigits, sign);
+    } else if (c0 == 'C') {
+      char c = safeCharAt(skeleton, offset++);
+      if (c == 'C') {
+        result = Notation.COMPACT_LONG;
+      } else {
+        result = Notation.COMPACT_SHORT;
+      }
+    } else if (c0 == 'I') {
+      result = Notation.SIMPLE;
+    }
+    output.notation = result;
+    return offset - originalOffset;
+  }
+
+  private static void unitToSkeleton(MeasureUnit value, StringBuilder sb) {
+    if (value.getType().equals("dimensionless")) {
+      if (value.getSubtype().equals("percent")) {
+        sb.append('%');
+      } else if (value.getSubtype().equals("permille")) {
+        sb.append("%%");
+      } else {
+        assert value.getSubtype().equals("base");
+        sb.append('B');
+      }
+    } else if (value.getType().equals("currency")) {
+      sb.append('$');
+      sb.append(value.getSubtype());
+    } else {
+      sb.append('U');
+      sb.append(value.getType());
+      sb.append(':');
+      sb.append(value.getSubtype());
+    }
+  }
+
+  private static int skeletonToUnit(String skeleton, int offset, MacroProps output) {
+    int originalOffset = offset;
+    char c0 = skeleton.charAt(offset++);
+    MeasureUnit result = null;
+    if (c0 == '%') {
+      char c = safeCharAt(skeleton, offset++);
+      if (c == '%') {
+        result = Dimensionless.PERCENT;
+      } else {
+        result = Dimensionless.PERMILLE;
+      }
+    } else if (c0 == 'B') {
+      result = Dimensionless.BASE;
+    } else if (c0 == '$') {
+      String currencyCode = skeleton.substring(offset, offset + 3);
+      offset += 3;
+      result = Currency.getInstance(currencyCode);
+    } else if (c0 == 'U') {
+      StringBuilder sb = new StringBuilder();
+      offset += consumeUntil(skeleton, offset, ':', sb);
+      String type = sb.toString();
+      sb.setLength(0);
+      offset += consumeUntil(skeleton, offset, ' ', sb);
+      String subtype = sb.toString();
+      for (MeasureUnit candidate : MeasureUnit.getAvailable(type)) {
+        if (candidate.getSubtype().equals(subtype)) {
+          result = candidate;
+          break;
+        }
+      }
+    }
+    output.unit = result;
+    return offset - originalOffset;
+  }
+
+  private static void roundingToSkeleton(IRounding value, StringBuilder sb) {
+    if (!(value instanceof Rounding)) {
+      // FIXME: Throw an exception here instead?
+      return;
+    }
+    MathContext mathContext;
+    if (value instanceof RoundingImplFraction) {
+      RoundingImplFraction rounding = (RoundingImplFraction) value;
+      sb.append('F');
+      minMaxToSkeletonHelper(rounding.minFrac, rounding.maxFrac, sb);
+      mathContext = rounding.mathContext;
+    } else if (value instanceof RoundingImplSignificant) {
+      RoundingImplSignificant rounding = (RoundingImplSignificant) value;
+      sb.append('S');
+      minMaxToSkeletonHelper(rounding.minSig, rounding.maxSig, sb);
+      mathContext = rounding.mathContext;
+    } else if (value instanceof RoundingImplFractionSignificant) {
+      RoundingImplFractionSignificant rounding = (RoundingImplFractionSignificant) value;
+      sb.append('F');
+      minMaxToSkeletonHelper(rounding.minFrac, rounding.maxFrac, sb);
+      if (rounding.minSig != -1) {
+        sb.append('>');
+        sb.append(rounding.minSig);
+      } else {
+        sb.append('<');
+        sb.append(rounding.maxSig);
+      }
+      mathContext = rounding.mathContext;
+    } else if (value instanceof RoundingImplIncrement) {
+      RoundingImplIncrement rounding = (RoundingImplIncrement) value;
+      sb.append('M');
+      sb.append(rounding.increment.toString());
+      mathContext = rounding.mathContext;
+    } else if (value instanceof RoundingImplCurrency) {
+      RoundingImplCurrency rounding = (RoundingImplCurrency) value;
+      sb.append('G');
+      sb.append(rounding.usage.name());
+      mathContext = rounding.mc;
+    } else {
+      RoundingImplInfinity rounding = (RoundingImplInfinity) value;
+      sb.append('Y');
+      mathContext = rounding.mathContext;
+    }
+    // RoundingMode
+    RoundingMode roundingMode = mathContext.getRoundingMode();
+    if (roundingMode != RoundingMode.HALF_EVEN) {
+      sb.append(';');
+      sb.append(roundingMode.name());
+    }
+  }
+
+  private static void minMaxToSkeletonHelper(int minFrac, int maxFrac, StringBuilder sb) {
+    if (minFrac == maxFrac) {
+      sb.append(minFrac);
+    } else {
+      boolean showMaxFrac = (maxFrac >= 0 && maxFrac < Integer.MAX_VALUE);
+      if (minFrac > 0 || !showMaxFrac) {
+        sb.append(minFrac);
+      }
+      sb.append('-');
+      if (showMaxFrac) {
+        sb.append(maxFrac);
+      }
+    }
+  }
+
+  private static int skeletonToRounding(String skeleton, int offset, MacroProps output) {
+    int originalOffset = offset;
+    char c0 = skeleton.charAt(offset++);
+    Rounding result = null;
+    if (c0 == 'F') {
+      int[] minMax = new int[2];
+      offset += skeletonToMinMaxHelper(skeleton, offset, minMax);
+      FractionRounding temp = RoundingImplFraction.getInstance(minMax[0], minMax[1]);
+      char c1 = skeleton.charAt(offset++);
+      if (c1 == '<') {
+        char c2 = skeleton.charAt(offset++);
+        result = temp.withMaxFigures(c2 - '0');
+      } else if (c1 == '>') {
+        char c2 = skeleton.charAt(offset++);
+        result = temp.withMinFigures(c2 - '0');
+      } else {
+        result = temp;
+      }
+    } else if (c0 == 'S') {
+      int[] minMax = new int[2];
+      offset += skeletonToMinMaxHelper(skeleton, offset, minMax);
+      result = RoundingImplSignificant.getInstance(minMax[0], minMax[1]);
+    } else if (c0 == 'M') {
+      StringBuilder sb = new StringBuilder();
+      offset += consumeUntil(skeleton, offset, ' ', sb);
+      BigDecimal increment = new BigDecimal(sb.toString());
+      result = RoundingImplIncrement.getInstance(increment);
+    } else if (c0 == 'G') {
+      StringBuilder sb = new StringBuilder();
+      offset += consumeUntil(skeleton, offset, ' ', sb);
+      CurrencyUsage usage = Enum.valueOf(CurrencyUsage.class, sb.toString());
+      result = Rounding.currency(usage);
+    } else if (c0 == 'Y') {
+      result = Rounding.NONE;
+    }
+    output.rounding = result;
+    return offset - originalOffset;
+  }
+
+  private static int skeletonToMinMaxHelper(String skeleton, int offset, int[] output) {
+    int originalOffset = offset;
+    char c0 = safeCharAt(skeleton, offset++);
+    char c1 = safeCharAt(skeleton, offset++);
+    // TODO: This algorithm breaks if the number is more than 1 char wide.
+    if (c1 == '-') {
+      output[0] = c0 - '0';
+      char c2 = safeCharAt(skeleton, offset++);
+      if (c2 == ' ') {
+        output[1] = Integer.MAX_VALUE;
+      } else {
+        output[1] = c2 - '0';
+      }
+    } else if ('0' <= c1 && c1 <= '9') {
+      output[0] = 0;
+      output[1] = c1 - '0';
+    } else {
+      offset--;
+      output[0] = c0 - '0';
+      output[1] = c0 - '0';
+    }
+    return offset - originalOffset;
+  }
+
+  private static void groupingToSkeleton(IGrouping value, StringBuilder sb) {
+    if (!(value instanceof Grouping)) {
+      // FIXME: Throw an exception here instead?
+      sb.append("custom");
+      return;
+    }
+    if (value.equals(Grouping.DEFAULT)) {
+      sb.append("DEFAULT");
+    } else if (value.equals(Grouping.DEFAULT_MIN_2_DIGITS)) {
+      sb.append("DEFAULT_MIN_2_DIGITS");
+    } else if (value.equals(Grouping.NONE)) {
+      sb.append("NONE");
+    } else {
+      GroupingImpl grouping = (GroupingImpl) value;
+      if (grouping.grouping2 == -1 || grouping.grouping2 == 0) {
+        sb.append("NONE");
+      } else {
+        sb.append(grouping.grouping1);
+        if (grouping.grouping2 != grouping.grouping1) {
+          sb.append(',');
+          sb.append(grouping.grouping2);
+        }
+        if (grouping.min2) {
+          sb.append('&');
+        }
+      }
+    }
+  }
+
+  private static int skeletonToGrouping(String skeleton, int offset, MacroProps output) {
+    int originalOffset = offset;
+    char c0 = skeleton.charAt(offset++);
+    Grouping result = null;
+    if ('0' <= c0 && c0 <= '9') {
+      char c1 = safeCharAt(skeleton, offset++);
+      if (c1 == ',') {
+        char c2 = safeCharAt(skeleton, offset++);
+        char c3 = safeCharAt(skeleton, offset++);
+        result = GroupingImpl.getInstance((short) (c0 - '0'), (short) (c2 - '0'), c3 == '&');
+      } else {
+        result = GroupingImpl.getInstance((short) (c0 - '0'), (short) (c0 - '0'), c1 == '&');
+      }
+    } else {
+      StringBuilder sb = new StringBuilder();
+      offset += consumeUntil(skeleton, --offset, ' ', sb);
+      String name = sb.toString();
+      if (name.equals("DEFAULT")) {
+        result = Grouping.DEFAULT;
+      } else if (name.equals("DEFAULT_MIN_2_DIGITS")) {
+        result = Grouping.DEFAULT_MIN_2_DIGITS;
+      } else if (name.equals("NONE")) {
+        result = Grouping.NONE;
+      }
+    }
+    output.grouping = result;
+    return offset - originalOffset;
+  }
+
+  private static void paddingToSkeleton(Padding value, StringBuilder sb) {
+    PaddingImpl padding = (PaddingImpl) value;
+    if (padding == Padding.NONE) {
+      sb.append("NONE");
+      return;
+    }
+    sb.append("CP:");
+    // TODO: Handle padding strings that contain ':'
+    sb.append(padding.paddingString);
+    sb.append(':');
+    sb.append(padding.targetWidth);
+    sb.append(':');
+    sb.append(padding.position.name());
+  }
+
+  private static void integerWidthToSkeleton(IntegerWidth value, StringBuilder sb) {
+    IntegerWidthImpl impl = (IntegerWidthImpl) value;
+    sb.append(impl.minInt);
+    if (impl.maxInt != impl.minInt) {
+      sb.append('-');
+      if (impl.maxInt < Integer.MAX_VALUE) {
+        sb.append(impl.maxInt);
+      }
+    }
+  }
+
+  private static void symbolsToSkeleton(Object value, StringBuilder sb) {
+    if (value instanceof DecimalFormatSymbols) {
+      // TODO: Check to see if any of the symbols are not default?
+      sb.append("loc:");
+      sb.append(((DecimalFormatSymbols) value).getULocale());
+    } else {
+      sb.append("ns:");
+      sb.append(((NumberingSystem) value).getName());
+    }
+  }
+
+  private static void unitWidthToSkeleton(FormatWidth value, StringBuilder sb) {
+    sb.append(value.name());
+  }
+
+  private static void signToSkeleton(SignDisplay value, StringBuilder sb) {
+    sb.append(value.name());
+  }
+
+  private static void decimalToSkeleton(DecimalMarkDisplay value, StringBuilder sb) {
+    sb.append(value.name());
+  }
+
+  private static char safeCharAt(String str, int offset) {
+    if (offset < str.length()) {
+      return str.charAt(offset);
+    } else {
+      return ' ';
+    }
+  }
+
+  private static int consumeUntil(String skeleton, int offset, char brk, StringBuilder sb) {
+    int originalOffset = offset;
+    char c = safeCharAt(skeleton, offset++);
+    while (c != brk) {
+      sb.append(c);
+      c = safeCharAt(skeleton, offset++);
+    }
+    return offset - originalOffset;
+  }
+}
diff --git a/icu4j/main/classes/core/src/newapi/impl/Worker1.java b/icu4j/main/classes/core/src/newapi/impl/Worker1.java
new file mode 100644 (file)
index 0000000..376f916
--- /dev/null
@@ -0,0 +1,263 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html#License
+package newapi.impl;
+
+import java.util.Map;
+
+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.Modifier;
+import com.ibm.icu.impl.number.NumberStringBuilder;
+import com.ibm.icu.impl.number.modifiers.ConstantAffixModifier;
+import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
+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.ULocale;
+
+import newapi.NumberFormatter.DecimalMarkDisplay;
+import newapi.NumberFormatter.Grouping;
+import newapi.NumberFormatter.NotationCompact;
+import newapi.NumberFormatter.NotationScientific;
+import newapi.NumberFormatter.Rounding;
+import newapi.NumberFormatter.SignDisplay;
+
+public class Worker1 {
+
+  public static Worker1 fromMacros(MacroProps macros) {
+    return new Worker1(make(macros, true));
+  }
+
+  public static MicroProps applyStatic(
+      MacroProps macros, FormatQuantity inValue, NumberStringBuilder outString) {
+    MicroProps micros = make(macros, false).withQuantity(inValue);
+    applyStatic(micros, inValue, outString);
+    return micros;
+  }
+
+  private static final Currency DEFAULT_CURRENCY = Currency.getInstance("XXX");
+
+  final QuantityChain microsGenerator;
+
+  private Worker1(QuantityChain microsGenerator) {
+    this.microsGenerator = microsGenerator;
+  }
+
+  public MicroProps apply(FormatQuantity inValue, NumberStringBuilder outString) {
+    MicroProps micros = microsGenerator.withQuantity(inValue);
+    applyStatic(micros, inValue, outString);
+    return micros;
+  }
+
+  //////////
+
+  private static QuantityChain make(MacroProps input, boolean build) {
+
+    String innerPattern = null;
+    Map<StandardPlural, Modifier> outerMods = null;
+    Rounding defaultRounding = Rounding.NONE;
+    Currency currency = DEFAULT_CURRENCY;
+    FormatWidth unitWidth = null;
+    boolean perMille = false;
+    PluralRules rules = input.rules;
+
+    MicroProps micros = new MicroProps();
+    QuantityChain 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.multiplier = 0;
+    micros.integerWidth =
+        input.integerWidth == null
+            ? IntegerWidthImpl.DEFAULT
+            : (IntegerWidthImpl) input.integerWidth;
+
+    if (input.unit == null || input.unit == Dimensionless.BASE) {
+      // No units; default format
+      innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.NUMBERSTYLE);
+    } else if (input.unit == Dimensionless.PERCENT) {
+      // Percent
+      innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.PERCENTSTYLE);
+      micros.multiplier += 2;
+    } else if (input.unit == Dimensionless.PERMILLE) {
+      // Permille
+      innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.PERCENTSTYLE);
+      micros.multiplier += 3;
+      perMille = true;
+    } else if (input.unit instanceof Currency && input.unitWidth != FormatWidth.WIDE) {
+      // Narrow, short, or ISO currency.
+      // TODO: Accounting style?
+      innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.CURRENCYSTYLE);
+      defaultRounding = Rounding.currency(CurrencyUsage.STANDARD);
+      currency = (Currency) input.unit;
+      micros.useCurrency = true;
+      unitWidth = (input.unitWidth == null) ? FormatWidth.NARROW : input.unitWidth;
+    } else if (input.unit instanceof Currency) {
+      // Currency long name
+      innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.NUMBERSTYLE);
+      outerMods = DataUtils.getCurrencyLongNameModifiers(input.loc, (Currency) input.unit);
+      defaultRounding = Rounding.currency(CurrencyUsage.STANDARD);
+      currency = (Currency) input.unit;
+      micros.useCurrency = true;
+      unitWidth = input.unitWidth = FormatWidth.WIDE;
+    } else {
+      // MeasureUnit
+      innerPattern = NumberFormat.getPatternForStyle(input.loc, NumberFormat.NUMBERSTYLE);
+      unitWidth = (input.unitWidth == null) ? FormatWidth.SHORT : input.unitWidth;
+      outerMods = DataUtils.getMeasureUnitModifiers(input.loc, input.unit, unitWidth);
+    }
+
+    // Parse the pattern, which is used for grouping and affixes only.
+    PatternParseResult patternInfo = LdmlPatternInfo.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) {
+      // TODO: Do this more efficiently. Will require modifying DecimalFormatSymbols.
+      NumberingSystem ns = (NumberingSystem) input.symbols;
+      ULocale temp = input.loc.setKeywordValue("numbers", ns.getName());
+      micros.symbols = DecimalFormatSymbols.getInstance(temp);
+    } else {
+      throw new AssertionError();
+    }
+
+    // TODO: Normalize the currency (accept symbols from DecimalFormatSymbols)?
+    // currency = CustomSymbolCurrency.resolve(currency, input.loc, micros.symbols);
+
+    // Multiplier (compatibility mode value).
+    // An int magnitude multiplier is used when not in compatibility mode to
+    // reduce object creations.
+    if (input.multiplier != null) {
+      // TODO: Make sure this is thread safe.
+      chain = input.multiplier.chain(chain);
+    }
+
+    // Rounding strategy
+    if (input.rounding != null) {
+      micros.rounding = RoundingImpl.normalizeType(input.rounding, currency);
+    } else if (input.notation instanceof NotationCompact) {
+      micros.rounding = RoundingImpl.RoundingImplFractionSignificant.COMPACT_STRATEGY;
+    } else {
+      micros.rounding = RoundingImpl.normalizeType(defaultRounding, currency);
+    }
+
+    // Grouping strategy
+    if (input.grouping != null) {
+      micros.grouping = GroupingImpl.normalizeType(input.grouping, patternInfo);
+    } else if (input.notation instanceof NotationCompact) {
+      // Compact notation uses minGrouping by default since ICU 59
+      micros.grouping = GroupingImpl.normalizeType(Grouping.DEFAULT_MIN_2_DIGITS, patternInfo);
+    } else {
+      micros.grouping = GroupingImpl.normalizeType(Grouping.DEFAULT, patternInfo);
+    }
+
+    // Inner modifier (scientific notation)
+    if (input.notation instanceof NotationScientific) {
+      assert input.notation instanceof NotationImpl.NotationScientificImpl;
+      chain =
+          ScientificImpl.getInstance(
+                  (NotationImpl.NotationScientificImpl) input.notation, micros.symbols, build)
+              .chain(chain);
+    } else {
+      // No inner modifier required
+      micros.modInner = ConstantAffixModifier.EMPTY;
+    }
+
+    // Middle modifier (patterns, positive/negative, currency symbols, percent)
+    MurkyModifier murkyMod = new MurkyModifier(false);
+    murkyMod.setPatternInfo((input.affixProvider != null) ? input.affixProvider : patternInfo);
+    murkyMod.setPatternAttributes(micros.sign, perMille);
+    if (murkyMod.needsPlurals()) {
+      if (rules == null) {
+        // Lazily create PluralRules
+        rules = PluralRules.forLocale(input.loc);
+      }
+      murkyMod.setSymbols(micros.symbols, currency, unitWidth, rules);
+    } else {
+      murkyMod.setSymbols(micros.symbols, currency, unitWidth, null);
+    }
+    if (build) {
+      chain = murkyMod.createImmutable().chain(chain);
+    } else {
+      chain = murkyMod.chain(chain);
+    }
+
+    // Outer modifier (CLDR units and currency long names)
+    if (outerMods != null) {
+      if (rules == null) {
+        // Lazily create PluralRules
+        rules = PluralRules.forLocale(input.loc);
+      }
+      chain = new QuantityDependentModOuter(outerMods, rules).chain(chain);
+    } else {
+      // No outer modifier required
+      micros.modOuter = ConstantAffixModifier.EMPTY;
+    }
+
+    // Padding strategy
+    if (input.padding != null) {
+      micros.padding = (PaddingImpl) input.padding;
+    } else {
+      micros.padding = PaddingImpl.NONE;
+    }
+
+    // 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 NotationCompact) {
+      assert input.notation instanceof NotationImpl.NotationCompactImpl;
+      if (rules == null) {
+        // Lazily create PluralRules
+        rules = PluralRules.forLocale(input.loc);
+      }
+      CompactStyle compactStyle = ((NotationImpl.NotationCompactImpl) input.notation).compactStyle;
+      CompactImpl worker;
+      if (compactStyle == null) {
+        // Use compact custom data
+        worker =
+            CompactImpl.getInstance(
+                ((NotationImpl.NotationCompactImpl) input.notation).compactCustomData, rules);
+      } else {
+        CompactType compactType =
+            (input.unit instanceof Currency) ? CompactType.CURRENCY : CompactType.DECIMAL;
+        worker = CompactImpl.getInstance(input.loc, compactType, compactStyle, rules);
+      }
+      if (build) {
+        worker.precomputeAllModifiers(murkyMod);
+      }
+      chain = worker.chain(chain);
+    }
+
+    if (build) {
+      micros.enableCloneInChain();
+    }
+
+    return chain;
+  }
+
+  //////////
+
+  private static int applyStatic(
+      MicroProps micros, FormatQuantity inValue, NumberStringBuilder outString) {
+    inValue.adjustMagnitude(micros.multiplier);
+    micros.rounding.apply(inValue);
+    inValue.setIntegerLength(micros.integerWidth.minInt, micros.integerWidth.maxInt);
+    int length = PositiveDecimalImpl.apply(micros, inValue, outString);
+    // 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;
+  }
+}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/MurkyModifierTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/MurkyModifierTest.java
new file mode 100644 (file)
index 0000000..c4d9d5c
--- /dev/null
@@ -0,0 +1,58 @@
+// © 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 org.junit.Test;
+
+import com.ibm.icu.impl.number.LdmlPatternInfo;
+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.NumberFormatter.SignDisplay;
+import newapi.impl.MurkyModifier;
+
+public class MurkyModifierTest {
+
+  @Test
+  public void basic() {
+    MurkyModifier murky = new MurkyModifier(false);
+    murky.setPatternInfo(LdmlPatternInfo.parse("a0b"));
+    murky.setPatternAttributes(SignDisplay.AUTO, false);
+    murky.setSymbols(
+        DecimalFormatSymbols.getInstance(ULocale.ENGLISH),
+        Currency.getInstance("USD"),
+        FormatWidth.SHORT,
+        null);
+    murky.setNumberProperties(false, null);
+    assertEquals("a", murky.getPrefix());
+    assertEquals("b", murky.getSuffix());
+    murky.setPatternAttributes(SignDisplay.ALWAYS_SHOWN, false);
+    assertEquals("+a", murky.getPrefix());
+    assertEquals("b", murky.getSuffix());
+    murky.setNumberProperties(true, null);
+    assertEquals("-a", murky.getPrefix());
+    assertEquals("b", murky.getSuffix());
+    murky.setPatternAttributes(SignDisplay.NEVER_SHOWN, false);
+    assertEquals("a", murky.getPrefix());
+    assertEquals("b", murky.getSuffix());
+
+    murky.setPatternInfo(LdmlPatternInfo.parse("a0b;c-0d"));
+    murky.setPatternAttributes(SignDisplay.AUTO, false);
+    murky.setNumberProperties(false, null);
+    assertEquals("a", murky.getPrefix());
+    assertEquals("b", murky.getSuffix());
+    murky.setPatternAttributes(SignDisplay.ALWAYS_SHOWN, false);
+    assertEquals("c+", murky.getPrefix());
+    assertEquals("d", murky.getSuffix());
+    murky.setNumberProperties(true, null);
+    assertEquals("c-", murky.getPrefix());
+    assertEquals("d", murky.getSuffix());
+    murky.setPatternAttributes(SignDisplay.NEVER_SHOWN, false);
+    assertEquals("c-", murky.getPrefix()); // TODO: What should this behavior be?
+    assertEquals("d", murky.getSuffix());
+  }
+}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/NumberFormatterTest.java
new file mode 100644 (file)
index 0000000..697be1c
--- /dev/null
@@ -0,0 +1,1248 @@
+// © 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.assertNotEquals;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Locale;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import com.ibm.icu.impl.number.formatters.PaddingFormat.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.ULocale;
+
+import newapi.NumberFormatter;
+import newapi.NumberFormatter.DecimalMarkDisplay;
+import newapi.NumberFormatter.Grouping;
+import newapi.NumberFormatter.IRounding;
+import newapi.NumberFormatter.IntegerWidth;
+import newapi.NumberFormatter.LocalizedNumberFormatter;
+import newapi.NumberFormatter.Notation;
+import newapi.NumberFormatter.NumberFormatterResult;
+import newapi.NumberFormatter.Padding;
+import newapi.NumberFormatter.Rounding;
+import newapi.NumberFormatter.SignDisplay;
+import newapi.NumberFormatter.UnlocalizedNumberFormatter;
+import newapi.impl.NumberFormatterImpl;
+
+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_SHOWN)),
+        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.COMPACT_SHORT),
+        ULocale.ENGLISH,
+        "88K",
+        "8.8K",
+        "876",
+        "88",
+        "8.8",
+        "0.88",
+        "0.088",
+        "0.0088",
+        "0");
+
+    assertFormatDescending(
+        "Compact Long",
+        "CC",
+        NumberFormatter.with().notation(Notation.COMPACT_LONG),
+        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.COMPACT_SHORT).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.COMPACT_LONG).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.COMPACT_LONG),
+        ULocale.forLanguageTag("es"),
+        1000000,
+        "1 millón");
+
+    assertFormatSingle(
+        "Compact Plural Other",
+        "CC",
+        NumberFormatter.with().notation(Notation.COMPACT_LONG),
+        ULocale.forLanguageTag("es"),
+        2000000,
+        "2 millones");
+
+    assertFormatSingle(
+        "Compact with Negative Sign",
+        "C",
+        NumberFormatter.with().notation(Notation.COMPACT_SHORT),
+        ULocale.ENGLISH,
+        -9876543.21,
+        "-9.9M");
+  }
+
+  @Test
+  public void unitMeasure() {
+    assertFormatDescending(
+        "Meters Short",
+        "Ulength: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",
+        "Ulength: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 Ulength:meter unit-width=WIDE",
+        NumberFormatter.with()
+            .notation(Notation.COMPACT_LONG)
+            .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",
+        "Ulength: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",
+        NumberFormatterImpl.fromPattern("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(Rounding.INTEGER),
+        ULocale.ENGLISH,
+        "87,650",
+        "8,765",
+        "876",
+        "88",
+        "9",
+        "1",
+        "0",
+        "0",
+        "0");
+
+    assertFormatDescending(
+        "Fixed Fraction",
+        "F3",
+        NumberFormatter.with().rounding(Rounding.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(Rounding.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(Rounding.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(Rounding.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(Rounding.fixedFigures(3)),
+        ULocale.ENGLISH,
+        -98,
+        "-98.0");
+
+    assertFormatSingle(
+        "Fixed Significant Rounding",
+        "S3",
+        NumberFormatter.with().rounding(Rounding.fixedFigures(3)),
+        ULocale.ENGLISH,
+        -98.7654321,
+        "-98.8");
+
+    assertFormatSingle(
+        "Fixed Significant Zero",
+        "S3",
+        NumberFormatter.with().rounding(Rounding.fixedFigures(3)),
+        ULocale.ENGLISH,
+        0,
+        "0.00");
+
+    assertFormatSingle(
+        "Min Significant",
+        "S2-",
+        NumberFormatter.with().rounding(Rounding.minFigures(2)),
+        ULocale.ENGLISH,
+        -9,
+        "-9.0");
+
+    assertFormatSingle(
+        "Max Significant",
+        "S-4",
+        NumberFormatter.with().rounding(Rounding.maxFigures(4)),
+        ULocale.ENGLISH,
+        98.7654321,
+        "98.77");
+
+    assertFormatSingle(
+        "Min/Max Significant",
+        "S3-4",
+        NumberFormatter.with().rounding(Rounding.minMaxFigures(3, 4)),
+        ULocale.ENGLISH,
+        9.99999,
+        "10.0");
+  }
+
+  @Test
+  public void roundingFractionFigures() {
+    assertFormatDescending(
+        "Basic Significant", // for comparison
+        "S-2",
+        NumberFormatter.with().rounding(Rounding.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(Rounding.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(Rounding.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(Rounding.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(Rounding.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(Rounding.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(Rounding.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(Rounding.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(Rounding.currency(CurrencyUsage.CASH).withCurrency(CZK)),
+        ULocale.ENGLISH,
+        "87,650",
+        "8,765",
+        "876",
+        "88",
+        "9",
+        "1",
+        "0",
+        "0",
+        "0");
+
+    assertFormatDescending(
+        "Lambda Function",
+        "",
+        NumberFormatter.with()
+            .rounding(
+                new IRounding() {
+                  @Override
+                  public BigDecimal round(BigDecimal input) {
+                    return input.setScale(2, RoundingMode.FLOOR);
+                  }
+                }),
+        ULocale.ENGLISH,
+        "87,650.00",
+        "8,765.00",
+        "876.50",
+        "87.65",
+        "8.76",
+        "0.87",
+        "0.08",
+        "0.00",
+        "0.00");
+  }
+
+  @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=DEFAULT",
+        NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouping.DEFAULT),
+        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=DEFAULT_MIN_2_DIGITS",
+        NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouping.DEFAULT_MIN_2_DIGITS),
+        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=DEFAULT_MIN_2_DIGITS",
+        NumberFormatter.with().unit(Dimensionless.PERMILLE).grouping(Grouping.DEFAULT_MIN_2_DIGITS),
+        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(Grouping.NONE),
+        new ULocale("en-IN"),
+        "87650000‰",
+        "8765000‰",
+        "876500‰",
+        "87650‰",
+        "8765‰",
+        "876.5‰",
+        "87.65‰",
+        "8.765‰",
+        "0‰");
+  }
+
+  @Test
+  public void padding() {
+    assertFormatDescending(
+        "Padding",
+        "padding=NONE",
+        NumberFormatter.with().padding(Padding.NONE),
+        ULocale.ENGLISH,
+        "87,650",
+        "8,765",
+        "876.5",
+        "87.65",
+        "8.765",
+        "0.8765",
+        "0.08765",
+        "0.008765",
+        "0");
+
+    assertFormatDescending(
+        "Padding",
+        "padding=CP:*:8:AFTER_PREFIX",
+        NumberFormatter.with().padding(Padding.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",
+        "padding=CP:𐇤:8:AFTER_PREFIX",
+        NumberFormatter.with().padding(Padding.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",
+        "padding=CP:*:8:AFTER_PREFIX symbols=ns:mathsanb",
+        NumberFormatter.with()
+            .padding(Padding.codePoints('*', 8, PadPosition.AFTER_PREFIX))
+            .symbols(NumberingSystem.getInstanceByName("mathsanb")),
+        ULocale.ENGLISH,
+        "**𝟴𝟳,𝟲𝟱𝟬",
+        "***𝟴,𝟳𝟲𝟱",
+        "***𝟴𝟳𝟲.𝟱",
+        "***𝟴𝟳.𝟲𝟱",
+        "***𝟴.𝟳𝟲𝟱",
+        "**𝟬.𝟴𝟳𝟲𝟱",
+        "*𝟬.𝟬𝟴𝟳𝟲𝟱",
+        "𝟬.𝟬𝟬𝟴𝟳𝟲𝟱",
+        "*******𝟬");
+
+    assertFormatDescending(
+        "Padding with currency spacing",
+        "$GBP padding=CP:*:10:AFTER_PREFIX unit-width=SHORT",
+        NumberFormatter.with()
+            .padding(Padding.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",
+        "padding=CP:*:8:BEFORE_PREFIX",
+        NumberFormatter.with().padding(Padding.codePoints('*', 8, PadPosition.BEFORE_PREFIX)),
+        ULocale.ENGLISH,
+        -88.88,
+        "**-88.88");
+
+    assertFormatSingle(
+        "Pad After Prefix",
+        "padding=CP:*:8:AFTER_PREFIX",
+        NumberFormatter.with().padding(Padding.codePoints('*', 8, PadPosition.AFTER_PREFIX)),
+        ULocale.ENGLISH,
+        -88.88,
+        "-**88.88");
+
+    assertFormatSingle(
+        "Pad Before Suffix",
+        "% padding=CP:*:8:BEFORE_SUFFIX",
+        NumberFormatter.with()
+            .padding(Padding.codePoints('*', 8, PadPosition.BEFORE_SUFFIX))
+            .unit(Dimensionless.PERCENT),
+        ULocale.ENGLISH,
+        0.8888,
+        "88.88**%");
+
+    assertFormatSingle(
+        "Pad After Suffix",
+        "% padding=CP:*:8:AFTER_SUFFIX",
+        NumberFormatter.with()
+            .padding(Padding.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.DEFAULT),
+        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.DEFAULT.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.COMPACT_SHORT)
+            .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_SHOWN",
+        NumberFormatter.with().sign(SignDisplay.ALWAYS_SHOWN),
+        ULocale.ENGLISH,
+        444444,
+        "+444,444");
+
+    assertFormatSingle(
+        "Sign Always Negative",
+        "sign=ALWAYS_SHOWN",
+        NumberFormatter.with().sign(SignDisplay.ALWAYS_SHOWN),
+        ULocale.ENGLISH,
+        -444444,
+        "-444,444");
+
+    assertFormatSingle(
+        "Sign Never Positive",
+        "sign=NEVER_SHOWN",
+        NumberFormatter.with().sign(SignDisplay.NEVER_SHOWN),
+        ULocale.ENGLISH,
+        444444,
+        "444,444");
+
+    assertFormatSingle(
+        "Sign Never Negative",
+        "sign=NEVER_SHOWN",
+        NumberFormatter.with().sign(SignDisplay.NEVER_SHOWN),
+        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_SHOWN",
+        NumberFormatter.with().decimal(DecimalMarkDisplay.ALWAYS_SHOWN),
+        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];
+      NumberFormatterResult positive = f.format(1);
+      NumberFormatterResult 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=WIDE",
+        NumberFormatter.with()
+            .unit(USD)
+            .unitWidth(FormatWidth.WIDE)
+            .rounding(Rounding.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(Rounding.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};
+    NumberFormatterImpl l1 = (NumberFormatterImpl) f.locale(locale); // no self-regulation
+    NumberFormatterImpl l2 = (NumberFormatterImpl) f.locale(locale); // all self-regulation
+    for (int i = 0; i < 9; i++) {
+      double d = inputs[i];
+      String actual1 = l1.formatWithThreshold(d, 0).toString();
+      assertEquals(message + ": L1: " + d, expected[i], actual1);
+      String actual2 = l2.formatWithThreshold(d, 1).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());
+    NumberFormatterImpl l1 = (NumberFormatterImpl) f.locale(locale); // no self-regulation
+    NumberFormatterImpl l2 = (NumberFormatterImpl) f.locale(locale); // all self-regulation
+    String actual1 = l1.formatWithThreshold(input, 0).toString();
+    assertEquals(message + ": L1: " + input, expected, actual1);
+    String actual2 = l2.formatWithThreshold(input, 1).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());
+    NumberFormatterImpl l1 = (NumberFormatterImpl) f.locale(locale); // no self-regulation
+    NumberFormatterImpl l2 = (NumberFormatterImpl) f.locale(locale); // all self-regulation
+    String actual1 = l1.formatWithThreshold(input, 0).toString();
+    assertEquals(message + ": L1: " + input, expected, actual1);
+    String actual2 = l2.formatWithThreshold(input, 1).toString();
+    assertEquals(message + ": L2: " + input, expected, actual2);
+  }
+}