]> granicus.if.org Git - icu/commitdiff
ICU-13177 Assorted internal data type changes. Should not change behavior.
authorShane Carr <shane@unicode.org>
Thu, 20 Jul 2017 04:06:29 +0000 (04:06 +0000)
committerShane Carr <shane@unicode.org>
Thu, 20 Jul 2017 04:06:29 +0000 (04:06 +0000)
X-SVN-Rev: 40272

icu4j/main/classes/core/src/com/ibm/icu/impl/number/AffixPatternUtils.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantity1.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/FormatQuantityBCD.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/PatternString.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/Rounder.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/RoundingUtils.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/formatters/ScientificFormat.java
icu4j/main/classes/core/src/com/ibm/icu/impl/number/rounders/SignificantDigitsRounder.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/AffixPatternUtilsTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/number/FormatQuantityTest.java

index 6019985b03bfcb0f4d595b88def00e2804f0e5e4..f1357836ba5d27ce91222efcacd7a78c450341d3 100644 (file)
@@ -35,21 +35,7 @@ import com.ibm.icu.text.NumberFormat.Field;
  *     case AffixPatternUtils.TYPE_PERCENT:
  *       // Current token is a percent sign.
  *       break;
- *     case AffixPatternUtils.TYPE_PERMILLE:
- *       // Current token is a permille sign.
- *       break;
- *     case AffixPatternUtils.TYPE_CURRENCY_SINGLE:
- *       // Current token is a single currency sign.
- *       break;
- *     case AffixPatternUtils.TYPE_CURRENCY_DOUBLE:
- *       // Current token is a double currency sign.
- *       break;
- *     case AffixPatternUtils.TYPE_CURRENCY_TRIPLE:
- *       // Current token is a triple currency sign.
- *       break;
- *     case AffixPatternUtils.TYPE_CURRENCY_OVERFLOW:
- *       // Current token has four or more currency signs.
- *       break;
+ *     // ... other types ...
  *     default:
  *       // Current token is an arbitrary code point.
  *       // The variable typeOrCp is the code point.
@@ -67,8 +53,11 @@ public class AffixPatternUtils {
   private static final int STATE_FIRST_CURR = 4;
   private static final int STATE_SECOND_CURR = 5;
   private static final int STATE_THIRD_CURR = 6;
-  private static final int STATE_OVERFLOW_CURR = 7;
+  private static final int STATE_FOURTH_CURR = 7;
+  private static final int STATE_FIFTH_CURR = 8;
+  private static final int STATE_OVERFLOW_CURR = 9;
 
+  /** Represents a literal character; the value is stored in the code point field. */
   private static final int TYPE_CODEPOINT = 0;
 
   /** Represents a minus sign symbol '-'. */
@@ -92,7 +81,13 @@ public class AffixPatternUtils {
   /** Represents a triple currency symbol '¤¤¤'. */
   public static final int TYPE_CURRENCY_TRIPLE = -7;
 
-  /** Represents a sequence of four or more currency symbols. */
+  /** Represents a quadruple currency symbol '¤¤¤¤'. */
+  public static final int TYPE_CURRENCY_QUAD = -8;
+
+  /** Represents a quintuple currency symbol '¤¤¤¤¤'. */
+  public static final int TYPE_CURRENCY_QUINT = -9;
+
+  /** Represents a sequence of six or more currency symbols. */
   public static final int TYPE_CURRENCY_OVERFLOW = -15;
 
   /**
@@ -278,8 +273,15 @@ public class AffixPatternUtils {
         case TYPE_CURRENCY_TRIPLE:
           output.append(currency3, Field.CURRENCY);
           break;
+        case TYPE_CURRENCY_QUAD:
+          output.appendCodePoint('\uFFFD', Field.CURRENCY);
+          break;
+        case TYPE_CURRENCY_QUINT:
+          // TODO: Add support for narrow currency symbols here.
+          output.appendCodePoint('\uFFFD', Field.CURRENCY);
+          break;
         case TYPE_CURRENCY_OVERFLOW:
-          output.append("\uFFFD", Field.CURRENCY);
+          output.appendCodePoint('\uFFFD', Field.CURRENCY);
           break;
         default:
           output.appendCodePoint(typeOrCp, null);
@@ -288,6 +290,61 @@ public class AffixPatternUtils {
     }
   }
 
+  private static final Field getFieldForType(int type) {
+    switch (type) {
+      case TYPE_MINUS_SIGN:
+        return Field.SIGN;
+      case TYPE_PLUS_SIGN:
+        return Field.SIGN;
+      case TYPE_PERCENT:
+        return Field.PERCENT;
+      case TYPE_PERMILLE:
+        return Field.PERMILLE;
+      case TYPE_CURRENCY_SINGLE:
+        return Field.CURRENCY;
+      case TYPE_CURRENCY_DOUBLE:
+        return Field.CURRENCY;
+      case TYPE_CURRENCY_TRIPLE:
+        return Field.CURRENCY;
+      case TYPE_CURRENCY_QUAD:
+        return Field.CURRENCY;
+      case TYPE_CURRENCY_QUINT:
+        return Field.CURRENCY;
+      case TYPE_CURRENCY_OVERFLOW:
+        return Field.CURRENCY;
+      default:
+        throw new AssertionError();
+    }
+  }
+
+  public static interface SymbolProvider {
+    public CharSequence getSymbol(int type);
+  }
+
+  public static int unescape(
+      CharSequence affixPattern,
+      NumberStringBuilder output,
+      int position,
+      SymbolProvider provider) {
+    // TODO: Is it worth removing this extra local object instantiation here?
+    NumberStringBuilder local = new NumberStringBuilder(10);
+    assert affixPattern != null;
+    long tag = 0L;
+    while (hasNext(tag, affixPattern)) {
+      tag = nextToken(tag, affixPattern);
+      int typeOrCp = getTypeOrCp(tag);
+      if (typeOrCp == TYPE_CURRENCY_OVERFLOW) {
+        // Don't go to the provider for this special case
+        local.appendCodePoint(0xFFFD, Field.CURRENCY);
+      } else if (typeOrCp < 0) {
+        local.append(provider.getSymbol(typeOrCp), getFieldForType(typeOrCp));
+      } else {
+        local.appendCodePoint(typeOrCp, null);
+      }
+    }
+    return output.insert(position, local);
+  }
+
   /**
    * Checks whether the given affix pattern contains at least one token of the given type, which is
    * one of the constants "TYPE_" in {@link AffixPatternUtils}.
@@ -323,6 +380,8 @@ public class AffixPatternUtils {
       if (typeOrCp == AffixPatternUtils.TYPE_CURRENCY_SINGLE
           || typeOrCp == AffixPatternUtils.TYPE_CURRENCY_DOUBLE
           || typeOrCp == AffixPatternUtils.TYPE_CURRENCY_TRIPLE
+          || typeOrCp == AffixPatternUtils.TYPE_CURRENCY_QUAD
+          || typeOrCp == AffixPatternUtils.TYPE_CURRENCY_QUINT
           || typeOrCp == AffixPatternUtils.TYPE_CURRENCY_OVERFLOW) {
         return true;
       }
@@ -437,13 +496,31 @@ public class AffixPatternUtils {
           }
         case STATE_THIRD_CURR:
           if (cp == '¤') {
-            state = STATE_OVERFLOW_CURR;
+            state = STATE_FOURTH_CURR;
             offset += count;
             // continue to the next code point
             break;
           } else {
             return makeTag(offset, TYPE_CURRENCY_TRIPLE, STATE_BASE, 0);
           }
+        case STATE_FOURTH_CURR:
+          if (cp == '¤') {
+            state = STATE_FIFTH_CURR;
+            offset += count;
+            // continue to the next code point
+            break;
+          } else {
+            return makeTag(offset, TYPE_CURRENCY_QUAD, STATE_BASE, 0);
+          }
+        case STATE_FIFTH_CURR:
+          if (cp == '¤') {
+            state = STATE_OVERFLOW_CURR;
+            offset += count;
+            // continue to the next code point
+            break;
+          } else {
+            return makeTag(offset, TYPE_CURRENCY_QUINT, STATE_BASE, 0);
+          }
         case STATE_OVERFLOW_CURR:
           if (cp == '¤') {
             offset += count;
@@ -475,6 +552,10 @@ public class AffixPatternUtils {
         return makeTag(offset, TYPE_CURRENCY_DOUBLE, STATE_BASE, 0);
       case STATE_THIRD_CURR:
         return makeTag(offset, TYPE_CURRENCY_TRIPLE, STATE_BASE, 0);
+      case STATE_FOURTH_CURR:
+        return makeTag(offset, TYPE_CURRENCY_QUAD, STATE_BASE, 0);
+      case STATE_FIFTH_CURR:
+        return makeTag(offset, TYPE_CURRENCY_QUINT, STATE_BASE, 0);
       case STATE_OVERFLOW_CURR:
         return makeTag(offset, TYPE_CURRENCY_OVERFLOW, STATE_BASE, 0);
       default:
@@ -522,6 +603,17 @@ public class AffixPatternUtils {
     return (type == 0) ? getCodePoint(tag) : -type;
   }
 
+  /**
+   * Encodes the given values into a 64-bit tag.
+   *
+   * <ul>
+   *   <li>Bits 0-31 => offset (int32)
+   *   <li>Bits 32-35 => type (uint4)
+   *   <li>Bits 36-39 => state (uint4)
+   *   <li>Bits 40-60 => code point (uint21)
+   *   <li>Bits 61-63 => unused
+   * </ul>
+   */
   private static long makeTag(int offset, int type, int state, int cp) {
     long tag = 0L;
     tag |= offset;
index 7b78fe5d9f87495fb9826cedce67f5d323a8a1f3..aa80a422948796427f20c2c99743f00bad45f42d 100644 (file)
@@ -21,17 +21,23 @@ import com.ibm.icu.text.PluralRules;
  * to be copied to every implementation?
  */
 public interface FormatQuantity extends PluralRules.IFixedDecimal {
-
   /**
-   * Sets the minimum and maximum digits that this {@link FormatQuantity} should generate. This
-   * method does not perform rounding.
+   * Sets the minimum and maximum integer digits that this {@link FormatQuantity} should generate.
+   * This method does not perform rounding.
    *
    * @param minInt The minimum number of integer digits.
    * @param maxInt The maximum number of integer digits.
+   */
+  public void setIntegerLength(int minInt, int maxInt);
+
+  /**
+   * Sets the minimum and maximum fraction digits that this {@link FormatQuantity} should generate.
+   * This method does not perform rounding.
+   *
    * @param minFrac The minimum number of fraction digits.
    * @param maxFrac The maximum number of fraction digits.
    */
-  public void setIntegerFractionLength(int minInt, int maxInt, int minFrac, int maxFrac);
+  public void setFractionLength(int minFrac, int maxFrac);
 
   /**
    * Rounds the number to a specified interval, such as 0.05.
@@ -100,6 +106,8 @@ public interface FormatQuantity extends PluralRules.IFixedDecimal {
 
   public BigDecimal toBigDecimal();
 
+  public void setToBigDecimal(BigDecimal input);
+
   public int maxRepresentableDigits();
 
   // TODO: Should this method be removed, since FormatQuantity implements IFixedDecimal now?
@@ -176,8 +184,6 @@ public interface FormatQuantity extends PluralRules.IFixedDecimal {
 
   public void copyFrom(FormatQuantity other);
 
-  /**
-   * This method is for internal testing only.
-   */
+  /** This method is for internal testing only. */
   public long getPositionFingerprint();
 }
index b7fe968a075ca0e16fadd442a7faa6fc7dc1b6cb..d6820b202cd2819b6c41662368cf2e0d5eb58bef 100644 (file)
@@ -229,6 +229,15 @@ public class FormatQuantity1 implements FormatQuantity {
   }
 
   public FormatQuantity1(BigDecimal decimal) {
+    setToBigDecimal(decimal);
+  }
+
+  public FormatQuantity1(FormatQuantity1 other) {
+    copyFrom(other);
+  }
+
+  @Override
+  public void setToBigDecimal(BigDecimal decimal) {
     if (decimal.compareTo(BigDecimal.ZERO) < 0) {
       setNegative(true);
       decimal = decimal.negate();
@@ -242,10 +251,6 @@ public class FormatQuantity1 implements FormatQuantity {
     }
   }
 
-  public FormatQuantity1(FormatQuantity1 other) {
-    copyFrom(other);
-  }
-
   @Override
   public FormatQuantity1 createCopy() {
     return new FormatQuantity1(this);
@@ -311,31 +316,35 @@ public class FormatQuantity1 implements FormatQuantity {
   }
 
   @Override
-  public void setIntegerFractionLength(int minInt, int maxInt, int minFrac, int maxFrac) {
+  public void setIntegerLength(int minInt, int maxInt) {
     // Graceful failures for bogus input
     minInt = Math.max(0, minInt);
     maxInt = Math.max(0, maxInt);
-    minFrac = Math.max(0, minFrac);
-    maxFrac = Math.max(0, maxFrac);
 
     // The minima must be less than or equal to the maxima
     if (maxInt < minInt) {
       minInt = maxInt;
     }
+
+    // Save values into internal state
+    // Negation is safe for minFrac/maxFrac because -Integer.MAX_VALUE > Integer.MIN_VALUE
+    lOptPos = maxInt;
+    lReqPos = minInt;
+  }
+
+  @Override
+  public void setFractionLength(int minFrac, int maxFrac) {
+    // Graceful failures for bogus input
+    minFrac = Math.max(0, minFrac);
+    maxFrac = Math.max(0, maxFrac);
+
+    // The minima must be less than or equal to the maxima
     if (maxFrac < minFrac) {
       minFrac = maxFrac;
     }
 
-    // Displaying neither integer nor fraction digits is not allowed
-    if (maxInt == 0 && maxFrac == 0) {
-      maxInt = Integer.MAX_VALUE;
-      maxFrac = Integer.MAX_VALUE;
-    }
-
     // Save values into internal state
     // Negation is safe for minFrac/maxFrac because -Integer.MAX_VALUE > Integer.MIN_VALUE
-    lOptPos = maxInt;
-    lReqPos = minInt;
     rReqPos = -minFrac;
     rOptPos = -maxFrac;
   }
index bb9c75206493a00dda2a7e7b1d69b0847b79caba..c82775d9a485aad4ce1956430330026180ca12e4 100644 (file)
@@ -134,17 +134,25 @@ public abstract class FormatQuantityBCD implements FormatQuantity {
   }
 
   @Override
-  public void setIntegerFractionLength(int minInt, int maxInt, int minFrac, int maxFrac) {
+  public void setIntegerLength(int minInt, int maxInt) {
     // Validation should happen outside of FormatQuantity, e.g., in the Rounder class.
     assert minInt >= 0;
     assert maxInt >= minInt;
-    assert minFrac >= 0;
-    assert maxFrac >= minFrac;
 
     // Save values into internal state
     // Negation is safe for minFrac/maxFrac because -Integer.MAX_VALUE > Integer.MIN_VALUE
     lOptPos = maxInt;
     lReqPos = minInt;
+  }
+
+  @Override
+  public void setFractionLength(int minFrac, int maxFrac) {
+    // Validation should happen outside of FormatQuantity, e.g., in the Rounder class.
+    assert minFrac >= 0;
+    assert maxFrac >= minFrac;
+
+    // Save values into internal state
+    // Negation is safe for minFrac/maxFrac because -Integer.MAX_VALUE > Integer.MIN_VALUE
     rReqPos = -minFrac;
     rOptPos = -maxFrac;
   }
@@ -510,7 +518,8 @@ public abstract class FormatQuantityBCD implements FormatQuantity {
    *
    * @param n The value to consume.
    */
-  public void setToBigDecimal(BigDecimal n) {
+  @Override
+public void setToBigDecimal(BigDecimal n) {
     setBcdToZero();
     flags = 0;
     if (n.signum() == -1) {
index 18cdafa51cf0ccaef1b6d6200910bbc9b5ad2bef..375dbb279575a65450071a64265b1fef66a3f7c1 100644 (file)
@@ -26,7 +26,7 @@ public class PatternString {
    */
   public static Properties parseToProperties(String pattern, int ignoreRounding) {
     Properties properties = new Properties();
-    LdmlDecimalPatternParser.parse(pattern, properties, ignoreRounding);
+    parse(pattern, properties, ignoreRounding);
     return properties;
   }
 
@@ -50,7 +50,7 @@ public class PatternString {
    */
   public static void parseToExistingProperties(
       String pattern, Properties properties, int ignoreRounding) {
-    LdmlDecimalPatternParser.parse(pattern, properties, ignoreRounding);
+    parse(pattern, properties, ignoreRounding);
   }
 
   public static void parseToExistingProperties(String pattern, Properties properties) {
@@ -431,487 +431,183 @@ public class PatternString {
   public static final int IGNORE_ROUNDING_IF_CURRENCY = 1;
   public static final int IGNORE_ROUNDING_ALWAYS = 2;
 
-  /** Implements a recursive descent parser for decimal format patterns. */
-  static class LdmlDecimalPatternParser {
-
-    /**
-     * An internal, intermediate data structure used for storing parse results before they are
-     * finalized into a DecimalFormatPattern.Builder.
-     */
-    private static class PatternParseResult {
-      SubpatternParseResult positive = new SubpatternParseResult();
-      SubpatternParseResult negative = null;
-
-      /** Finalizes the temporary data stored in the PatternParseResult to the Builder. */
-      void saveToProperties(Properties properties, int _ignoreRounding) {
-        // Translate from PatternState to Properties.
-        // Note that most data from "negative" is ignored per the specification of DecimalFormat.
-
-        boolean ignoreRounding;
-        if (_ignoreRounding == IGNORE_ROUNDING_NEVER) {
-          ignoreRounding = false;
-        } else if (_ignoreRounding == IGNORE_ROUNDING_IF_CURRENCY) {
-          ignoreRounding = positive.hasCurrencySign;
-        } else {
-          assert _ignoreRounding == IGNORE_ROUNDING_ALWAYS;
-          ignoreRounding = true;
-        }
-
-        // Grouping settings
-        if (positive.groupingSizes[1] != -1) {
-          properties.setGroupingSize(positive.groupingSizes[0]);
-        } else {
-          properties.setGroupingSize(Properties.DEFAULT_GROUPING_SIZE);
-        }
-        if (positive.groupingSizes[2] != -1) {
-          properties.setSecondaryGroupingSize(positive.groupingSizes[1]);
-        } else {
-          properties.setSecondaryGroupingSize(Properties.DEFAULT_SECONDARY_GROUPING_SIZE);
-        }
-
-        // For backwards compatibility, require that the pattern emit at least one min digit.
-        int minInt, minFrac;
-        if (positive.totalIntegerDigits == 0 && positive.maximumFractionDigits > 0) {
-          // patterns like ".##"
-          minInt = 0;
-          minFrac = Math.max(1, positive.minimumFractionDigits);
-        } else if (positive.minimumIntegerDigits == 0 && positive.minimumFractionDigits == 0) {
-          // patterns like "#.##"
-          minInt = 1;
-          minFrac = 0;
-        } else {
-          minInt = positive.minimumIntegerDigits;
-          minFrac = positive.minimumFractionDigits;
-        }
-
-        // Rounding settings
-        // Don't set basic rounding when there is a currency sign; defer to CurrencyUsage
-        if (positive.minimumSignificantDigits > 0) {
-          properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS);
-          properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS);
-          properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
-          properties.setMinimumSignificantDigits(positive.minimumSignificantDigits);
-          properties.setMaximumSignificantDigits(positive.maximumSignificantDigits);
-        } else if (!positive.rounding.isZero()) {
-          if (!ignoreRounding) {
-            properties.setMinimumFractionDigits(minFrac);
-            properties.setMaximumFractionDigits(positive.maximumFractionDigits);
-            properties.setRoundingIncrement(positive.rounding.toBigDecimal());
-          } else {
-            properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS);
-            properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS);
-            properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
-          }
-          properties.setMinimumSignificantDigits(Properties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS);
-          properties.setMaximumSignificantDigits(Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS);
-        } else {
-          if (!ignoreRounding) {
-            properties.setMinimumFractionDigits(minFrac);
-            properties.setMaximumFractionDigits(positive.maximumFractionDigits);
-            properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
-          } else {
-            properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS);
-            properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS);
-            properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
-          }
-          properties.setMinimumSignificantDigits(Properties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS);
-          properties.setMaximumSignificantDigits(Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS);
-        }
-
-        // If the pattern ends with a '.' then force the decimal point.
-        if (positive.hasDecimal && positive.maximumFractionDigits == 0) {
-          properties.setDecimalSeparatorAlwaysShown(true);
-        } else {
-          properties.setDecimalSeparatorAlwaysShown(false);
-        }
-
-        // Scientific notation settings
-        if (positive.exponentDigits > 0) {
-          properties.setExponentSignAlwaysShown(positive.exponentShowPlusSign);
-          properties.setMinimumExponentDigits(positive.exponentDigits);
-          if (positive.minimumSignificantDigits == 0) {
-            // patterns without '@' can define max integer digits, used for engineering notation
-            properties.setMinimumIntegerDigits(positive.minimumIntegerDigits);
-            properties.setMaximumIntegerDigits(positive.totalIntegerDigits);
-          } else {
-            // patterns with '@' cannot define max integer digits
-            properties.setMinimumIntegerDigits(1);
-            properties.setMaximumIntegerDigits(Properties.DEFAULT_MAXIMUM_INTEGER_DIGITS);
-          }
-        } else {
-          properties.setExponentSignAlwaysShown(Properties.DEFAULT_EXPONENT_SIGN_ALWAYS_SHOWN);
-          properties.setMinimumExponentDigits(Properties.DEFAULT_MINIMUM_EXPONENT_DIGITS);
-          properties.setMinimumIntegerDigits(minInt);
-          properties.setMaximumIntegerDigits(Properties.DEFAULT_MAXIMUM_INTEGER_DIGITS);
-        }
-
-        // Padding settings
-        if (positive.padding.length() > 0) {
-          // The width of the positive prefix and suffix templates are included in the padding
-          int paddingWidth =
-              positive.paddingWidth
-                  + AffixPatternUtils.unescapedLength(positive.prefix)
-                  + AffixPatternUtils.unescapedLength(positive.suffix);
-          properties.setFormatWidth(paddingWidth);
-          if (positive.padding.length() == 1) {
-            properties.setPadString(positive.padding.toString());
-          } else if (positive.padding.length() == 2) {
-            if (positive.padding.charAt(0) == '\'') {
-              properties.setPadString("'");
-            } else {
-              properties.setPadString(positive.padding.toString());
-            }
-          } else {
-            properties.setPadString(
-                positive.padding.subSequence(1, positive.padding.length() - 1).toString());
-          }
-          assert positive.paddingLocation != null;
-          properties.setPadPosition(positive.paddingLocation);
-        } else {
-          properties.setFormatWidth(Properties.DEFAULT_FORMAT_WIDTH);
-          properties.setPadString(Properties.DEFAULT_PAD_STRING);
-          properties.setPadPosition(Properties.DEFAULT_PAD_POSITION);
-        }
-
-        // Set the affixes
-        // Always call the setter, even if the prefixes are empty, especially in the case of the
-        // negative prefix pattern, to prevent default values from overriding the pattern.
-        properties.setPositivePrefixPattern(positive.prefix.toString());
-        properties.setPositiveSuffixPattern(positive.suffix.toString());
-        if (negative != null) {
-          properties.setNegativePrefixPattern(negative.prefix.toString());
-          properties.setNegativeSuffixPattern(negative.suffix.toString());
-        } else {
-          properties.setNegativePrefixPattern(null);
-          properties.setNegativeSuffixPattern(null);
-        }
-
-        // Set the magnitude multiplier
-        if (positive.hasPercentSign) {
-          properties.setMagnitudeMultiplier(2);
-        } else if (positive.hasPerMilleSign) {
-          properties.setMagnitudeMultiplier(3);
-        } else {
-          properties.setMagnitudeMultiplier(Properties.DEFAULT_MAGNITUDE_MULTIPLIER);
-        }
-      }
+  static void parse(String pattern, Properties properties, int ignoreRounding) {
+    if (pattern == null || pattern.length() == 0) {
+      // Backwards compatibility requires that we reset to the default values.
+      // TODO: Only overwrite the properties that "saveToProperties" normally touches?
+      properties.clear();
+      return;
     }
 
-    private static class SubpatternParseResult {
-      int[] groupingSizes = new int[] {0, -1, -1};
-      int minimumIntegerDigits = 0;
-      int totalIntegerDigits = 0;
-      int minimumFractionDigits = 0;
-      int maximumFractionDigits = 0;
-      int minimumSignificantDigits = 0;
-      int maximumSignificantDigits = 0;
-      boolean hasDecimal = false;
-      int paddingWidth = 0;
-      PadPosition paddingLocation = null;
-      FormatQuantity4 rounding = new FormatQuantity4();
-      boolean exponentShowPlusSign = false;
-      int exponentDigits = 0;
-      boolean hasPercentSign = false;
-      boolean hasPerMilleSign = false;
-      boolean hasCurrencySign = false;
-
-      StringBuilder padding = new StringBuilder();
-      StringBuilder prefix = new StringBuilder();
-      StringBuilder suffix = new StringBuilder();
-    }
-
-    /** An internal class used for tracking the cursor during parsing of a pattern string. */
-    private static class ParserState {
-      final String pattern;
-      int offset;
-
-      ParserState(String pattern) {
-        this.pattern = pattern;
-        this.offset = 0;
-      }
-
-      int peek() {
-        if (offset == pattern.length()) {
-          return -1;
-        } else {
-          return pattern.codePointAt(offset);
-        }
-      }
-
-      int next() {
-        int codePoint = peek();
-        offset += Character.charCount(codePoint);
-        return codePoint;
-      }
+    // TODO: Use whitespace characters from PatternProps
+    // TODO: Use thread locals here.
+    LdmlPatternInfo.PatternParseResult result = LdmlPatternInfo.parse(pattern);
+    saveToProperties(properties, result, ignoreRounding);
+  }
 
-      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());
-      }
+  /** Finalizes the temporary data stored in the PatternParseResult to the Builder. */
+  private static void saveToProperties(
+      Properties properties, LdmlPatternInfo.PatternParseResult ppr, int _ignoreRounding) {
+    // Translate from PatternParseResult to Properties.
+    // Note that most data from "negative" is ignored per the specification of DecimalFormat.
+
+    LdmlPatternInfo.SubpatternParseResult positive = ppr.positive;
+    LdmlPatternInfo.SubpatternParseResult negative = ppr.negative;
+    String pattern = ppr.pattern;
+
+    boolean ignoreRounding;
+    if (_ignoreRounding == IGNORE_ROUNDING_NEVER) {
+      ignoreRounding = false;
+    } else if (_ignoreRounding == IGNORE_ROUNDING_IF_CURRENCY) {
+      ignoreRounding = positive.hasCurrencySign;
+    } else {
+      assert _ignoreRounding == IGNORE_ROUNDING_ALWAYS;
+      ignoreRounding = true;
     }
 
-    static void parse(String pattern, Properties properties, int ignoreRounding) {
-      if (pattern == null || pattern.length() == 0) {
-        // Backwards compatibility requires that we reset to the default values.
-        // TODO: Only overwrite the properties that "saveToProperties" normally touches?
-        properties.clear();
-        return;
-      }
-
-      // TODO: Use whitespace characters from PatternProps
-      // TODO: Use thread locals here.
-      ParserState state = new ParserState(pattern);
-      PatternParseResult result = new PatternParseResult();
-      consumePattern(state, result);
-      result.saveToProperties(properties, ignoreRounding);
+    // Grouping settings
+    short grouping1 = (short) (positive.groupingSizes & 0xffff);
+    short grouping2 = (short) ((positive.groupingSizes >>> 16) & 0xffff);
+    short grouping3 = (short) ((positive.groupingSizes >>> 32) & 0xffff);
+    if (grouping2 != -1) {
+      properties.setGroupingSize(grouping1);
+    } else {
+      properties.setGroupingSize(Properties.DEFAULT_GROUPING_SIZE);
     }
-
-    private static void consumePattern(ParserState state, PatternParseResult result) {
-      // pattern := subpattern (';' subpattern)?
-      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");
-      }
+    if (grouping3 != -1) {
+      properties.setSecondaryGroupingSize(grouping2);
+    } else {
+      properties.setSecondaryGroupingSize(Properties.DEFAULT_SECONDARY_GROUPING_SIZE);
     }
 
-    private static void consumeSubpattern(ParserState state, SubpatternParseResult result) {
-      // subpattern := literals? number exponent? literals?
-      consumePadding(state, result, PadPosition.BEFORE_PREFIX);
-      consumeAffix(state, result, result.prefix);
-      consumePadding(state, result, PadPosition.AFTER_PREFIX);
-      consumeFormat(state, result);
-      consumeExponent(state, result);
-      consumePadding(state, result, PadPosition.BEFORE_SUFFIX);
-      consumeAffix(state, result, result.suffix);
-      consumePadding(state, result, PadPosition.AFTER_SUFFIX);
+    // For backwards compatibility, require that the pattern emit at least one min digit.
+    int minInt, minFrac;
+    if (positive.totalIntegerDigits == 0 && positive.maximumFractionDigits > 0) {
+      // patterns like ".##"
+      minInt = 0;
+      minFrac = Math.max(1, positive.minimumFractionDigits);
+    } else if (positive.minimumIntegerDigits == 0 && positive.minimumFractionDigits == 0) {
+      // patterns like "#.##"
+      minInt = 1;
+      minFrac = 0;
+    } else {
+      minInt = positive.minimumIntegerDigits;
+      minFrac = positive.minimumFractionDigits;
     }
 
-    private static void consumePadding(
-        ParserState state, SubpatternParseResult result, PadPosition paddingLocation) {
-      if (state.peek() != '*') {
-        return;
+    // Rounding settings
+    // Don't set basic rounding when there is a currency sign; defer to CurrencyUsage
+    if (positive.minimumSignificantDigits > 0) {
+      properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS);
+      properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS);
+      properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
+      properties.setMinimumSignificantDigits(positive.minimumSignificantDigits);
+      properties.setMaximumSignificantDigits(positive.maximumSignificantDigits);
+    } else if (positive.rounding != null) {
+      if (!ignoreRounding) {
+        properties.setMinimumFractionDigits(minFrac);
+        properties.setMaximumFractionDigits(positive.maximumFractionDigits);
+        properties.setRoundingIncrement(positive.rounding.toBigDecimal());
+      } else {
+        properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS);
+        properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS);
+        properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
       }
-      result.paddingLocation = paddingLocation;
-      state.next(); // consume the '*'
-      consumeLiteral(state, result.padding);
-    }
-
-    private static void consumeAffix(
-        ParserState state, SubpatternParseResult result, StringBuilder destination) {
-      // literals := { literal }
-      while (true) {
-        switch (state.peek()) {
-          case '#':
-          case '@':
-          case ';':
-          case '*':
-          case '.':
-          case ',':
-          case '0':
-          case '1':
-          case '2':
-          case '3':
-          case '4':
-          case '5':
-          case '6':
-          case '7':
-          case '8':
-          case '9':
-          case -1:
-            // Characters that cannot appear unquoted in a literal
-            return;
-
-          case '%':
-            result.hasPercentSign = true;
-            break;
-
-          case '‰':
-            result.hasPerMilleSign = true;
-            break;
-
-          case '¤':
-            result.hasCurrencySign = true;
-            break;
-        }
-        consumeLiteral(state, destination);
+      properties.setMinimumSignificantDigits(Properties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS);
+      properties.setMaximumSignificantDigits(Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS);
+    } else {
+      if (!ignoreRounding) {
+        properties.setMinimumFractionDigits(minFrac);
+        properties.setMaximumFractionDigits(positive.maximumFractionDigits);
+        properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
+      } else {
+        properties.setMinimumFractionDigits(Properties.DEFAULT_MINIMUM_FRACTION_DIGITS);
+        properties.setMaximumFractionDigits(Properties.DEFAULT_MAXIMUM_FRACTION_DIGITS);
+        properties.setRoundingIncrement(Properties.DEFAULT_ROUNDING_INCREMENT);
       }
+      properties.setMinimumSignificantDigits(Properties.DEFAULT_MINIMUM_SIGNIFICANT_DIGITS);
+      properties.setMaximumSignificantDigits(Properties.DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS);
     }
 
-    private static void consumeLiteral(ParserState state, StringBuilder destination) {
-      if (state.peek() == -1) {
-        throw state.toParseException("Expected unquoted literal but found EOL");
-      } else if (state.peek() == '\'') {
-        destination.appendCodePoint(state.next()); // consume the starting quote
-        while (state.peek() != '\'') {
-          if (state.peek() == -1) {
-            throw state.toParseException("Expected quoted literal but found EOL");
-          } else {
-            destination.appendCodePoint(state.next()); // consume a quoted character
-          }
-        }
-        destination.appendCodePoint(state.next()); // consume the ending quote
-      } else {
-        // consume a non-quoted literal character
-        destination.appendCodePoint(state.next());
-      }
+    // If the pattern ends with a '.' then force the decimal point.
+    if (positive.hasDecimal && positive.maximumFractionDigits == 0) {
+      properties.setDecimalSeparatorAlwaysShown(true);
+    } else {
+      properties.setDecimalSeparatorAlwaysShown(false);
     }
 
-    private static void consumeFormat(ParserState state, SubpatternParseResult result) {
-      consumeIntegerFormat(state, result);
-      if (state.peek() == '.') {
-        state.next(); // consume the decimal point
-        result.hasDecimal = true;
-        result.paddingWidth += 1;
-        consumeFractionFormat(state, result);
+    // Scientific notation settings
+    if (positive.exponentDigits > 0) {
+      properties.setExponentSignAlwaysShown(positive.exponentShowPlusSign);
+      properties.setMinimumExponentDigits(positive.exponentDigits);
+      if (positive.minimumSignificantDigits == 0) {
+        // patterns without '@' can define max integer digits, used for engineering notation
+        properties.setMinimumIntegerDigits(positive.minimumIntegerDigits);
+        properties.setMaximumIntegerDigits(positive.totalIntegerDigits);
+      } else {
+        // patterns with '@' cannot define max integer digits
+        properties.setMinimumIntegerDigits(1);
+        properties.setMaximumIntegerDigits(Properties.DEFAULT_MAXIMUM_INTEGER_DIGITS);
       }
+    } else {
+      properties.setExponentSignAlwaysShown(Properties.DEFAULT_EXPONENT_SIGN_ALWAYS_SHOWN);
+      properties.setMinimumExponentDigits(Properties.DEFAULT_MINIMUM_EXPONENT_DIGITS);
+      properties.setMinimumIntegerDigits(minInt);
+      properties.setMaximumIntegerDigits(Properties.DEFAULT_MAXIMUM_INTEGER_DIGITS);
     }
 
-    private static void consumeIntegerFormat(ParserState state, SubpatternParseResult result) {
-      boolean seenSignificantDigitMarker = false;
-      boolean seenDigit = false;
-
-      outer:
-      while (true) {
-        switch (state.peek()) {
-          case ',':
-            result.paddingWidth += 1;
-            result.groupingSizes[2] = result.groupingSizes[1];
-            result.groupingSizes[1] = result.groupingSizes[0];
-            result.groupingSizes[0] = 0;
-            break;
-
-          case '#':
-            if (seenDigit) throw state.toParseException("# cannot follow 0 before decimal point");
-            result.paddingWidth += 1;
-            result.groupingSizes[0] += 1;
-            result.totalIntegerDigits += (seenSignificantDigitMarker ? 0 : 1);
-            // no change to result.minimumIntegerDigits
-            // no change to result.minimumSignificantDigits
-            result.maximumSignificantDigits += (seenSignificantDigitMarker ? 1 : 0);
-            result.rounding.appendDigit((byte) 0, 0, true);
-            break;
-
-          case '@':
-            seenSignificantDigitMarker = true;
-            if (seenDigit) throw state.toParseException("Cannot mix 0 and @");
-            result.paddingWidth += 1;
-            result.groupingSizes[0] += 1;
-            result.totalIntegerDigits += 1;
-            // no change to result.minimumIntegerDigits
-            result.minimumSignificantDigits += 1;
-            result.maximumSignificantDigits += 1;
-            result.rounding.appendDigit((byte) 0, 0, true);
-            break;
-
-          case '0':
-          case '1':
-          case '2':
-          case '3':
-          case '4':
-          case '5':
-          case '6':
-          case '7':
-          case '8':
-          case '9':
-            seenDigit = true;
-            if (seenSignificantDigitMarker) throw state.toParseException("Cannot mix @ and 0");
-            // TODO: Crash here if we've seen the significant digit marker? See NumberFormatTestCases.txt
-            result.paddingWidth += 1;
-            result.groupingSizes[0] += 1;
-            result.totalIntegerDigits += 1;
-            result.minimumIntegerDigits += 1;
-            // no change to result.minimumSignificantDigits
-            // no change to result.maximumSignificantDigits
-            result.rounding.appendDigit((byte) (state.peek() - '0'), 0, true);
-            break;
-
-          default:
-            break outer;
+    // Compute the affix patterns (required for both padding and affixes)
+    String posPrefix = ppr.getString(LdmlPatternInfo.PatternParseResult.POS_PREFIX);
+    String posSuffix = ppr.getString(LdmlPatternInfo.PatternParseResult.POS_SUFFIX);
+
+    // Padding settings
+    if (positive.paddingEndpoints != 0) {
+      // The width of the positive prefix and suffix templates are included in the padding
+      int paddingWidth =
+          positive.paddingWidth
+              + AffixPatternUtils.unescapedLength(posPrefix)
+              + AffixPatternUtils.unescapedLength(posSuffix);
+      properties.setFormatWidth(paddingWidth);
+      String rawPaddingString = ppr.getString(LdmlPatternInfo.PatternParseResult.POS_PADDING);
+      if (rawPaddingString.length() == 1) {
+        properties.setPadString(rawPaddingString);
+      } else if (rawPaddingString.length() == 2) {
+        if (rawPaddingString.charAt(0) == '\'') {
+          properties.setPadString("'");
+        } else {
+          properties.setPadString(rawPaddingString);
         }
-        state.next(); // consume the symbol
-      }
-
-      // Disallow patterns with a trailing ',' or with two ',' next to each other
-      if (result.groupingSizes[0] == 0 && result.groupingSizes[1] != -1) {
-        throw state.toParseException("Trailing grouping separator is invalid");
-      }
-      if (result.groupingSizes[1] == 0 && result.groupingSizes[2] != -1) {
-        throw state.toParseException("Grouping width of zero is invalid");
+      } else {
+        properties.setPadString(rawPaddingString.substring(1, rawPaddingString.length() - 1));
       }
+      assert positive.paddingLocation != null;
+      properties.setPadPosition(positive.paddingLocation);
+    } else {
+      properties.setFormatWidth(Properties.DEFAULT_FORMAT_WIDTH);
+      properties.setPadString(Properties.DEFAULT_PAD_STRING);
+      properties.setPadPosition(Properties.DEFAULT_PAD_POSITION);
     }
 
-    private static void consumeFractionFormat(ParserState state, SubpatternParseResult result) {
-      int zeroCounter = 0;
-      boolean seenHash = false;
-      while (true) {
-        switch (state.peek()) {
-          case '#':
-            seenHash = true;
-            result.paddingWidth += 1;
-            // no change to result.minimumFractionDigits
-            result.maximumFractionDigits += 1;
-            zeroCounter++;
-            break;
-
-          case '0':
-          case '1':
-          case '2':
-          case '3':
-          case '4':
-          case '5':
-          case '6':
-          case '7':
-          case '8':
-          case '9':
-            if (seenHash) throw state.toParseException("0 cannot follow # after decimal point");
-            result.paddingWidth += 1;
-            result.minimumFractionDigits += 1;
-            result.maximumFractionDigits += 1;
-            if (state.peek() == '0') {
-              zeroCounter++;
-            } else {
-              result.rounding.appendDigit((byte) (state.peek() - '0'), zeroCounter, false);
-              zeroCounter = 0;
-            }
-            break;
-
-          default:
-            return;
-        }
-        state.next(); // consume the symbol
-      }
+    // Set the affixes
+    // Always call the setter, even if the prefixes are empty, especially in the case of the
+    // negative prefix pattern, to prevent default values from overriding the pattern.
+    properties.setPositivePrefixPattern(posPrefix);
+    properties.setPositiveSuffixPattern(posSuffix);
+    if (negative != null) {
+      properties.setNegativePrefixPattern(ppr.getString(LdmlPatternInfo.PatternParseResult.NEG_PREFIX));
+      properties.setNegativeSuffixPattern(ppr.getString(LdmlPatternInfo.PatternParseResult.NEG_SUFFIX));
+    } else {
+      properties.setNegativePrefixPattern(null);
+      properties.setNegativeSuffixPattern(null);
     }
 
-    private static void consumeExponent(ParserState state, SubpatternParseResult result) {
-      if (state.peek() != 'E') {
-        return;
-      }
-      state.next(); // consume the E
-      result.paddingWidth++;
-      if (state.peek() == '+') {
-        state.next(); // consume the +
-        result.exponentShowPlusSign = true;
-        result.paddingWidth++;
-      }
-      while (state.peek() == '0') {
-        state.next(); // consume the 0
-        result.exponentDigits += 1;
-        result.paddingWidth++;
-      }
+    // Set the magnitude multiplier
+    if (positive.hasPercentSign) {
+      properties.setMagnitudeMultiplier(2);
+    } else if (positive.hasPerMilleSign) {
+      properties.setMagnitudeMultiplier(3);
+    } else {
+      properties.setMagnitudeMultiplier(Properties.DEFAULT_MAGNITUDE_MULTIPLIER);
     }
   }
 }
index e878870fca57af74e3e6974d39a5b9a072c49f75..f13b46292bd6499296b1146dce8d1da28d0ef74a 100644 (file)
@@ -231,7 +231,8 @@ public abstract class Rounder extends Format.BeforeFormat {
    * @param input The digits being formatted.
    */
   protected void applyDefaults(FormatQuantity input) {
-    input.setIntegerFractionLength(minInt, maxInt, minFrac, maxFrac);
+    input.setIntegerLength(minInt, maxInt);
+    input.setFractionLength(minFrac, maxFrac);
   }
 
   @Override
index 80ff29e3cc7435127d4bf72a6f713e51daccb627..6594c5ee39066373b47901b8f6ddf8773fb34b25 100644 (file)
@@ -162,4 +162,15 @@ public class RoundingUtils {
     }
     return mathContext;
   }
+
+  /**
+   * Gets a MathContext with unlimited precision and the specified RoundingMode. Equivalent to "new
+   * MathContext(0, roundingMode)", but pulls from a singleton to prevent object thrashing.
+   *
+   * @param roundingMode The {@link RoundingMode} to use.
+   * @return The corresponding {@link MathContext}.
+   */
+  public static MathContext mathContextUnlimited(RoundingMode roundingMode) {
+    return MATH_CONTEXT_BY_ROUNDING_MODE_UNLIMITED[roundingMode.ordinal()];
+  }
 }
index f62820199bf0bf07aa15d3b14540a6dbbdcd8350..78926550bbbb5c1e37e25f009b844ccb0bde33d1 100644 (file)
@@ -198,7 +198,8 @@ public class ScientificFormat extends Format.BeforeFormat implements Rounder.Mul
     FormatQuantity exponentQ = FormatQuantitySelector.from(exponent);
     StringBuilder exponentSB = threadLocalStringBuilder.get();
     exponentSB.setLength(0);
-    exponentQ.setIntegerFractionLength(exponentDigits, Integer.MAX_VALUE, 0, 0);
+    exponentQ.setIntegerLength(exponentDigits, Integer.MAX_VALUE);
+    exponentQ.setFractionLength(0, 0);
     for (int i = exponentQ.getUpperDisplayMagnitude(); i >= 0; i--) {
       exponentSB.append(digitStrings[exponentQ.getDigit(i)]);
     }
index f1850a7a869c51ae0fb70b844710e169fbdafd68..7959b8973b8218d9f51d133a7335f661fceff6c2 100644 (file)
@@ -180,17 +180,18 @@ public class SignificantDigitsRounder extends Rounder {
     switch (mode) {
       case OVERRIDE_MAXIMUM_FRACTION:
         // Ensure minSig is always displayed.
-        input.setIntegerFractionLength(
-            minInt, maxInt, Math.max(minFrac, -magMinSig), Integer.MAX_VALUE);
+        input.setIntegerLength(minInt, maxInt);
+        input.setFractionLength(Math.max(minFrac, -magMinSig), Integer.MAX_VALUE);
         break;
       case RESPECT_MAXIMUM_FRACTION:
         // Ensure minSig is displayed, unless doing so is in violation of maxFrac.
-        input.setIntegerFractionLength(
-            minInt, maxInt, Math.min(maxFrac, Math.max(minFrac, -magMinSig)), maxFrac);
+        input.setIntegerLength(minInt, maxInt);
+        input.setFractionLength(Math.min(maxFrac, Math.max(minFrac, -magMinSig)), maxFrac);
         break;
       case ENSURE_MINIMUM_SIGNIFICANT:
         // Follow minInt/minFrac, but ensure all digits are allowed to be visible.
-        input.setIntegerFractionLength(minInt, maxInt, minFrac, Integer.MAX_VALUE);
+        input.setIntegerLength(minInt, maxInt);
+        input.setFractionLength(minFrac, Integer.MAX_VALUE);
         break;
     }
   }
index 72c0a55d265f380b1f5a79dcd49629a4151c78b7..4f5e6a7f8151cf15149feb621905bf275bbfe014 100644 (file)
@@ -8,6 +8,7 @@ import static org.junit.Assert.fail;
 import org.junit.Test;
 
 import com.ibm.icu.impl.number.AffixPatternUtils;
+import com.ibm.icu.impl.number.AffixPatternUtils.SymbolProvider;
 import com.ibm.icu.impl.number.NumberStringBuilder;
 import com.ibm.icu.text.DecimalFormatSymbols;
 import com.ibm.icu.util.ULocale;
@@ -63,6 +64,7 @@ public class AffixPatternUtilsTest {
       {"¤¤¤", true, 3, "long name"},
       {"¤¤¤¤", true, 4, "\uFFFD"},
       {"¤¤¤¤¤", true, 5, "\uFFFD"},
+      {"¤¤¤¤¤¤", true, 6, "\uFFFD"},
       {"¤¤¤a¤¤¤¤", true, 8, "long namea\uFFFD"},
       {"a¤¤¤¤b¤¤¤¤¤c", true, 12, "a\uFFFDb\uFFFDc"},
       {"¤!", true, 2, "$!"},
@@ -73,7 +75,8 @@ public class AffixPatternUtilsTest {
       {"'¤'", false, 1, "¤"},
       {"%", false, 1, "٪\u061C"},
       {"'%'", false, 1, "%"},
-      {"¤'-'%", true, 3, "$-٪\u061C"}
+      {"¤'-'%", true, 3, "$-٪\u061C"},
+      {"#0#@#*#;#", false, 9, "#0#@#*#;#"}
     };
 
     // ar_SA has an interesting percent sign and various Arabic letter marks
@@ -151,4 +154,39 @@ public class AffixPatternUtilsTest {
       }
     }
   }
+
+  @Test
+  public void testUnescapeWithSymbolProvider() {
+    String[][] cases = {
+      {"", ""},
+      {"-", "1"},
+      {"'-'", "-"},
+      {"- + % ‰ ¤ ¤¤ ¤¤¤ ¤¤¤¤ ¤¤¤¤¤", "1 2 3 4 5 6 7 8 9"},
+      {"'¤¤¤¤¤¤'", "¤¤¤¤¤¤"},
+      {"¤¤¤¤¤¤", "\uFFFD"}
+    };
+
+    SymbolProvider provider =
+        new SymbolProvider() {
+          @Override
+          public CharSequence getSymbol(int type) {
+            return Integer.toString(Math.abs(type));
+          }
+        };
+
+    NumberStringBuilder sb = new NumberStringBuilder();
+    for (String[] cas : cases) {
+      String input = cas[0];
+      String expected = cas[1];
+      sb.clear();
+      AffixPatternUtils.unescape(input, sb, 0, provider);
+      assertEquals("With symbol provider on <" + input + ">", expected, sb.toString());
+    }
+
+    // Test insertion position
+    sb.clear();
+    sb.append("abcdefg", null);
+    AffixPatternUtils.unescape("-+%", sb, 4, provider);
+    assertEquals("Symbol provider into middle", "abcd123efg", sb.toString());
+  }
 }
index baadfdca31d60dfa6bb2243364e44a5530afcabb..45bc3b5c3461b8bd13edf29c6ffce76620c63d09 100644 (file)
@@ -162,7 +162,8 @@ public class FormatQuantityTest extends TestFmwk {
     FormatQuantity q0 = rq.createCopy();
     // Force an accurate double
     q0.roundToInfinity();
-    q0.setIntegerFractionLength(1, Integer.MAX_VALUE, 1, Integer.MAX_VALUE);
+    q0.setIntegerLength(1, Integer.MAX_VALUE);
+    q0.setFractionLength(1, Integer.MAX_VALUE);
     for (int m = q0.getUpperDisplayMagnitude(); m >= q0.getLowerDisplayMagnitude(); m--) {
       sb.append(q0.getDigit(m));
       if (m == 0) sb.append('.');